Парсинг сайтов
0,0
рейтинг
18 апреля 2012 в 07:30

Разработка → Что такое Grab:Spider?

Никак не могу дописать документацию по Grab:Spider — это часть библиотеки Grab — для написания асинхронных пауков. Подумал выкладывать куски документации на хабрахабр. Думаю, с некоторым фидбэком дело быстрей пойдёт. На данный момент в документации есть лишь введение, описывающие в общих чертах, что за это за зверь такой Grab:Spider. Его и выкладываю.

Модуль Spider это фреймворк позволяющий описать парсер сайта как набор функций обработчиков, где каждый обработчик отвечает за специфичный тип запроса. Например, при парсинге форума у вас будут обработчики для главной страницы, страницы подфорума, страницы топика, страницы профиля участника. Изначально такая структура парсера была разработана в силу ограничений асинхронного режима, но впоследствии оказалось, что писать парсеры в таком структурированном виде (один запрос — одна функция) очень удобно.

Модуль Spider работает асинхронно. Это значит что всегда есть только один рабочий поток программы. Для множественных запросов не создаются ни треды, ни процессы. Все созданные запросы обрабатываются библиотекой multicurl. Суть асинхронного подхода в том, что программа создаёт сетевые запросы и ждёт сигналы о готовности ответа на эти запроссы. Как только готов ответ, то вызывается функция-обработчик, которую мы привязали к конкретному запросу. Асинхронный подход позволяет обрабатывать большее количество одновременных соединений чем подход, связанный с созданием тредов или процессов т.к. память занята всего одним процессом и процессору не нужно постоянно переключаться между множество процессов.

Есть один нюанс, который будет очень непривычен тем, кто привык работать в синхронном стиле. Асинхронный подход позволяет вызывать функции обработчики при готовности сетевого ответа. Если алгоритм парсинга состоит из нескольких последовательных сетевых запросов, то нужно где-то хранить информацию о том, для чего мы создали сетевой запрос и что с ним делать. Spider позволяет достаточно удобно решать эту проблему.

Каждая функция-обработчки получает два входных аргумента. Первый аргумент — это объект Grab, в котором хранится информация о сетевом ответе. Вся прелесть Spider модуля в том, что он сохранил знакомый вам интерфейс для работы с синхронными запросами. Второй аргумент функции-обработчика это Task объект. Task объекты создаются в Spideer для того, чтобы добавить в очередь сетевых запросов новое задание. С помощью Task объекта можно сохранять промежуточные данные между множественными запросами.

Рассмотрим пример простого парсера. Допустим, мы хотим зайти на сайт habrahabr.ru, считать заголовки последних новостей, далее для каждого заголовка найти картинку с помощью images.yandex.ru и сохранить полученные данные в файл:

# coding: utf-8
import urllib
import csv
import logging

from grab.spider import Spider, Task

class ExampleSpider(Spider):
    # Список страниц, с которых Spider начнёт работу
    # для каждого адреса в этом списке будет сгенерировано
    # задание с именем initial
    initial_urls = ['http://habrahabr.ru/']

    def prepare(self):
        # Подготовим файл для записи результатов
        # Функция prepare вызываетя один раз перед началом
        # работы парсера
        self.result_file = csv.writer(open('result.txt', 'w'))
        # Этот счётчик будем использовать для нумерации
        # найденных картинок, чтобы создавать им простые имена файлов.
        self.result_counter = 0

    def task_initial(self, grab, task):
        print 'Habrahabr home page'

        # Это функция обработчик для заданий с именем initial
        # т.е. для тех заданий, чтобы были созданы для
        # адреов указанных в self.initial_urls

        # Как видите интерфейс работы с ответом такой же
        # как и в обычном Grab
        for elem in grab.xpath_list('//h1[@class="title"]/a[@class="post_title"]'):
            # Для каждой ссылки-заголовка создадим новое задание
            # с именем habrapost
            # Обратите внимание, что мы создаём задания с помощью
            # вызова yield - это сделано исключительно ради красоты
            # По-сути это равносильно следующему коду:
            # self.add_task(Task('habrapost', url=...))
            yield Task('habrapost', url=elem.get('href'))

    def task_habrapost(self, grab, task):
        print 'Habrahabr topic: %s' % task.url

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

        # Для начала сохраним адрес и заголовк топика в массив
        post = {
            'url': task.url,
            'title': grab.xpath_text('//h1/span[@class="post_title"]'),
        }

        # Теперь создадим запрос к поиску картинок яндекса, обратите внимание,
        # что мы передаём объекту Task информацию о хабрапосте. Таким образом
        # в функции обработки поиска картинок мы будем знать, для какого именно
        # хабрапоста мы получили результат поиска картинки. Дело в том, что все
        # нестандартные аргументы конструктора Task просто запоминаются в созданном
        # объекте и доступны в дальнейшем как его атррибуты
        query = urllib.quote_plus(post['title'].encode('utf-8'))
        search_url = 'http://images.yandex.ru/yandsearch?text=%s&rpt=image' % query
        yield Task('image_search', url=search_url, post=post)

    def task_image_search(self, grab, task):
        print 'Images search result for %s' % task.post['title']

        # В этой функции мы получили результат обработки поиска картинок, но
        # это ещё не сама картинка! Это только список найденных картинок,
        # Теперь возьмём адрес первой картинки и создадим задание для её
        # скачивания. Не забудем передать информацию о хабрапосте, для которого
        # мы ищем картинку, эта информация хранится в `task.post`.
        image_url = grab.xpath_text('//div[@class="b-image"]/a/img/@src')
        yield Task('image', url=image_url, post=task.post)

    def task_image(self, grab, task):
        print 'Image downloaded for %s' % task.post['title']

        # Это последнняя функция в нашем парсере.
        # Картинка получена, можно сохранить результат.
        path = 'images/%s.jpg' % self.result_counter
        grab.response.save(path)
        self.result_file.writerow([
            task.post['url'].encode('utf-8'),
            task.post['title'].encode('utf-8'),
            path
        ])
        # Не забудем увеличить счётчик ответов, чтобы
        # следующая картинка записалась в другой файл
        self.result_counter += 1


