суббота, 21 декабря 2013 г.

Парсим сайт при помощи Python

Люблю на досуги полистать web-комиксы. Как-то мне пришла мысль, что было бы неплохо иметь способ скачать понравившиеся серии для просмотра оффлайн.

Ставим задачу.

Скрипт должен уметь парсить сайт, скачивать нужные изображения выдирая для них  названия из заголовков на странице, вести учет скачанного, для того чтобы загружать только обновления, и уметь работать из под прокси.



Что нам потребуется:

Нам потребуется установленное окружение python 2.7 (я в курсе что уже есть третья версия, но 2.7 мне более привычен). Для работы с интернетом я предпочитаю использовать библиотеку urllib2 она имеет множество полезных фитч и в частности умеет работать с прокси требующими авторизации (а иногда мне приходится коннектиться именно таким образом). Для учета скачанных комиксов нам идеально подойдет sqLite. Что касается парсинга есть три основных варианта:
  • сравнение по шаблону при помощи регулярных выражений;
  • разбор документа в DOM-структуру и ее анализ;
  • использование SAX-парсера;
Регулярные выражения хороши, но их тяжело поддерживать и они имеют ряд ограничений связанных с объемом буфера для поиска в обратном направлении. Обход дом структуры - хорошее решение, но требует больше машинного времени и памяти зато позволяет анализировать документ в целом. Я же выбрал последний вариант, который реализован в  виде библиотеки HTMLParser.

SAX-парсер - это простое API для просмотра содержимого документа, требующее фиксированное количество памяти. Оно работает по принципу нотификации. Для работы с ним требуется написать обработчик событий описывающий реакцию приложения на различные элементы документа.

Приступим к реализации:

HTMLParser имеет пару неприятных и не очевидных особенностей. Первое это то что как и любой другой SAX-парсер он не предоставляет ни какого API для работы с контекстом. То есть в обработчике события вы получите данные только о том чем является конкретно этот элемент документа. К примеру вам нужно разобрать следующий фрагмент кода:

Вам нужно определить что текст "Some content" содержится в теге с классом content который в свою очередь вложен в тег с классом conteiner. Для решения этой проблемы проще всего использовать стек в который мы будем складывать элементы по нотификации handle_starttag и вынимать по наступлению handle_endtag. Второй момент это то что при анализе тега мы получаем его аргументы не в виде словаря а в виде кортежа содержащего кортежи ключ-значение. Для решения этих проблем я расширил HTMLParser добавив требуемый функционал.

Таким образом приведенный пример может быть проанализирован следующим способом:


Как видите код получился довольно простым и лаконичным.

Однако чтобы парсить данные нам нужно их сначала получить. Отправить запрос посредством библиотеки urllib2 достаточно просто... Но есть нюанс. Мне часто приходится работать из под прокси, причем требующего авторизации. В решении этой задачи нам помогут классы ProxyHandler и HTTPPasswordMgrWithDefaultRealm.

Класс HTTPPasswordMgrWithDefaultRealm используется для хранения данных авторизации. При этом вы можете хранить данные о нескольких аккаунтах и использовать их параллельно, либо использовать аккаунт по умолчанию.

Класс ProxyHandler реализует логику работы с соединением установленным через прокси сервер. Настройки данного соединения могут быть произведены тремя способами:
  • Явное указание настроек при создании экземпляра класса.
  • Если настройки не переданы явно - класс попытается прочитать их из переменных окружения среды заданных в следующем формате:
    <protocol_name>_proxy=<url_of_proxy_for_current_protocol_with_port_number>
    к примеру http_proxy=my-http-proxy:8080
  • Если переменные среды не были найдены ProxyHandler попытается получить доступ к системным настройкам прокси сервера. Для windows-систем они задаются в секции Internet Settings, для Mac OS посредством OS X System Configuration Framework.
Я не хотел лезть в системные настройки, поэтому решил что конфигурацию прокси сервера я жестко задам в тексте скрипта, однако у меня не было желания оставлять там свой личный пароль для подключения. И компрометировать его каждый раз вводя с клавиатуры в консоль в явном виде тоже было не допустимо. Решение нашлось быстро в виде библиотеки getpass. Она позволяет ввести пароль в консоли при этом не отображая введенных символов.

Используя все выше перечисленное я написал следующий код который использовал в своем скрипте:


Что у нас получилось:

Приводить в статье полный разбор кода скрипта я не вижу смысла. Он опубликован в моем публичном репозитории на GitHub и любой желающий может с ним ознакомится:

downloader2.py

Планы на будущее:

В планах добавить функционал по отправке нотификаций об обновлениях в коллекции на e-mail и повесить скрипт на крон на домашнем сервере.

Комментариев нет:

Отправить комментарий