Kodomo

Пользователь

Лог №10

Вспоминаем прошлое: как почистить HTML

Мы можем запихать весь наш код для чистки HTML, который мы писали в прошлый раз, в функцию. Это удобно:

   1 import re
   2 
   3 def clean(html):
   4     noscript = re.sub('<script>[^<>]*</script>', '', html)
   5     nostyle = re.sub('<style>[^<>]*</style>', '', noscript)
   6     notags = re.sub('<[^>]*>', '', html)
   7     text = re.sub('[&][^;]*;', ' ', notags)
   8     text = re.sub(r'\s+', ' ', text)
   9     return text

>>> 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, которая позволяет нам открыть страницу в сети в точности так, как мы открываем файл:

   1 import ullib.request
   2 
   3 file = urllib.request.urlopen('http://ya.ru')
   4 print(file.read())

Как и с файлами, лучше пользоваться with. И, конечно, каждый раз писать всё название этой функции со всеми регалиями не хочется, поэтому мы будем импортировать его по-другому:

   1 from urllib.request import urlopen
   2 
   3 with urlopen('http://yandex.ru') as file:
   4     print(file.read())

Одного этого вполне достаточно, чтобы уже уметь делать очень много.

Ещё раз про кодировки: 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 не умеет самостоятельно определять кодировку, в которой пришли данные и расшифровывать их. У нас есть два варианта:

Попробуем второй подход:

   1 >>> with urlopen('http://ya.ru') as file:
   2                 textbytes = file.read()
   3 
   4 >>> text = textbytes.decode('utf-8')
   5 >>> with open('xyz.html', 'w', encoding='utf-8') as file:
   6         file.write(text)
   7 8995

Ура, вышло! И в файле всё, что нужно есть!

Два слова про устройство 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 import re
   2 
   3 def clean(html):
   4     noscript = re.sub('<script>[^<>]*</script>', '', html)
   5     nostyle = re.sub('<style>[^<>]*</style>', '', noscript)
   6     notags = re.sub('<[^>]*>', '', nostyle)
   7     text = re.sub('[&][^;]*;', ' ', notags)
   8     text = re.sub(r'\s+', ' ', text)
   9     return text

Прибавляем чтение списка городов:

   1 with open('words.txt', encoding='cp1251') as file:
   2     cities = file.read().split()

Кстати, проверим сразу, что мы всё угадали, и с кодировками всё так. запустим программу и выполним:

   1 >>> print(len(cities), cities[:10])

Работает? Осталось приделать проход по списку городов, собирание адреса и запись в файл:

   1 for city in cities:
   2     with urlopen('http://ru.wikipedia.org/wiki/' + city) as file:
   3         text = file.read()
   4     clean(text)
   5     with open(city + '.txt', 'w', encoding='utf-8') as file:
   6         file.write(text)

Итого:

   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 >>> import urllib.parse
   2 >>> urllib.parse.quote('Москва')
   3 '%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0'

Поменяем

   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...

Выглядит так:

   1 try:
   2     кусок кода, который может сломаться
   3 except ошибка:
   4     что делать, если кусок кода сломался

Мы должны указать питону тип ошибки. Это и есть первое слово (до двоеточия) в последней строке, в которой питон нам говорит об ошибке. Про ошибку 404 питон нам говорит словами urllib.error.HTTPError: ...

То есть мы можем сделать так:

   1 from urllib.error import HTTPError
   2 from urllib.request import urlopen
   3 
   4 try:
   5   urlopen('http://ya.ru/xyzzy')
   6 except HTTPError:
   7   print("Нет страницы xyzzy, ну нет её")

Данные про города

Добавляем это в наш пример. Меняем:

   1     with urlopen('http://ru.wikipedia.org/wiki/' + quote(city)) as file:
   2         text = file.read().decode('utf-8')

на:

   1     try:
   2         with urlopen('http://ru.wikipedia.org/wiki/' + quote(city)) as file:
   3             text = file.read().decode('utf-8')
   4     except urllib.error.HTTPError:
   5         print(city, "не найден")

И получаем:

   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 очень простые и ограниченные, гораздо проще, чем регулярные выражения. В них есть всего четыре спецсимвола:

Первые десять слов

Всё очень просто: получаем список файлов, проходим по нему, каждый файл открываем, читаем, и добываем из него нужное.

   1 import glob
   2 
   3 filenames = glob.glob('cities/*.txt')
   4 for filename in filenames:
   5     with open(filename, encoding='utf-8') as file:
   6         text = file.read()
   7     words = text.split()
   8     first_words = words[:10]
   9     fragment = ' '.join(first_words)
  10     print(fragment)

Или, если в разумных сократить всё, что легко сократить, получаем:

   1 import glob
   2 
   3 for filename in glob.glob('cities/*'):
   4     with open(filename, encoding='utf-8') as file:
   5         words = file.read().split()
   6     print(' '.join(words[:10]))
  1. Если его у вас на диске не лежит, поищите в списке рассылки или сделайте своими руками из какой-нибудь страницы с названием "списки городов" в википедии. (1)