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

в 22:11, , рубрики: data mining, dom, html, lxml, python, xpath, парсинг сайтов, метки: , , , , , ,

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

Ранее я уже писал на хабре о 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

Источник

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


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