Лог №10
Содержание
Вспоминаем прошлое: как почистить HTML
Мы можем запихать весь наш код для чистки HTML, который мы писали в прошлый раз, в функцию. Это удобно:
>>> with open("Молоко — Википедия.html", encoding='utf-8') as file: text = file.read() >>> clean(text) ерундакакаято
На самом деле, всё работает. Печаль, правда, в том, что мы недочистили строку (стили и скрипты открываются тэгами <style> и <script> с дополнительными аргументами, поэтому <style> и <script> их не отлавливают), и получили огромный кусок текста (в котором ещё и несколько картинок зашифровано), с такими огромным текстом IDLE обращается очень плохо. Очень плохо. Настолько плохо, что можно полчаса на них глядеть и не видеть, что здесь есть всё, что нужно. Поэтому правильнее записать результат сразу в файл, и смотреть на файл каким-нибудь более удачным текстовым редактором:
>>> with open("Молоко — Википедия.html", encoding='utf-8') as file: text = file.read() >>> text = clean(text) >>> with open("clean.txt", "w", encoding='utf-8') as file: file.write(text)
Теперь всё видно.
Как скачивать данные из сети
В питоне это очень просто.
Есть модуль urllib.request, и в нём есть функция urlopen, которая позволяет нам открыть страницу в сети в точности так, как мы открываем файл:
Как и с файлами, лучше пользоваться with. И, конечно, каждый раз писать всё название этой функции со всеми регалиями не хочется, поэтому мы будем импортировать его по-другому:
Одного этого вполне достаточно, чтобы уже уметь делать очень много.
Ещё раз про кодировки: bytes.decode()
1 >>> from urllib.request import urlopen
2 >>> with urlopen('http://ya.ru') as file:
3 text = file.read()
4
5 >>> with open('xyz.html', 'w', encoding='utf-8') as file:
6 file.write(text)
7
8 Traceback (most recent call last):
9 File "<pyshell#45>", line 2, in <module>
10 file.write(text)
11 TypeError: must be str, not bytes
Дело в том, что urllib не умеет самостоятельно определять кодировку, в которой пришли данные и расшифровывать их. У нас есть два варианта:
- либо открывать выходной файл, не указывая кодировку (то есть просто не давать аргумент encoding функции open), тогда мы будем писать в файл поток байт, не имея на руках представления его в виде букв,
- либо при чтении страницы расшифровывать байты и получать из них строку; для этого у байтовых объектов есть метод decode, который хочет от нас знать название кодировки.
Попробуем второй подход:
Ура, вышло! И в файле всё, что нужно есть!
Два слова про устройство URL
URL – адрес ресурса в Интернете. Аббревиатура URL расшифровывается Universla Resource Locator (универсальный "находитель" ресурсов... ну и язык у них!).
Устроен URL в самом общем случае таким образом: название протокола : адрес.
Нас из адресов интересуют только такие, которые работают с протоколами http, https или ftp. У них есть куда более длинная общая часть: название протокола : домен / путь ? параметры запроса # подраздел страницы
http://yandex.ru/yandsearch?text="битон"&lr=103421 Протокол – это язык, на котором разговаривают между собой компьютеры. (В отличие от языков программирования и разметки – которыми разговаривают с компьютерами люди). Протоколы бывают читаемые для человека и нечитаемые (программисты любят читаемые: с ними гораздо проще отлаживать программу). Когда две программы разговаривают друг с другом, в зависимости от того, чем эти программы занимаются, у них есть разные требования к тому, какими данными обмениваться и какой смысл им придавать. Для веб-сервисов более-менее единственное взаимодействие такое:
браузер – серверу: дай мне страницу по такому-то адресу, я умею понимать такие языки и такие способы сжатия информации
сервер – браузеру: ок, страница нашлась, она в таком-то формате, в такой-то кодировке, вот тебе текст файла
Так протокол и устроен протокол http.
Домен – адрес компьютера в сети. Чтобы с ним соединиться, нужно сначала читаемое имя домена преобразовать в набор чисел – ip-адрес. За это отвечает система DNS или name service – если с сетью проблемы, то именно от неё вы увидите сообщение об ошибке.
В примере домен yandex.ru
Путь – мы можем думать о нём как о пути к файлу внутри сервера, на котором хранится страница. (На деле это часто оказывается несколько более виртуальным понятием, просто способом организации данных на сервер)
В примере путь /yandsearch
Про параметры запроса и подраздел страницы мы ещё поговорим. В нашем примере параметры запроса выглядят так: text="битон"&lr=103421, а подраздела страницы у нас нет.
Пример: скачать все описания городов
Пусть в файле words.txt у нас лежит список городов.1
Немножко приглядевшись к википедии, мы обнаруживаем, что все адреса страниц имеют один и тот же вид: http://ru.wikipedia.org/wiki/Название_Страницы. Значит, всё, что нам нужно сделать, это:
- прочитать файл со списком городов, получить из него список названий
- для каждого названия сконструировать адрес и прочитать
Так как нам предстоит скачать много кусочков текста очень большого суммарного объёма, то создадим отдельную папку cities рядом с нашей программой, и будем складывать тексты страниц в неё.
Итак, берём наш кусочек кода для чистки html:
Прибавляем чтение списка городов:
Кстати, проверим сразу, что мы всё угадали, и с кодировками всё так. запустим программу и выполним:
1 >>> print(len(cities), cities[:10])
Работает? Осталось приделать проход по списку городов, собирание адреса и запись в файл:
Итого:
1 import re
2 from urllib.request import urlopen
3
4 def clean(html):
5 noscript = re.sub('<script>[^<>]*</script>', '', html)
6 nostyle = re.sub('<style>[^<>]*</style>', '', noscript)
7 notags = re.sub('<[^>]*>', '', nostyle)
8 text = re.sub('[&][^;]*;', ' ', notags)
9 text = re.sub(r'\s+', ' ', text)
10 return text
11
12 with open('words.txt', encoding='cp1251') as file:
13 cities = file.read().split()
14
15 for city in cities:
16 with urlopen('http://ru.wikipedia.org/wiki/' + city) as file:
17 text = file.read()
18 clean(text)
19 with open(city + '.txt', 'w', encoding='utf-8') as file:
20 file.write(text)
URL-encoding
При запуске мы получаем сообщени об ошибке про кодировке из строки с urlopen.
Traceback (most recent call last): File "C:\Users\dendik\Desktop\learn-python\example.py", line 285, in <module> with urlopen('http://ru.wikipedia.org/wiki/' + city) as file: File "C:\Python34\lib\urllib\request.py", line 153, in urlopen return opener.open(url, data, timeout) File "C:\Python34\lib\urllib\request.py", line 455, in open response = self._open(req, data) File "C:\Python34\lib\urllib\request.py", line 473, in _open '_open', req) File "C:\Python34\lib\urllib\request.py", line 433, in _call_chain result = func(*args) File "C:\Python34\lib\urllib\request.py", line 1202, in http_open return self.do_open(http.client.HTTPConnection, req) File "C:\Python34\lib\urllib\request.py", line 1174, in do_open h.request(req.get_method(), req.selector, req.data, headers) File "C:\Python34\lib\http\client.py", line 1090, in request self._send_request(method, url, body, headers) File "C:\Python34\lib\http\client.py", line 1118, in _send_request self.putrequest(method, url, **skips) File "C:\Python34\lib\http\client.py", line 975, in putrequest self._output(request.encode('ascii')) UnicodeEncodeError: 'ascii' codec can't encode characters in position 10-12: ordinal not in range(128)
Дело вот в чём. Стандарт RFC 3986 не предполагает в URL никаких букв, кроме латинских. Вместо этого в нём предусмотрен способ кодирования нелатинских букв специальной кодировкой. Например, адрес https://ru.wikipedia.org/wiki/Битон выглядит в этой кодировке так: https://ru.wikipedia.org/wiki/%D0%91%D0%B8%D1%82%D0%BE%D0%BD. (Нетрудно догадаться, что % и две шестнадцатеричных цифры обозначает значение одного байта, и столь же нетрудно вычислить, что одна русская буква кодируется здесь двумя байтами, например %D0%91 обозначает букву Б). Браузеры считают оба представления адресов абсолютно эквивалентными. А вот urllib здесь требует строгого соблюдения стандарта.
Так что нам нужно научиться название города переводить в такую кодировку. Это делает функция urllib.parse.quote.
Поменяем
1 with urlopen('http://ru.wikipedia.org/wiki/' + city) as file:
на
1 with urlopen('http://ru.wikipedia.org/wiki/' + quote(city)) as file:
Запустим. Снова ошибка с кодировкой.
Traceback (most recent call last): File "C:\Users\dendik\Desktop\learn-python\example.py", line 288, in <module> text = clean(text) File "C:\Users\dendik\Desktop\learn-python\example.py", line 275, in clean noscript = re.sub('<script>[^<>]*</script>', '', html) File "C:\Python34\lib\re.py", line 175, in sub return _compile(pattern, flags).sub(repl, string, count) TypeError: can't use a string pattern on a bytes-like object
Значит, text оказался не строкой? А, ну да, мы же это только что проходили: urllib не умеет сам расшифровывать кодировки!
Меняем:
1 text = file.read()
1 text = file.read().decode('utf-8')
Запустив программу, мы не видим, чтобы файлы создавались в папочке cities, которую мы создали... Да ещё и обнаруживаем, что у нас создалось куча мусора там же, где лежит программа. Ну да, конечно, мы же и не сказали питону, где файлы писать. Вспоминаем прошлое занятие про пути. Самый простой способ добиться нужного поведения – приписать к путю к файлу имя папки (раз папка лежит рядом с программой, то только имени достаточно, получается корректный относительный путь). Меняем:
1 with open(city + '.txt', 'w', encoding='utf-8') as file:
на:
1 with open('cities/' + city + '.txt', 'w', encoding='utf-8') as file:
В итоге получаем:
1 import re
2 from urllib.request import urlopen
3 from urllib.parse import quote
4
5 def clean(html):
6 noscript = re.sub('<script>[^<>]*</script>', '', html)
7 nostyle = re.sub('<style>[^<>]*</style>', '', noscript)
8 notags = re.sub('<[^>]*>', '', nostyle)
9 text = re.sub('[&][^;]*;', ' ', notags)
10 text = re.sub(r'\s+', ' ', text)
11 return text
12
13 with open('words.txt', encoding='cp1251') as file:
14 cities = file.read().split()
15
16 for city in cities:
17 with urlopen('http://ru.wikipedia.org/wiki/' + quote(city)) as file:
18 text = file.read().decode('utf-8')
19 text = clean(text)
20 with open('cities/' + city + '.txt', 'w', encoding='utf-8') as file:
21 file.write(text)
Немножко работает, а потом падает с ошибкой HTTPError 404.
Traceback (most recent call last): File "C:\Users\dendik\Desktop\learn-python\example.py", line 286, in <module> with urlopen('http://ru.wikipedia.org/wiki/' + quote(city)) as file: File "C:\Python34\lib\urllib\request.py", line 153, in urlopen return opener.open(url, data, timeout) File "C:\Python34\lib\urllib\request.py", line 461, in open response = meth(req, response) File "C:\Python34\lib\urllib\request.py", line 571, in http_response 'http', request, response, code, msg, hdrs) File "C:\Python34\lib\urllib\request.py", line 499, in error return self._call_chain(*args) File "C:\Python34\lib\urllib\request.py", line 433, in _call_chain result = func(*args) File "C:\Python34\lib\urllib\request.py", line 579, in http_error_default raise HTTPError(req.full_url, code, msg, hdrs, fp) urllib.error.HTTPError: HTTP Error 404: Not Found
Протокол http описывает множество кодов ошибок, среди которых полезно знать узнавать числа 200 (отсутствие ошибки, всё ОК), 404 (страница не найдена), 403 (формально "нет прав", на деле оно значит, что автор решил удалить ресурс), 500 (сервер сломался).
Кусочек теории: исключения
Теперь нам хочется продолжать исполнять питонский код несмотря на то, что в нём случилась ошибка. Да, страницу с ошибкой мы скачать не сможем, но мы можем пойти дальше.
Хочется сказать питону: попробуй скачать страницу, но если не получилось, то и ладно бы, только скажи нам.
Ошибка по-питонски называется "exception" (формальное русской название "исключительная ситуация" или "исключение"). Поэтому предназначенная для этого в питоне конструкция называется try..except...
Выглядит так:
Мы должны указать питону тип ошибки. Это и есть первое слово (до двоеточия) в последней строке, в которой питон нам говорит об ошибке. Про ошибку 404 питон нам говорит словами urllib.error.HTTPError: ...
То есть мы можем сделать так:
Данные про города
Добавляем это в наш пример. Меняем:
на:
И получаем:
1 import re
2 from urllib.request import urlopen
3 from urllib.parse import quote
4 import urllib.error
5
6 def clean(html):
7 noscript = re.sub('<script>[^<>]*</script>', '', html)
8 nostyle = re.sub('<style>[^<>]*</style>', '', noscript)
9 notags = re.sub('<[^>]*>', '', nostyle)
10 text = re.sub('[&][^;]*;', ' ', notags)
11 text = re.sub(r'\s+', ' ', text)
12 return text
13
14 with open('words.txt', encoding='cp1251') as file:
15 cities = file.read().split()
16
17 for city in cities:
18 try:
19 with urlopen('http://ru.wikipedia.org/wiki/' + quote(city)) as file:
20 text = file.read().decode('utf-8')
21 except urllib.error.HTTPError:
22 print(city, "не найден")
23 text = clean(text)
24 with open('cities/' + city + '.txt', 'w', encoding='utf-8') as file:
25 file.write(text)
Запускаем...
>>> Абомей-Калави не найден Авали не найден Агадеc не найден Адда-Дуэни не найден ... Эль-Муруж не найден Эль-Хардж не найден Ягуа не найден гиви не найден >>>
Ура, работает!
glob
Попробуем теперь пройтись по скачанным результатам и посмотреть на начало каждой статьи, скажем, первые десять слов. Но не перекачивать же нам все статьи заново для этого!
Нам нужно научиться проходить по всем файлам в папке. В питоне это делается тривиальнейше с помощью модуля glob:
1 >>> import glob
2 >>> glob.glob('*.py')
3 ['buttons.p.py', 'control-18-10.py', 'example.py', 'examples-errors-mystem.py', 'fern.py', 'primes.py']
4 >>> glob.glob('*.txt')
5 ['cities.txt', 'clean.txt', 'control-18-10.txt', 'errors.txt', 'helps.txt', 'lemmas.txt', 'sentences.txt', 'smiles.txt', 'words.txt']
6 >>> glob.glob('cities/*.txt')
7 ['cities\\Аба.txt', 'cities\\Абадан.txt', 'cities\\Абакалики.txt', 'cities\\Абботсфорд.txt', 'cities\\Абенгуру.txt', 'cities\\Абеокута.txt', 'cities\\Абердин.txt', 'cities\\Абеше.txt', 'cities\\Абиджан.txt', 'cities\\Абовян.txt']
Функция glob берёт на вход шаблон для имён файлов и возвращает список имён файлов, подходящих под этот шаблон (он возвращает имена файлов ровно в таком виде, чтобы их можно было сразу, ничего не меняя, дать функции open).
Шаблоны у glob очень простые и ограниченные, гораздо проще, чем регулярные выражения. В них есть всего четыре спецсимвола:
* обозначает любое количество любых символов в имени файла
? обозначает один любой символ в имени файла
[..] обозначает один символ из множеста, так же, как и у регулярных выражений
** обозначает любое количество любых символов в имени пути; отличие * от ** в том, что *.py находит только файлы с расширением .py в текущей папке, а **.py находит все файлы с расширением .py в текущей папке и всех вложенных в неё папках, и всех папках, вложенных в них, и т.п.
Первые десять слов
Всё очень просто: получаем список файлов, проходим по нему, каждый файл открываем, читаем, и добываем из него нужное.
Или, если в разумных сократить всё, что легко сократить, получаем:
Если его у вас на диске не лежит, поищите в списке рассылки или сделайте своими руками из какой-нибудь страницы с названием "списки городов" в википедии. (1)