if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    # Запустим парсер в многопоточном режиме - два потока
    # Можно больше, только вас яндекс забанит
    # Он вас и с двумя то потоками забанит, если много будете его беспокоить
    bot = ExampleSpider(thread_number=2)
    bot.run()


В примере рассмотрены простейший парсеры и не затронуты очень много возможностей, которые умееет Spider. Читайте о них в подробной документации. Обратите внимание, что часть функций обработчиков отработают с ошибкой, например, потому что, яндекс ничего не найдёт по заданному запросу.

Далее я планирую описать различные способы создания Task-объектов, обработку сетевых ошибок и фунционал повтороного выполнения заданий, остановленных по ошибке.

В письменной форме вопросы о Grab лучше спрашивать в майл-листе: groups.google.com/group/python-grab

Заказать доработку Grab, а также парсер на базе Grab и Grab::Spider можно здесь: datalab.io
@itforge
карма
75,2
рейтинг 0,0
Парсинг сайтов
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (43)

  • 0
    Ну вот теперь вы меня заинтересовали :) Еще бы (py)curl из зависимостей убрать — было бы вообще шикарно :)
    • +2
      Хорошо, что вы комментарий написали, а то тут так пусто :)
      Curl я почти выпилил уже. Ну из Grab то его, допустим, можно лего вытащить и юзать urllib, сейчас транспорт почти готов. А вот из Spider вытащить то я уже вытащил, но альтернативой пока что может служить только пул тредов (или процессов) а это значит, что собо много потоков нельзя будет запустить — cpu убьётся или память. В общем, я эти фичи щас попиливаю потихоньку. Чувствую, где-то в течении месяца уже работающие вещи будут.
      • 0
        можно еще твистед по идее заместо multicurl юзать
      • 0
        Еще есть gevent, который можно использовать напрямую или пропатчить им urllib
      • 0
        Вобщем как только — так сразу на ваш фрэймворк со Scrapy перейду :)
        • 0
          а в чем проблема с pycurl? все равно придется что-то использовать или его или Twisted (как в scrapy)
          • 0
            Проблема в том что он требует curl, который на некоторых хостингах не поставить, ибо не в sudoers
  • 0
    Вопрос может не совсем в тему. Но колебаюсь сейчас взять Grab или искать(писать) аналоги.
    Так вот вопрос:
    Есть ли замеры по производительности/нагрузке? Особенно когда нужно грабить большой объём страниц и большой объём данных на каждой странице.
    Ну и так же каковы пожелания по аппаратной составляющей сервера, на котором всё это будет крутится (под большие объёмы, постоянного граббинга).
    • 0
      Неа, пока каких-то инструментов для сравнения нету. Библиотека пишется спонтанно. Естественно, если вы будете писать своё решение, то будучи заточенное под задачу он будет быстрее Grab работать.
      Пожелания по железу. Используя асинхронность вы скорее всего упрётесь раньше в CPU или жёсткий диск или в канал. Также учтите что для парсинга Grab использует только одно ядро.
      • 0
        Ограничение GIL'a?
        • 0
          Как это? У вас же программа асинхронно выполняется, откуда там GIL?
          • 0
            Тогда откуда ограничение на одно ядро?
            • 0
              Ну, оттуда. Вот запустили вы парсер, он работает на одном ядре :)
              • 0
                Один процесс — одно ядро. Чтобы работал на нескольких ядрах, одного желания мало. Надо ещё писать код, который будет синхронизироать работу нескольких процессов.
                • 0
                  а, ну вот я про это и говорю…
              • 0
                То есть очередей нет, и процессы не спаунятся?
                • 0
                  Очередь есть, но пока она как просто хранилище заданий в памяти работает. Если вынести очередь в какой-нить мемкэшед или редис, то уже по идее с некоторыми предосторожностями можно запустить несколько спайдеров. Ну, это один из будущих шагов рефакторинга. Выделения очереди как отдельного слоя архитектуры и различные бэкенды для очереди.
                  • 0
                    ну стандартно — Redis, Celery…
                  • 0
                    Вообще конечно хотелось бы на кластере научиться запускаться %-)
                  • 0
                    Учитывая, что точка входа для парсинга обычна одна (к примеру, главная страница) ну или же как минимум известно, что парсим (т.е. понимаем структуру и логические связи в данных на странице), то распалить задачу парсинга на Х нод становиться довольно просто. Я к тому, что какого-то особого слоя архитектуры тут не требуется. Поясню пожалуй на примере парсинга каталога товаров.

                    Есть начальная страница каталога на которой есть меню первого уровня. Есть знание того, каким образом на внутренних страницах можно выцепить ссылки на подразделы каталога что бы спускаться вниз по иерархии. Есть центральная нода которая непосредственно не занимается, есть куча нод которые и ходят по сайту. Центральная нода оправляет на любую из подчиненных задание скачать начальную страницу, выдергивает из её контента ссылки на подкатегории первого уровня и оправляет задание на рекурсивное скачивание подчиненным. Причем подраздел только на одну ноду, т.е. один подраздел не может скачиваеться двумя нодами. Если следовать этому правилу, то отпадает необходимость синхронизации. А поскольку мы знаем структуру входных данных, то уходя вниз рекурсивно по иерархии подчиненые ноды ни когда не уходят на «левые» ссылки и идут четко по дереву. Ситуация, когда две ноды скачали одну страницу вполне возможна если на сайте есть ошибки (две разных ссылки ведут по сути на одну страницу), но это не проблема, кроме того такие ошибки вполне отлавливаемые ибо мы знаем структуры данных которые мы парсим.

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

                    Наверное стоит отдельно упомянуть на чем это имплементировал я. А сделано это на PHP. Причем код мастер ноды и рабочих нод один и тот же. А кто в кластере будет мастером регулируется через конфиги. На каждой ноде есть файл конфига где описаны реквизиты для связи с остальными нодами и роль этой ноды в кластере. В качестве правил для парсера используются XPath инструкции. Схема удобная тем, что позволяет развернуть кластер на базе банального шаред хостинга.
                    • 0
                      > Центральная нода оправляет на любую из подчиненных задание скачать начальную страницу, выдергивает из её контента ссылки на подкатегории первого уровня и оправляет задание на рекурсивное скачивание подчиненным.

                      Так это и есть синхронизация, особый слой архитектуры. Т.е. центральная нод должна определить какая из но свободна, дать ей задание, потом когда нода пройдёт вниз по дереву, то она опять станет свободна и центральная нода должна узнать об этом. Плюс надо данные с нод забирать, объединять.

                      > Причем подраздел только на одну ноду, т.е. один подраздел не может скачиваеться двумя нодами.

                      Я разрабатываю универсальное решение — у сайта вообще не может быть структуры, самого сайта не может быть, может быть просто список ссылок например или на сайте может не быть разделов. Что угодно может быть.
                      • 0
                        Имхо, универсальные решения слишком много ресурсов требуют на разработку. И если их пилить, то на постоянке. Напилить группу парсеров каждый из которых решает свою задачу, как показывает мой опыт, проще, в том числе и на будущем сапорте кода. Что не исключает наличия некоего универсального парсера-объекта через API которого и идет взаимодействия наружу. Он просто инкапсулирует в себе эту группу. Единственное неудобство, нужно явно в конфиге указывать, какого типа контент тянем.
                        • +1
                          > Имхо, универсальные решения слишком много ресурсов требуют на разработку. И если их пилить, то на постоянке. Напилить группу парсеров каждый из которых решает свою задачу, как показывает мой опыт, проще, в том числе и на будущем сапорте кода.

                          У меня несколько более глобальная задача. Содать универсальное решение, создать community-вокруг него. Сделать экосистему для библиотеки и компании grablab, которая занимается парсингом сайтов.
                          • 0
                            Глобально. Это радует. Особенно на фоне того, что в среднем по больничке все как-то уныло. Удачи в этом начинании!
                            • 0
                              Спасибо. Есть и приятные примеры. Вон парни, которые пилят scrapy, какой-то стартап замутили связанный с запуском scrapy-пауков — инвестирования собирают.
    • +1
      Пару недель назад грабил несколько тысяч порнороликов с туб-сайта.
      Сервер с 1гигабитным каналом — 8гигабайт памяти, все ок (20 потоков), запускал на сервере с 4я гигабайтами — бывало вылетал (не хватало памяти).

      Объем страниц парсил «за раз» в районе одного миллиона (каталог софта) — никаких проблем (кроме того что они места занимают немало).

      Замеры, хочу попрбовать сравнить Scrapy vs. Grab, но это скорее всего на следующей неделе, а то мы уже задолбали тут наверное всех с этим grab'ом )
      • 0
        Вылетал, наверное, потому что ты там слишком много заданий в очередь добавлял.
      • 0
        Замеры, хочу попрбовать сравнить Scrapy vs. Grab, но это скорее всего на следующей неделе
        Действуйте. Интересно будет почитать.
  • 0
    Хорошая статья, хотя и не пользуюсь Grab, сохранил на будущее.
    Хотелось бы также видеть статьи по ещё одному парсеру — Scrapy, его документация, увы, не раскрывает всех подробностей его работы.
    • 0
      Scrapy != parser.
      Scrapy == crawler.

      И статья на Хабре есть.
      • 0
        И статья на Хабре есть.
        Всего одна! И я уже её неоднократно читал. Хочу moar статей!
        • 0
          Скрапи очень быстро меняется… Проще на практике изучать. Единственная асболютно неудобная вещь в нем — pipelines. Вообще непонятно зачем она существует, если в нее срут все пауки. Если бы был маппинг по Item() — было бы лучше…
          • 0
            Я в граб в начале одну сущность сделал, впрочем она и сейчас есть, но оказалось что она ненужная. Можно из функци-обработчика сделать не yield Task(name), а yield Data(name) (ещё один спецкласс) и тогда Spider вызовет функцию `def data_` в которой можно сохранить куда-надо данные. Возможно при разработке многоядерной-многомшинной архитектуры это подходет будет более важен, а пока можно прямо из любого обработчика ответа сохранять данные, что как-то проще, чем ещё дополнительные функции городить.
            • 0
              Проще делать как в djcelery, оборачивая каждый таск декоратором.
              • 0
                Что за декоратор такой?
                • 0
                  @task(delay)
                  def task_initial(self, grab, task):
                      return/yield result
                  
                  • 0
                    Не знаю, я вообще не люблю декораторы использовать, как-то уродливо выглядят. В данном случае декоратор будет бесполезен т.к. функция-обработчик может yieldить результаты разных типов для которых нужны разные обработчики.
                    • 0
                      Простите, перепутал ветку. Это к очередям относится.
                      А про разные типы данных — просто по неймингу классов ориентироваться, и указать в гайде: ASpider(Spider), AItem(Item), APipeline(Pipeline), etc…
          • 0
            Проще на практике изучать.
            С этого момента поподробнее, пожалуйста.
            • +1
              zalil.ru/33094662
              На правильность не претендую…
        • +1
          habrahabr.ru/users/seriyPS/ трясите с него, он вроде юзает активно scrapy :)
        • 0
          А, так вот чего…
          Ну не знаю, я лично в свое время открыл doc.scrapy.org/en/0.14/intro/tutorial.html и сделал по образу и подобию. Может у меня мозг как то по особому устроен, но никаких проблем не встретил, документация у Scrapy очень подробная.
          Единственное с чем постоянно приходится бороться так это с сохранением результатов в базу данных и постобработкой данных. Если делать это в Pipeline с применением ресурсоемких операций или блокирующего IO (сохранение в MySQL/Postgres с проверкой «а не сохранено ли уже» например), то будут тормоза и 100% процессора на нем не выжмешь. В последнем проекте прикрутил неблокирующий AMQP и отдельных воркеров — сохранятелей… Уперся в CPU, и это хорошо)
  • 0
    А как Spider'у выставить значение User-agent?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.