Парсинг сайтов
0,1
рейтинг
20 марта 2013 в 02:11

Разработка → Grab — новый интерфейс для работы с DOM-деревом HTML-документа

Исторический экскурс


Ранее я уже писал на хабре о Grab — фреймворке для написания парсеров сайтов: раз, два, три, четыре. В двух словах, Grab это удобная оболочка поверх двух библиотек: pycurl для работы с сетью и lxml для разбора HTML-документов.

Библиотека lxml позволяет совершать XPATH-запросы к DOM-дереву и получать результаты в виде ElementTree объектов, имеющих кучу полезных свойств. Несколько лет назад я разработал несколько простых методов, которые позволяли применять xpath-запросы к документу, загруженному через граб. Проиллюстрирую кодом:

>>> from grab import Grab
>>> g = Grab()
>>> g.go('http://habrahabr.ru/')
<grab.response.Response object at 0x7fe5f7189850>
>>> print g.xpath_text('//title')
Лучшие за сутки / Посты / Хабрахабр


По сути это аналогично следующему коду:

>>> from urllib import urlopen
>>> from lxml.html import fromstring
>>> data = urlopen('http://habrahabr.ru/').read()
>>> dom = fromstring(data)
>>> print dom.xpath('//title')[0].text_content()
Лучшие за сутки / Посты / Хабрахабр


Удобство метода xpath_text заключается в том, что он автоматически применяется к загруженному через Grab документу, не нужно строить дерево, это делается автоматически, также не нужно вручную выбирать первый элемент, метод xpath_text делает это автоматически, также этот метод автоматически извлекает текст из всех вложенных элементов. Далее я привожу все методы библиотеки Grab с их кратким описанием:

  • grab.xpath — вернуть первый элемент, удовлетворящий условию
  • grab.xpath_list — вернуть все элементы
  • grab.xpath_text — взять первый элемент, удовлетворяющий условию и извлечь из него текстовое содержимое, также позволяет задать default значение, возвращаемое, если элемент не найден
  • grab.xpath_number — взять результат grab.xpath_text и найти в нём число


Не обошлось и без конфузов. Метод grab.xpath — возвращает первый элемент выборки, в то время как метод xpath ElementTree объекта возвращает весь список. Народ неоднократно натыкался на эту граблю. Также хочу заметить, что был точно такой же набор методов для работы с css запросами т.е. grab.css, grab.css_list, grab.css_text и т.д., но я лично отказался от CSS-выражений в пользу XPATH т.к. XPATH более мощный инструмент и часто есть смысл использовать его и я не хотел видеть в коде мешанину из CSS и XPATH выражений.

У вышеописанных методов был ряд недостатков:

Во-первых, когда требовалось вынести код выборки элементов в отдельную функцию, то возникал соблазн передавать в неё весь Grab объект, чтобы вызывать от него эти функции. По другому никак: или передаем Grab объект или передаём голый DOM-объект, у которого нет полезных функций, типа xpath_text.

Во-вторых, результат работы функций grab.xpath и grab.xpath_list — это голые ElementTree элементы, у которых уже нету методов типа xpath_text.

В-третьих, хотя это скорее проблема расширений фреймворка, но, так или иначе, область имён объекта Grab засоряется множеством вышеописанных методов.

А, да, и четвёртое. Меня заколебали вопросом о том, как получить HTML код элементов, найденных с помощью методов grab.xpath и grab.xpath_list. Народ не хотел понимать, что grab это просто обёртка вокруг lxml и что нужно просто прочитать мануал на lxml.de

Новый интерфейс для работы с DOM-деревом призван устранить эти недостатки. Если вы пользуетесь фреймворком Scrapy, то нижеописанные вещи будут вам уже знакомы. Я хочу рассказать о селекторах.

Селекторы



Селекторы, что это? Это обёртки вокруг ElementTree элементов. Изначальное в обёртку заворачивается всё DOM дерево документа т.е. обёртка строится вокруг корневого html элемента. Далее мы можем с помощью метода select получить список элементов, удовлетворяющих XPATH выражению и каждый такой элемент будет опять завёрнут в Selector обёртку.

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

>>> from grab.selector import Selector
>>> from lxml.html import fromstring
>>> root = Selector(fromstring('<html><body><h1>Header</h1><ul><li>Item 1</li><li><li>item 2</li></ul><span id="color">green</span>'))


Теперь сделаем выборку методом select, получим список новых селекторов. Мы можем обращаться к нужному селектору по индексу, также есть метод one() для выбора первого селектора. Обратите внимание, чтобы получить доступ непосредственно к ElementTree элементу, нам нужно обратиться к атрибуту node у любого селектора.

