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

в 6:07, , рубрики: grab, grablab, python, парсинг сайтов, метки: , ,

Всем привет!

Я активный пользователь 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js