Full-stack developer
0,0
рейтинг
17 апреля 2012 в 10:07

Разработка → Использование Grab:Spider для парсинга сайтов

Всем привет!

Я активный пользователь open-source фрэймворка Grab (itforge уже писал о нем здесь и здесь) и 1/2 проекта GrabLab (который занимается собственно коммерческой эксплуатацией фрэймворка). Т.к. парсим сайты мы часто, помногу и задания как правило совершенно не похожи друг на друга, хотелось бы поделится своим опытом в вопросе построения типичного парсинг проекта.

Немного про инструментарий который помогает мне в работе

В качестве рабочего браузера я использую FireFox с плагинами HttpFox (анализировать входящий/исходящий http трафик), XPather (позволяет проверять xpath выражения), SQLite Manager (просмотр sqlite таблиц), код набираю в emacs, где активно использую сниппеты (YASnippets) для часто встречающихся конструкций.

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


Для работы с sql базами куда, как правило (реже в json/xml), нужно разложить данные мы используем ORM — SQLAlchemy.

Собственно сам фрэймворк Grab предполагает большую гибкость в построении проекта и контроль за своими действиями. Однако, последние несколько проектов хорошо ложились в следующую структуру, отлично знакомую тем кто занимается веб-разработкой:

1) models.py — описываю модели данных
2) config.py — аналог settings.py из мира джанги, настройки, инициализация orm.
3) /spiders/*.py — код пауков
4) spider.py или project_name.py — главный файл проекта, по совместительству обычно реализует command-line интерфейс для запуска различных пауков, т.к. зачастую сайт парсится по частям.

В качестве примера не сильно оторванного от реальной жизни напишем парсер «Trending projects» и «Most popular Python projects» c open-source цитадели GitHub.

Полный код примера можно посмотреть тут.

Сперва нужно описать модель.

class Item(Base):
    __tablename__ = 'item'

    sqlite_autoincrement = True
    id = Column(Integer, primary_key=True)

    title = Column(String(160))
    author = Column(String(160))
    description = Column(String(255))
    url = Column(String(160))

    last_update = Column(DateTime, default=datetime.datetime.now)


Далее, в файле config.py выполняется начальная инициализация orm, создание таблиц, константы и находится функция которая конструирует параметры запуска паука в зависимости от настроек (default_spider_params), которая обычо общая для всех пауков в проекте.

def init_engine():
    db_engine = create_engine(
        'sqlite+pysqlite:///data.sqlite', encoding='utf-8')
    Base.metadata.create_all(db_engine)
    return db_engine

    
db_engine = init_engine()
Session = sessionmaker(bind=db_engine)


def default_spider_params():
    params = {
        'thread_number': MAX_THREADS,
        'network_try_limit': 20,
        'task_try_limit': 20,
    }
    if USE_CACHE:
        params.update({
            'thread_number': 3,
            'use_cache': True,
            'cache_db': CACHE_DB,
            'debug_error' :True,
        })
        
    return params


В большинстве случаев нет необходимости использовать mongodb на сервере, поэтому удобно сделать кэш отключаемым. При деплое проекта я просто ставлю USE_CACHE = False и все отлично работает. SAVE_TO_DB используется чтобы резрешить/запретить запись данных в базу данных.

Собственно переходим к самому интересному у нас будет 2а паука, первый будет парсить 5 репозиториев «Top Trending» проектов, а второй «Most watched Python».

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

Не будем пренебрегать ООП и напишем BaseHubSpider в котором определим 2а метода save() и log_progress().

class BaseHubSpider(Spider):
    initial_urls = ['http://github.com']

    items_total = 0

    def save(self, data):
        if not SAVE_TO_DB:
            return
            
        session = Session()

        if not session.query(Item).filter_by(title=data['title']).first():
            obj = Item(**data)
            session.add(obj)
        session.commit()

    def log_progress(self, str):
        self.items_total += 1
        print "(%d) Item scraped: %s" % (self.items_total, str)


В реальном приложении весьма вероятно наличие функции разбора страницы в зависимости от каких-то параметров — названий полей которые на каждой странице разные в то время как xpath путь к ним практически одинаковый и т.д.

Например как-нибудь так (это не рабочий пример, а просто иллюстрация для лучшего понимания):

    XPATH = u'//table[@class="standart-table table"]' + \
            u'//tr[th[text() = "%s"]]/td'

    values = (
        ('title', u'Наименование товара'),
        ('rating', u'Рейтинг'),
        ('categories', u'Категория товара'),
        ('description', u'Описание'),        
    )
    
    for db_field, field_title in values:
        try:
            data[db_field] = get_node_text(grab.xpath(
                XPATH % field_title, None)).strip()
        except AttributeError:
            data[db_field] = ''


https://github.com/istinspring/grab-default-project-example/blob/master/spiders/lang_python.py

Код паука который парсит и сохраняет в базу данных 20 самых популярных python проектов.

Обратите внимание

        repos = grab.xpath_list(
            '//table[@class="repo"]//tr/td[@class="title"]/..')
        for repo in repos:
            data = {
                'author': repo.xpath('./td[@class="owner"]/a/text()')[0],
                'title': repo.xpath('./td[@class="title"]/a/text()')[0],}

repos = grab.xpath_list('') — возвращает список lxml объект, в то время как например grab.xpath('') возвращает первый элемент, т.к. xpath в данном случае метод объекта grab, т.е. оперируя в цикле repo.xpath('./h3/a[1]/text()') — мы получаем список или исключение если lxml не смог найти xpath. Проще говоря, xpath от объекта grab и xpath от lxml объекта — разные вещи, в первом случае вернется первый элемент (или default или бросит exception), а во втором вернется список элементов ['something'].

^^ Читается непонятно, но как только вы встретите подобное на практике, сразу вспомните про этот абзац.

Надеюсь, информация была полезной. Товарищ itforge работает не покладая рук над развитием опен сурс продукта Grab, документация по grab нему доступна на русском языке, а вот для grab:spider, к сожалению, доступна только вводная часть.

Для вопросов по фрэймворку у нас есть jabber конференция на grablab@conference.jabber.ru
istinspring @istinspring
карма
9,0
рейтинг 0,0
Full-stack developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

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

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

  • +2
    Спасибо, интересно.

    Так же интересно было бы сравнение с уже более опытными open-source альтернативами, вроде Scrapy, а так же мысли по поводу защиты сайта от подобного софта.
    • +1
      Для того, чтобы сравнить, нужно для начала определиться по каким критериям сравнивать. Этакую матрицу критериев выработать. Я уже сам не знаю, что получится через год из Spider. Хочется верить, что сделаю поддержку работы Spider на кластере :) И ещё хочется допилить selenium. А ещё я там недавно вынес код работы с сетью в отдельный слой (в Spider, в Grab это давно уже) и теперь можно реально делать работу Spider на других сетевых библиотеках: twisted, gevent, pool тредов или процессов.
    • 0
      Я использовал Scrapy, потом как-то делал проект, большой (около миллиона страниц), там возникли трудности с которыми я не смог справиться, переписал на Grab:spider и все заработало и быстро сделалось. На мой взгляд Grab работает намного быстрее и проще для понимания.

      В скрапи например была проблема в 0.13 версии, при парсинге если у тебя Twisted 11.01 (не помню точно) он вылетал через некоторое время с ошибкой, и советовали даунгрейдить версию на 11.00. Более того у них в группах постоянно всплывают темы, как увеличить скорость и как добавить прокси. Подозреваю что нормальной поддержки стэка проксей они не делают из-за своего стартапа который будет предоставлять всю эту инфраструктуру на облаке.
      • 0
        Scrapy сам по себе крайне шустрый, мне удавалось полностью утилизировать процессор на нем (т.е. упирался не в IO и не в память, а именно уже в процессор!). Но его надо уметь готовить. Основная причина тормозов — синхронные вызовы. Если у вас присутствуют блокирующие операции в процессе работы (обращения к базе данных например), то тормоза гарантированы. Я для себя эту проблему решил прикручиванием асинхронного AMQP пайплайна и обработкой/сохранением результатов отдельными воркерами.
        Для прокси есть нормальная работающая MiddleWare. чем не устраивает? Что такое стек прокси? Если подразумеваются прокси-листы, то это пишется за 10 минут…
        • 0
          хотелось бы взглянуть на MiddleWare для прокси.

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

          USER_AGENTS_LIST = open(os.path.join(ROOT, 'user_agents.txt'), 'r').readlines()
          PROXY_LIST = open(os.path.join(ROOT, 'proxy_list.txt'), 'r').readlines()
          
          PROXY_USR_PWD_ENCODED = base64.b64encode('user:pass').strip()
          
          
          class RandomUserAgentMiddleware(object):
              def process_request(self, request, spider):
                  request.headers['User-Agent'] = random.choice(USER_AGENTS_LIST)
          
          
          class RandomProxyMiddleware(object):
              def process_request(self, request, spider):
                  request.meta['proxy'] = "http://%s" % random.choice(PROXY_LIST)
                  request.headers['Proxy-Authorization'] = 'Basic ' + \
                                                           PROXY_USR_PWD_ENCODED
          
          
          class RemoveRefererMiddleware(object):
              def process_request(self, request, spider):
                  if 'Referer' in request.headers:
                      del request.headers['Referer'] # or = 'http://google.com'
          


          На Spider:grab все это делается намного проще. Можно ничего не передавать (тогда создасться новый объект grab), а можно в следующий таск передать клон объекта grab со всеми куками и хидерами.
        • 0
          Мы зачастую упираемся в возможности сервера на котором расположена цель. :)
          Я думаю соберусь как-нибудь протестирую граб и скрапи на эталонном сайте.
    • 0
      Лучший способ защиты — кривая верстка и невалидный HTML))) На самом деле — ограничение на число запросов. Но это не гарантирует защиту, а просто сильно усложняет работу (нужно увеличивать таймауты или прокси использовать).
      Если нужно что-то посерьезнее — то куки, устанавливаемые обфусцированным JavaScript на клиенте. JS интерпретатор в паука встраивать занятие непростое (но тут и поисковики не смогут на сайт попасть).
  • 0
    Для того, чтобы сравнить, нужно для начала определиться по каким критериям сравнивать.

    В Ваших интересах, как минимум, по тем критериям, по которым Grab выигрывает. Это может быть всё, что угодно с точки зрения пользователя: удобство API, документация, поддержка, фичи, активность разработки итд. Если бы я был потенциальным пользователем и был бы на стадии выбора фреймворка для своего проекта, то рассматривал бы разные альтернативы. Google первым результатом выдаёт scrapy, почему бы мне его не использовать?
    • 0
      Первым и последним. Больше не с чем сравнивать :) Если кто-то накидает ещё ссылок на похожие проекты — буду благодарен. И правильно говорить о сравнении Grab:Spider и Scrapy. Ибо Grab это нечто другое — это API для синхронных сетевых запросов и обработки полученных ответов. Grab скорее надо сравнивать с urllib2, urllib3, requests, pycurl, mechanize.
      • 0
        И правильно говорить о сравнении Grab:Spider и Scrapy.

        Не правильно?
        Ибо Grab это нечто другое — это API для синхронных сетевых запросов и обработки полученных ответов.
        Вот главная страница grablib.org/ говорит, что Grab — фреймворк для парсинга сайтов. Это тоже, можно сказать, обработка полученных ответов.
        В scrapy:
        Scrapy is a fast high-level screen scraping and web crawling framework, used to crawl websites and extract structured data from their pages.
        На первый взгляд — то же самое.
        • 0
          Ну хорошо, сравнивайте Grab и Spider, если так хочется :D
  • 0
    Ой, ладно скромничать, ты не «активный пользователь Grab», ты один из разработчиков =)
    • 0
      ага, 1 коммит сделал )
      • 0
        istinspring является главным тестером Grab, тестирование — тоже процесс разработки :)
  • 0
    Все молодцы :) Вот сейчас как раз очередной паук разрабатывается… :)
  • 0
    Отличная статья! Большое спасибо.

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