>>> root.select('//ul')
<grab.selector.selector.SelectorList object at 0x7fe5f41922d0>
>>> root.select('//ul')[0]
<grab.selector.selector.Selector object at 0x7fe5f419bed0>
>>> root.select('//ul')[0].node
<Element ul at 0x7fe5f41a7a70>
>>> root.select('//ul').one()
<grab.selector.selector.Selector object at 0x7fe5f419bed0>


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

>>> root.select('//ul/li')[0].text()
'Item 1'
>>> root.select('//ul/li')[0].number()
1
>>> root.select('//ul/li/text()')[0].rex('(\w+)').text()
'Item'


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

>>> root.select('//ul/li').text()
'Item 1'
>>> root.select('//ul/li').number()
1
>>> root.select('//ul/li/text()').rex('em (\d+)').text()
'1'
>>> root.select('//ul/li/text()').rex('em (\d+)').number()
1


Что ещё? Метод html для получения HTML-кода селектора, метод exists для проверки существования селектора. Также вы можете вызывать метод селект у любого селектора.

>>> root.select('//span')[0].html()
u'<span id="color">green</span>'
>>> root.select('//span').exists()
True
>>> root.select('//god').exists()
False
>>> root.select('//ul')[0].select('./li[3]').text()
'item 2'


Как работать с селектором непосредственно из Grab объекта? C помощью аттрибута doc вы можете получить доступ к корневому селектору DOM-дерева и далее использовать метод select для нужной выборки:

>>> from grab import Grab
>>> g = Grab()
>>> g.go('http://habrahabr.ru/')
<grab.response.Response object at 0x2853410>
>>> print g.doc.select('//h1').text()
Сказ о том, как один нерадивый провинциал в MIT поступал из песочницы
>>> print g.doc.select('//div[contains(@class, "post")][2]')[0].select('.//div[@class="favs_count"]').number()
60
>>> print g.doc.select('//div[contains(@class, "post")][2]')[0].select('.//div[@class="favs_count"]')[0].html()
<div class="favs_count" title="Количество пользователей, добавивших пост в избранное">60</div>


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

В версии Grab, доступной через pypi селекторов пока нет. Если хотите поиграться с селекторами, ставьте Grab из репозитория: bitbucket.org/lorien/grab. Конкретно реализация селекторов находится тут

