Парсинг ресурсов при помощи Python

Хочу отметить, что работа над этой статьей ещё не закончена. Если у вас есть замечания или дополнение, добро пожаловать в комментарии.

Важно

Всегда сначала посмотрите предлагает ли сайт собственный API, RSS/Atom фиды также пригодятся.

Требования

Мы будем использовать две дополнительные библиотеки для Python.

Запросы

Мы будем использовать библиотеку requests вместо urllib2, так как она во всех отношениях превосходит urllib2. Я мог бы долго это доказывать, но, как мне кажется, на странице этой библиотеки все сказано в одном абзаце:

Библиотека urllib2 предлагает нам большинство необходимых аспектов для работы с HTTP, но API оставляет желать лучшего. Она была создана в другое время и для другой сети интернет. Она требует невероятного объёма работ даже для простых задач.

LXML

LXML — это XML\HTML парсер, его мы будем использовать для извлечения необходимой нам информации из веб-страниц. Некоторые предпочитают BeautifulSoup, но я довольно близко знаком с написанием собственных xPath и стараюсь использовать Ixml для достижения максимальной скорости.

Краулинг и Парсинг

Краулинг (Crawling) — это самый обыкновенный парсер, только ссылку на следующую страницу вы получаете с той, на которой находитесь в данный момент (т.е. это поисковый робот, например, зашли на сайт и получили все ссылки, потом перешли по ним, потом снова получили остальные ссылки и перешли по ним и т.д). Краулинг используется для создания больших баз данных.

Примеры

  • Краулинг категорий на сайте ebay (кстати у них есть API)
  • Краулинг контактных данных из телефонных справочников

Сохранение просмотренных адресов

Просмотренные адреса стоит сохранять, чтобы не парсить их по несколько раз. Для обработки не более 50000 адресов, я бы советовал использовать set (множество, тип данных). Можно просто просмотреть, есть ли адрес уже в наборе перед добавлением его в очередь на парсинг.

Если вы будете парсить крупный сайт, то предыдущий способ не поможет. Набор ссылок попросту достигнет больших размеров и будет использовать слишком много памяти. Решение — Фильр Блума. Не будем углубляться в подробности, но этот фильтр просто позволяет нам хранить посещенные адреса, а на вопрос “Был ли я уже по этому адресу?” он с очень большой вероятностью даёт верный ответ, при этом использует очень малый объём памяти. pybloomfiltermmap — очень хорошая реализация Фильтра Блума на Python.

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

CSS селекторы

Если у вас есть опыт работы с javascript, то вам, наверное, будет удобнее делать выборки из DOM при помощи CSS селекторов, чем использовать xPath. Библиотека Ixml вам в этом поможет, она имеет метод css_to_xpath. Вот краткое руководство: http://lxml.de/cssselect.html

Так же я бы рекомендовал PyQuery, так как это тоже очень распространённое средство.

Извлекаем блок главного содержимого

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

Можно использовать readability-lxml для этой цели. Основная его задача — извлекать основное содержимое со страницы (html всё равно может содержаться в извлекаемом содержимом). Продолжая решение задачи по подсчету слов, вы можете удалить html при помощи clean_html из lxml.html.clean, потом разбить результат по пробелам и произвести подсчет.

Параллелизм

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

Для запуска одновременных запросов используйте библиотеку grequests. Достаточно всего пары строк для распараллеливания вашего кода.

rs = (grequests.get(u) for u in urls)
responses = grequests.map(rs) 

Robots.txt

Все возможные руководство говорят, что необходимо следовать правилам файла Robots.txt. Но мы попробуем отличиться. Дело в том, что эти правила обычно очень ограничивают и далеко не отражают настоящие ограничения сайта. Чаще всего, этот файл генерирует сам фреймворк по умолчанию или какой-нибудь плагин по поисковой оптимизации.

Обычно исключением является случай, если вы пишите обычный парсер, например как у Google или Bing. В таком случае я бы советовал обращать внимание на файл robots.txt, чтобы избежать лишних задержек парсера. Если владелец хоста не хочет чтобы вы индексировали какое то содержимое, то и не следует этого делать.

Как не быть замеченным?

Не всегда хочется, чтобы ваши действия были замечены. На самом деле этого довольно просто добиться. Для начала следует изменять значения user agent в случайном порядке. Ваш IP адрес не будет изменяться, но такое часто случается в сети. Большинство крупных организаций, например университеты, используют один IP адрес для организации доступа в интернет всей своей локальной сети.

Следующее средство — прокси-сервера. Существует довольно много недорогих прокси-серверов. Если вы запускаете свои запросы со ста разных адресов, постоянно изменяете user agent да и ещё всё это происходит из разных дата центров, то отследить вас будет довольно сложно. Вот неплохой сервер — My Private Proxy.

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

Наследуем класс Response

Я бы советовал всегда наследовать класс requests.model.Response, так как в нем заключены очень полезные методы.

Частые проблемы

Установка LXML

Довольно часто возникают проблемы при установке Ixml, особенно в линуксе. Просто вначале надо установить зависимости:

