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 под ваши нужды.

Мы используем для парсинга сайтов

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

Никто ещё не голосовал. Воздержавшихся нет.

ссылка на оригинал статьи http://habrahabr.ru/post/173509/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *