GrabDuck

Фреймворк для парсинга Grab:Spider

:

Я автор python библиотеки Grab, которая упрощает написание парсеров веб-сайтов. Я о ней писал вводную статью некоторое время назад на хабре. Недавно я решил вплотную занять парсингом, стал искать free-lance заказы по парсингу и мне понадобился инструмент для парсинга сайтов с большим количеством страниц.

Раньше я реализовывал мультипоточные парсеры с помощью python-тредов с помощью такой вот библиотечки. У threading-подхода есть плюсы и минусы. Плюс в том, что мы запускаем отдельный поток(thread) и делаем в нём, что хотим: можем делать последовательно несколько сетевых вызовов и всё это в пределах одного контекста — никуда не надо переключаться, что-то запоминать и вспоминать. Минус в том, что треды тормозят и жрут память.

Какие альтернативы?

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

Так вот, я написал интерфейс к multicurl — это часть библиотеки pycurl, которая позволяет работать с сетью асинхронно. Я выбрал multicurl, потому что Grab использует pycurl и я подумал, что мне удастся использовать его и для работы с multicurl. Так оно и вышло. Я был даже несколько удивлён, что в первый же день экспериментов оно заработало :) Архитектура парсеров на базе Grab:Spider весьма похожа на парсеры на базе фреймворка scrapy, что, в общем, не удивительно и логично.

Приведу пример простейшего паука:

# coding: utf-8
from grab.spider import Spider, Task

class SimpleSpider(Spider):
    initial_urls = ['http://ya.ru']

    def task_initial(self, grab, task):
        grab.set_input('text', u'ночь')
        grab.submit(make_request=False)
        yield Task('search', grab=grab)

    def task_search(self, grab, task):
        for elem in grab.xpath_list('//h2/a'):
            print elem.text_content()


if __name__ == '__main__':
    bot = SimpleSpider()
    bot.run()
    print bot.render_stats()

Что тут происходит? Для каждого URL в `self.initial_urls` создаётся задание с именем initial, после того как multicurl скачивает документ, вызывается обработчик с именем `task_initial`. Самое главное, это то, что внутри обработчика мы получаем Grab-объект связанный с запрошенным документом: мы можем использовать практические любые функции из Grab API. В данном примере, мы используем его работу с формами. Обратите внимание, нам нужно указать параметр `make_request=False`, чтобы форма не отсылалась тут же, ибо мы хотим, чтобы этот сетевой запрос был обработан асинхронно.

В кратце, работа с Grab:Spider сводится к генерации запросов с помощью Task объектов и дальнейшей их обработке в специальных методах. У каждого задания есть имя, именно по нему потом выбирается метод для обработки запрошенного сетевого документа.

Создать Task объект можно двумя способами. Простой способ:

Task('foo', url='http://google.com')

После того как документ будет полностью скачан из сети, будет вызван метод с именем `task_foo`

Более сложный способ:

g = Grab()
g.setup(....настраиваем запрос как угодно...)
Task('foo', grab=g)

Этим способом мы можем настроить параметры запроса в соответствии с нашими нуждами, выставить куки, специальные заголовки, сгенерировать POST-запрос, что угодно.

В каких местах можно создавать запросы? В любом методе-обработчике можно сделать yield Task объекта и он будет добавлен в асинхроннную очередь для скачивания. Также можно вернуть Task объект через return. Кроме того есть ещё два пути генерации Task объектов.

1) Можно указать в аттрибуте `self.initial_urls` список адресов и для них будут созданы задания с именем 'initial'.

2) Можно определить метод `task_generator` и yield'ить в нём сколько угодно запросов. Причём новые запросы из него будут браться по мере выполнения старых. Это позволяет например без проблем проитерировать по миллиону строк из файла файла и не засирать, ой простите, засорять, ими всю память.

Первоначально я планировал сделать обработку извлечённых данных как в scrapy. Там это сделано с помощю Pipeline объектов. Например, вы получили страницу с фильмом, пропарсили её и вернули Pipeline объект с типом Movie. А ещё предварительно вы написали в конфиге, что Movie Pipeline должен сохраняться в базу данных или в CSV-файл. Как-то так. На практике оказалось, что проще не заморачиваться с дополнительной обёрткой и писать данные в БД или в файл сразу в методе обработчике запроса. Конечно, это не будет работать в случае распараллеливания методов по облаку машин, но до этого момента ещё надо дожить, а пока удобнее делать всё непосредственно в методе обработчике.

Task объекту можно передавать дополнительные аргументы. Например, мы делаем запрос в google поиск. Формируем нужный url и создаём Task объект: Task('search', url='...', query=query) Далее в методе `task_search` мы сможем узнать какой именно запрос мы искали, обратившись к аттрибуту `task.query`

Grab:spider автоматически пытается исправить сетевые ошибки. В случае network timeout он выполняет задание ещё раз. Количество попыток вы можете настраивать с помощью `network_try_limit` опции при создании Spider объекта.

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

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

Резюме. Если вы используете Grab, поглядите spider модуль, возможно, вам понравится. Если вы не знаете, что такое Grab, возможно вам лучше поглядеть фреймворк scrapy он документирован в сто крат краше нежели Grab.

P.S. Использую mongodb, чтобы хранить результаты парсинга — она просто офигенна :) Только не забудьте поставить 64bit систему, иначе больше двух гигабайт базу не сможете создать.

P.S. Пример реального парсера для парсинга сайта dumpz.org/119395

P.S. Официальный сайт проекта grablib.org (там ссылки на репозиторий, гугл-группу и документацию)

P.S. Пишу на заказ парсеры на базе Grab, подробности тут datalab.io