apt-get install libxml2-dev libxslt-dev python-dev lib32z1-dev

а уже после можно выполнять команду pip install lxml в обычном режиме.

Как парсить сайты с Ajax

Обычно Ajax упрощает парсинг. При помощи закладки Сеть в Google Chrome вы сможете просмореть ajax запросы, а также ответы на них, которые обычно приходят в JSON. Получается что страницу даже не надо парсить, вы можете просто отправить запрос напрямую по тому же адресу. Если так сделать не получается, то следует использовать сам браузер. Прочтите раздел “Сайт выдаёт другое содержимое парсеру” для дополнительной информации.

Сайт выдаёт другое содержимое парсеру

  1. Проверьте исходный код страницы и убедитесь в том, что искомые данные действительно там присутствуют. Вполне возможно, что содержимое было изменено при помощи javascript, а наш парсер не выполняет подобные действия. В таком случае есть только один выход — PhantomJS{:target=»_blank»}. Управлять им вы можете при помощи Python, просто используйте selenium{:target=»_blank»} или splinter.
  2. Часто сайты выдают разное содержимое разным браузерам. Сверьте параметры user agent вашего браузера и парсера.
  3. Иногда сайты различают ваше географическое положение и изменяют своё содержимое исходя из него. Если вы запускаете свой скрипт со стороннего сервера, то это может повлиять на содержимое ответа от сервера.

Пагинация

Иногда приходится парсить данные разбитые по страницам. К счастью, это тоже довольно легко обойти. Посмотрите внимательно на URL адреса, как правило, там присутствует параметр page или offset, отвечающий за номер страницы. Вот пример управления таким параметром:

base_url = "http://example.com/something/?page=%s"
for url in [base_url % i for i in xrange(10)]:
    r = requests.get(url)

Ошибка 403 / ограничения по частоте запросов

Решение простое, просто немного отстаньте от этого сайта. Будьте аккуратны с количеством параллельных запросов. Неужели вам действительно нужны эти данные в течение часа, может стоит немного подождать?

Если все таки это так необходимо, то стоит обратиться к услугам прокси-серверов. Просто создайте обычный прокси менеджер, который будет отслеживать, чтобы каждый прокси-сервер использовался не чаще чем через интервал X. Обратите внимание на разделы “Как не быть замеченным?” и “Параллелизм”

Примеры кода

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

Пример простого запроса

Я не собираюсь многого показывать в этом примере. Всё что вам необходимо, вы найдете в документации к соответствующему классу.

import requests
response = requests.get('http://devacademy.ru/')
# Ответ
print response.status_code # Код ответа  
print response.headers # Заголовки ответа
print response.content # Тело ответа
# Запрос
print response.request.headers # Заголовки отправленные с запросом

Запрос с прокси

import requests
proxy = {'http' : 'http://102.32.3.1:8080',
           'https': 'http://102.32.3.1:4444'}
response = requests.get('http://devacademy.ru/', proxies=proxy) 

Парсинг содержимого ответа

Довольно легко распарсить html код полученный при помощи lxml. Как только мы преобразовали данные в дерево, можно использовать xPath для извлечения данных.

import requests
from lxml import html
response = requests.get('http://devacademy.ru/')
# Преобразование тела документа в дерево элементов (DOM)
parsed_body = html.fromstring(response.text)
# Выполнение xpath в дереве элементов
print parsed_body.xpath('//title/text()') # Получить title страницы 
print parsed_body.xpath('//a/@href') # Получить аттрибут href для всех ссылок

Скачиваем все изображения со страницы

Следующий скрипт скачает все изображения и сохранит их в downloaded_images/. Только сначала не забудьте создать соответствующий каталог.

import requests
from lxml import html
import sys
import urlparse
response = requests.get('http://imgur.com/')
parsed_body = html.fromstring(response.text)
# Парсим ссылки с картинками
images = parsed_body.xpath('//img/@src')
if not images:
    sys.exit("Found No Images")
# Конвертирование всех относительных ссылок в абсолютные
images = [urlparse.urljoin(response.url, url) for url in images]
print 'Found %s images' % len(images)
# Скачиваем только первые 10
for url in images[0:10]
    r = requests.get(url)
    f = open('downloaded_images/%s' % url.split('/')[-1], 'w')
    f.write(r.content)
    f.close()

Парсер на один поток

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

import requests
from lxml import html
import urlparse
import collections
STARTING_URL = 'http://devacademy.ru/'
urls_queue = collections.deque()
urls_queue.append(STARTING_URL)
found_urls = set()
found_urls.add(STARTING_URL)
while len(urls_queue):
    url = urls_queue.popleft()
    response = requests.get(url)
    parsed_body = html.fromstring(response.content)
    # Печатает заголовок страницы
    print parsed_body.xpath('//title/text()')
    # Ищет все ссылки
    links = [urlparse.urljoin(response.url, url) for url in parsed_body.xpath('//a/@href')]
    for link in links:
        found_urls.add(link)
        # Добавить в очередь, только если ссылка HTTP/HTTPS
        if url not in found_urls and url.startswith('http'):
            urls_queue.append(link)