Я представляю компанию datalab.io — мы занимаемся парсингом сайтов, парсим с помощью Grab и не только. Если ваша компания использует Grab, вы можете обращаться к нам по поводу доработки Grab под ваши нужды.
@itforge
карма
70,2
рейтинг 0,1
Парсинг сайтов
Реклама помогает поддерживать и развивать наши сервисы

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

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

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

  • +3
    Ну так все же, а чем это лучше использования lxml+xpath? Ну кроме сохранения пары строк на получение дерева? Лучше б описал преимущества grab, а то я их перед собой не вижу :(

    P.S. в опросе только библиотеки для получения данных, а не парсинга ;) я бы проголосовал бы за requests+lxml
    • 0
      Библиотеки для разбора DOM не включал в опрос т.к. это по сути добавило бы два измерения ответов, кто-то например использует requests + lxml, а кто-то requests + beautifulsoup, а кто-то gevent + lxml. А те кто используют Grab или Scrapy скорее всего используют их функции-обёртки для работы с DOM.

      > Лучше б описал преимущества grab, а то я их перед собой не вижу :(

      Преимущества Grab перед Requests трудно мне перечислить т.к. я Requests практически не использовал никогда. Всё таки Grab и Requests это разные вещи. Grab это комплексный фреймворк, а Requests это конкретно сетевая библиотека. В Grab есть так называемые сетевые транспорты, одним из которых является pycurl. Этот вариант хорошо оттестирован. Другим вариантом может быть Requests. Такой транспорт давно написан, но никто не тестирует его, а мне самому этот вариант не нужен т.к.удовлетворяет pycurl меня.

      В голом виде Grab я практически не использую. Использую для всех задач Grab::Spider, вот эту штуку уже смысла нет сравнивать с Requests, только со Scrapy т.к. Spider это:
      * интструмент для структуризации логики парсера вашего сайта
      * удобный интерфейс для обработки асинхронных запросов
      * автоматическая обработка сбойнувших запросов
      * работа со списками прокси
      * кэширующий слой (жутко полезная штука для отладки и не только)
      * инструменты сбора статистических данных о парсинге: количество тех или иных событий, время работы различных слоёв парсера
      * интерфейс работы с DOM деревом документа (описаны выше в статье)
      * в данный момент я делаю рефакторинг Spider для выполнения обраотчиков на нескольких ядрах, в будущем хочу сделать кластерное решение.
  • 0
    А его как-нибудь можно скрестить с V8, чтоб получить этакий headless-браузер?
    • 0
      Можно. Разобраться как работает V8, разобраться как работает Grab и скрестить.
  • –1
    Использую BeautifulSoup.
    • 0
      Советую переходить на lxml+xpath, суп медленный. Хотя если объёмы парсинга небольшие, можно и его парсить.

      Плюсы xml:
      * более быстрый
      * возможно памяти меньше ест, врать не буду, не помню
      * позволяет использовать стандарт, который знают все вменяемые специалисты: xpath запросы
      * есть дополнительные плюшки см модуль lxml.html
      * суп раньше банально падал на некоторых типах страниц, я даже писал простенький регексп, который все javascritp выкусывал, перед тем как передать текст страницы в суп
    • 0
      он все еще падает на «индусско» писаных сайтах, где есть не закрытые тэги, опечатки и т.п.? В этом плане lxml рулит, т.е. уронить его парсер очень тяжело
    • 0
      Bsoup жутко медленный. Плохо работает с PyPy. habrahabr.ru/post/163979/

      Когда запускал бенчмарк на индекспейджах TOP1000 ALEXA, увидел странную особенность — на части страниц зависимость скорости парсинга от размера страницы имеет вид примерно Speed = Size * 3 а на некоторых Speed = Size * 10. Т.е. на графике получается не одна линия, а 2. Что говорит о том, что некоторые «типы» страниц он сильно недолюбливает. Возможно там происходит переключение режима из Strict в Compatible, не знаю, в код не заглядывал.
    • 0
      /удалено — промахнулся/
  • 0
    Почему никто не рассматривает PyQuery? Отлично справляется со своими задачами! По названию сами понимаете, на что он похож ;)
    • 0
      Признаю, дурацкий опрос вышел. Я Рассказывал в статье про доступ к DOM-дереву документа, а в опросе пытался выяснить какую либу народу юзает для сетевых операций. Кстати, в Grab есть «поддержка» pyquery, через аттрибут pyqeruy можете делать нужные вам выборки применительно к загруежнному документу.
      • 0
        Кстати, любой язык запросов можно реализровать в селекторах. Сейчас метод select понимает только xpath запросы, можно научить его и CSS запросам через cssselect или pyquery, например, с таким синтаксисом: select(css='....')
    • 0
      Я его тоже использую, однако бесит такой момент, что когда я делаю

      for a in pq.find('a.class'):
      pass

      в цикл передается уже HtmlElement, а не объект класса PyQuery.
      Приходится городить такую вот конструкцию:

      a_list = pq.find('a.class')
      for i in range(len(a_list)):
      a_list.eq(i).find…
      • 0
        Собственно, статья и рассказывает о механизме, который решает эту проблему ;-)
        Хотел с ходу набросать поддержку pyquery в селекторах но сразу не понял, как оно работает. Нужны ответы на вопросы:

        1) Есть ли возможность построить pyquery объект не по html по данной etree.Element ноде?
        2) Каким методом осуществляется поиск? find? Что он возвращает, etree.Element ноды?
        • 0
          1) По идее можно, просто передать в конструктор ноду
          2) Да, find, возвращает объект класса PyQuery, но итерации в цикле по нему происходят как по массиву HtmlElement (странное немного поведение)

          pq.find('a').find('b')
          • 0
            Прикрутил pyquery к селекторам:

            >>> from grab import Grab
            >>> g = Grab()
            >>> g.go('http://habrahabr.ru/post/173509/')
            <grab.response.Response object at 0x139d3d0>
            >>> for sel in g.doc.select(pyquery='.comment_item'):
            ...     print sel.select(pyquery='.username').text()
            ... 
            gigimon
            itforge
            ertaquo
            itforge
            elky
            itforge
            gigimon
            galkin
            itforge
            itforge
            Arceny
            itforge
            Arceny
            zxmd
            itforge
            
          • 0
            > pq.find('a').find('b')

            Здесь найдётся список b детей из первого a родителя или из всех a родителей?
  • 0
    а почему нет в голосовалке lxml?
  • 0
    Топик напомнил давнее обсуждение habrahabr.ru/post/134918/#comment_4482652
    • 0
      Да я чё-то думал, селекторы сложнее сделать, может и раньше бы запилил. Оказалось, что всё довольно просто.
  • –1
    А на PHP никто не парсит что ли? Если это так, то почему?
    • 0
      Парсят.
  • 0
    Спасибо за статью. Подскажите, как можно получить значнеие конкретного арибута в найденном селекторе?
    • 0
      element = grab.doc.select('//h1/a')
      print element.attr('href')
      
      • 0
        Уху, в доке нигде нету, залез на гитхаб и там нашел. Но все равно спасибо.

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