Селекторы

Когда вы просматриваете веб-страницы, наиболее частая задача, которую вам нужно выполнить, — это извлечь данные из источника HTML. Для этого доступно несколько библиотек, например:

  • BeautifulSoup — очень популярная библиотека для парсинга веб-страниц среди программистов Python, которая создаёт объект Python на основе структуры кода HTML, а также достаточно хорошо справляется с плохой разметкой, но у нее есть один недостаток: она медленная.

  • lxml — это библиотека синтаксического анализа XML (которая также анализирует HTML) с API Python на основе ElementTree. (lxml не входит в стандартную библиотеку Python.)

Scrapy имеет собственный механизм извлечения данных. Они называются селекторами, потому что они «выбирают» определённые части документа HTML, заданные выражениями XPath или CSS.

XPath — это язык для выбора узлов в XML-документах, который также можно использовать с HTML. CSS — это язык для применения стилей к HTML-документам. Он определяет селекторы для связывания данных стилей с определенными элементами HTML.

Примечание

Scrapy Selectors — это тонкая оболочка библиотеки parsel; цель этой оболочки — обеспечить лучшую интеграцию с объектами ответа Scrapy.

parsel — это автономная библиотека для парсинга веб-страниц, которую можно использовать без Scrapy. Он использует внутреннюю библиотеку lxml и реализует простой API поверх lxml API. Это означает, что селекторы Scrapy по скорости и точности анализа очень похожи на lxml.

С помощью селекторов

Построение селекторов

Объекты ответа предоставляют экземпляр Selector по атрибуту .selector:

>>> response.selector.xpath('//span/text()').get()
'good'

Запросы ответов с использованием XPath и CSS настолько распространены, что ответы включают ещё два ярлыка: response.xpath() и response.css():

>>> response.xpath('//span/text()').get()
'good'
>>> response.css('span::text').get()
'good'

Селекторы Scrapy — это экземпляры класса Selector, созданные путём передачи объекта TextResponse или разметки в виде строки (в аргументе text).

Обычно нет необходимости создавать селекторы Scrapy вручную: объект response доступен в обратных вызовах Spider, поэтому в большинстве случаев удобнее использовать ярлыки response.css() и response.xpath(). Используя response.selector или один из данных ярлыков, вы также можете гарантировать, что тело ответа будет проанализировано только один раз.

Но при необходимости можно использовать Selector напрямую. Конструирование из текста:

>>> from scrapy.selector import Selector
>>> body = '<html><body><span>good</span></body></html>'
>>> Selector(text=body).xpath('//span/text()').get()
'good'

Построение из ответа — HtmlResponse является одним из подклассов TextResponse:

>>> from scrapy.selector import Selector
>>> from scrapy.http import HtmlResponse
>>> response = HtmlResponse(url='http://example.com', body=body)
>>> Selector(response=response).xpath('//span/text()').get()
'good'

Selector автоматически выбирает лучшие правила синтаксического анализа (XML или HTML) в зависимости от типа ввода.

С помощью селекторов

Чтобы объяснить, как использовать селекторы, мы воспользуемся Scrapy shell (который обеспечивает интерактивное тестирование) и примером страницы, расположенной на сервере документации Scrapy:

Для полноты, вот его полный HTML-код:

<html>
 <head>
  <base href='http://example.com/' />
  <title>Example website</title>
 </head>
 <body>
  <div id='images'>
   <a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg' /></a>
   <a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg' /></a>
   <a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg' /></a>
   <a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg' /></a>
   <a href='image5.html'>Name: My image 5 <br /><img src='image5_thumb.jpg' /></a>
  </div>
 </body>
</html>

Сначала откроем оболочку:

scrapy shell https://docs.scrapy.org/en/latest/_static/selectors-sample1.html

Затем, после загрузки оболочки, у вас будет доступный ответ как переменная оболочки response и связанный с ней селектор в атрибуте response.selector.

Поскольку мы имеем дело с HTML, селектор автоматически будет использовать синтаксический анализатор HTML.

Итак, посмотрев на HTML код этой страницы, давайте создадим XPath для выбора текста внутри тега заголовка:

>>> response.xpath('//title/text()')
[<Selector xpath='//title/text()' data='Example website'>]

Чтобы фактически извлечь текстовые данные, вы должны вызвать методы селектора .get() или .getall(), как показано ниже:

>>> response.xpath('//title/text()').getall()
['Example website']
>>> response.xpath('//title/text()').get()
'Example website'

.get() всегда возвращает единственный результат; если есть несколько совпадений, возвращается содержимое первого совпадения; если совпадений нет, возвращается None. .getall() возвращает список со всеми результатами.

Обратите внимание, что селекторы CSS могут выбирать текстовые или атрибутные узлы с помощью псевдоэлементов CSS3:

>>> response.css('title::text').get()
'Example website'

Как видите, методы .xpath() и .css() возвращают экземпляр SelectorList, который представляет собой список новых селекторов. Данный API можно использовать для быстрого выбора вложенных данных:

>>> response.css('img').xpath('@src').getall()
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']

Если вы хотите извлечь только первый совпавший элемент, вы можете вызвать селектор .get() (или его псевдоним .extract_first(), обычно используемый в предыдущих версиях Scrapy):

>>> response.xpath('//div[@id="images"]/a/text()').get()
'Name: My image 1 '

Он возвращает None, если элемент не был найден:

>>> response.xpath('//div[@id="not-exists"]/text()').get() is None
True

В качестве аргумента можно указать возвращаемое значение по умолчанию, которое будет использоваться вместо None:

>>> response.xpath('//div[@id="not-exists"]/text()').get(default='not-found')
'not-found'

Вместо использования, например, '@src' XPath можно запрашивать атрибуты, используя свойство .attrib из Selector:

>>> [img.attrib['src'] for img in response.css('img')]
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']

В качестве ярлыка .attrib также доступен напрямую в SelectorList; он возвращает атрибуты для первого совпадающего элемента:

>>> response.css('img').attrib['src']
'image1_thumb.jpg'

Это наиболее полезно, когда ожидается только один результат, например при выборе по идентификатору или выборе уникальных элементов на веб-странице:

>>> response.css('base').attrib['href']
'http://example.com/'

Теперь мы собираемся получить базовый URL и несколько ссылок на изображения:

>>> response.xpath('//base/@href').get()
'http://example.com/'
>>> response.css('base::attr(href)').get()
'http://example.com/'
>>> response.css('base').attrib['href']
'http://example.com/'
>>> response.xpath('//a[contains(@href, "image")]/@href').getall()
['image1.html',
 'image2.html',
 'image3.html',
 'image4.html',
 'image5.html']
>>> response.css('a[href*=image]::attr(href)').getall()
['image1.html',
 'image2.html',
 'image3.html',
 'image4.html',
 'image5.html']
>>> response.xpath('//a[contains(@href, "image")]/img/@src').getall()
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']
>>> response.css('a[href*=image] img::attr(src)').getall()
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']

Расширения селекторов CSS

Согласно стандартам W3C, CSS селекторы не поддерживает выбор текстовых узлов или значений атрибутов. Но их выбор настолько важен в контексте парсинга веб-страниц, что Scrapy (parsel) реализует пару нестандартных псевдоэлементов:

  • для выбора текстовых узлов используйте ::text

  • для выбора значений атрибутов используйте ::attr(name), где name — это имя атрибута, значение которого вы хотите получить

Предупреждение

Данные псевдоэлементы специфичны для Scrapy/Parsel. Скорее всего, они не будут работать с другими библиотеками, такими как lxml или PyQuery.

Примеры:

  • title::text выбирает дочерние текстовые узлы дочернего элемента <title>:

>>> response.css('title::text').get()
'Example website'
  • *::text выбирает все текстовые узлы-потомки текущего контекста селектора:

>>> response.css('#images *::text').getall()
['\n   ',
 'Name: My image 1 ',
 '\n   ',
 'Name: My image 2 ',
 '\n   ',
 'Name: My image 3 ',
 '\n   ',
 'Name: My image 4 ',
 '\n   ',
 'Name: My image 5 ',
 '\n  ']
  • foo::text не возвращает результатов, если элемент foo существует, но не содержит текста (т.е. текст пуст):

>>> response.css('img::text').getall()
[]

Это означает, что .css('foo::text').get() может возвращает None, даже если элемент существует. Используйте default='', если вам всегда нужна строка:

>>> response.css('img::text').get()
>>> response.css('img::text').get(default='')
''
  • a::attr(href) выбирает значение атрибута href для дочерних ссылок:

>>> response.css('a::attr(href)').getall()
['image1.html',
 'image2.html',
 'image3.html',
 'image4.html',
 'image5.html']

Примечание

См. также: Выбор атрибутов элемента.

Примечание

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

Селекторы вложенности

Методы выбора (.xpath() или .css()) возвращают список селекторов одного и того же типа, поэтому вы также можете вызывать методы выбора для данных селекторов. Вот пример:

>>> links = response.xpath('//a[contains(@href, "image")]')
>>> links.getall()
['<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg"></a>',
 '<a href="image2.html">Name: My image 2 <br><img src="image2_thumb.jpg"></a>',
 '<a href="image3.html">Name: My image 3 <br><img src="image3_thumb.jpg"></a>',
 '<a href="image4.html">Name: My image 4 <br><img src="image4_thumb.jpg"></a>',
 '<a href="image5.html">Name: My image 5 <br><img src="image5_thumb.jpg"></a>']
>>> for index, link in enumerate(links):
...     href_xpath = link.xpath('@href').get()
...     img_xpath = link.xpath('img/@src').get()
...     print(f'Link number {index} points to url {href_xpath!r} and image {img_xpath!r}')
Link number 0 points to url 'image1.html' and image 'image1_thumb.jpg'
Link number 1 points to url 'image2.html' and image 'image2_thumb.jpg'
Link number 2 points to url 'image3.html' and image 'image3_thumb.jpg'
Link number 3 points to url 'image4.html' and image 'image4_thumb.jpg'
Link number 4 points to url 'image5.html' and image 'image5_thumb.jpg'

Выбор атрибутов элемента

Есть несколько способов получить значение атрибута. Во-первых, можно использовать синтаксис XPath:

>>> response.xpath("//a/@href").getall()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

Синтаксис XPath имеет несколько преимуществ: это стандартная функция XPath, а @attributes можно использовать в других частях выражения XPath — например, можно фильтровать по значению атрибута.

Scrapy также предоставляет расширение для селекторов CSS (::attr(...)), которое позволяет получать значения атрибутов:

>>> response.css('a::attr(href)').getall()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

Вдобавок к этому есть свойство Selector .attrib. Вы можете использовать его, если предпочитаете искать атрибуты в коде Python без использования расширений XPath или CSS:

>>> [a.attrib['href'] for a in response.css('a')]
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

Это свойство также доступно в SelectorList; он возвращает словарь с атрибутами первого совпадающего элемента. Удобно использовать, когда ожидается, что селектор даст единственный результат (например, при выборе по идентификатору элемента или при выборе уникального элемента на странице):

>>> response.css('base').attrib
{'href': 'http://example.com/'}
>>> response.css('base').attrib['href']
'http://example.com/'

.attrib пустое свойство SelectorList пусто:

>>> response.css('foo').attrib
{}

Использование селекторов с регулярными выражениями

Selector также имеет метод .re() для извлечения данных с использованием регулярных выражений. Однако, в отличие от методов .xpath() или .css(), .re() возвращает список строк. Таким образом, вы не можете создавать вложенные вызовы .re().

Вот пример, используемый для извлечения имён изображений из HTML кода выше:

>>> response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)')
['My image 1',
 'My image 2',
 'My image 3',
 'My image 4',
 'My image 5']

Есть дополнительный помощник, который отвечает .get() (и его псевдоним .extract_first()) на .re(), с именем .re_first(). Используйте его для извлечения только первой совпадающей строки:

>>> response.xpath('//a[contains(@href, "image")]/text()').re_first(r'Name:\s*(.*)')
'My image 1'

extract() и extract_first()

Если вы давний пользователь Scrapy, вы, вероятно, знакомы с методами выбора .extract() и .extract_first(). Многие сообщения в блогах и учебные пособия также используют их. Данные методы по-прежнему поддерживаются Scrapy, нет никаких планов, по их исключению.

Однако теперь документация по использованию Scrapy написана с использованием методов .get() и .getall(). Мы считаем, что данные новые методы позволяют сделать код более лаконичным и читаемым.

В следующих примерах показано, как данные методы сопоставляются друг с другом.

  1. SelectorList.get() совпадает с SelectorList.extract_first():

    >>> response.css('a::attr(href)').get()
    'image1.html'
    >>> response.css('a::attr(href)').extract_first()
    'image1.html'
    
  2. SelectorList.getall() совпадает с SelectorList.extract():

    >>> response.css('a::attr(href)').getall()
    ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']
    >>> response.css('a::attr(href)').extract()
    ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']
    
  3. Selector.get() совпадает с Selector.extract():

    >>> response.css('a::attr(href)')[0].get()
    'image1.html'
    >>> response.css('a::attr(href)')[0].extract()
    'image1.html'
    
  4. Для согласованности существует также Selector.getall(), который возвращает список:

    >>> response.css('a::attr(href)')[0].getall()
    ['image1.html']
    

Итак, основное отличие состоит в том, что вывод методов .get() и .getall() более предсказуем: .get() всегда возвращает один результат, .getall() всегда возвращает список всех извлеченных результатов. При использовании метода .extract() не всегда было очевидно, является ли результат списком или нет; чтобы получить единый результат, нужно вызвать .extract() или .extract_first().

Работа с XPath

Вот несколько советов, которые могут помочь вам эффективно использовать XPath с селекторами Scrapy. Если вы ещё не очень хорошо знакомы с XPath, вы можете сначала взглянуть на XPath учебник.

Примечание

Некоторые советы основаны на постах из блога Zyte.

Работа с относительными XPath

Имейте в виду, что если вы вставляете селекторы и используете XPath, который начинается с /, данный XPath будет абсолютным для документа, а не относительно Selector, из которого вы его вызываете.

Например, предположим, что вы хотите извлечь все элементы <p> внутри элементов <div>. Сначала вы получить все элементы <div>:

>>> divs = response.xpath('//div')

Сначала у вас может возникнуть соблазн использовать следующий подход, который неверен, поскольку он фактически извлекает все элементы <p> из документа, а не только те, которые находятся внутри элементов <div>:

>>> for p in divs.xpath('//p'):  # this is wrong - gets all <p> from the whole document
...     print(p.get())

Это правильный способ сделать это (обратите внимание на точку перед .//p XPath):

>>> for p in divs.xpath('.//p'):  # extracts all <p> inside
...     print(p.get())

Другой распространенный случай — извлечь всех прямых потомков <p>:

>>> for p in divs.xpath('p'):
...     print(p.get())

Дополнительные сведения об относительных XPath см. в разделе Location Paths в спецификации XPath.

При запросе по классу рассмотрите возможность использования CSS

Поскольку элемент может содержать несколько классов CSS, метод XPath для выбора элементов по классам является довольно подробным:

*[contains(concat(' ', normalize-space(@class), ' '), ' someclass ')]

Если вы используете @class='someclass', вы можете в конечном итоге потерять элементы, которые имеют другие классы, и если вы просто воспользуетесь contains(@class, 'someclass'), чтобы восполнить это, у вас может появиться больше элементов, которые вы хотите, если у них есть другое имя класса, которое разделяет строку someclass.

Как оказалось, селекторы Scrapy позволяют объединять селекторы в цепочку, поэтому в большинстве случаев вы можете просто выбирать по классам с помощью CSS, а затем при необходимости переключаться на XPath:

>>> from scrapy import Selector
>>> sel = Selector(text='<div class="hero shout"><time datetime="2014-07-23 19:00">Special date</time></div>')
>>> sel.css('.shout').xpath('./time/@datetime').getall()
['2014-07-23 19:00']

Это чище, чем использование подробного трюка XPath, показанного выше. Просто не забудьте использовать . в следующих далее выражениях XPath.

Остерегайтесь разницы между //node[1] и (//node)[1]

//node[1] выбирает все узлы, находящиеся первыми в их соответствующих родительских элементах.

(//node)[1] выбирает все узлы в документе, а затем получает только первый из них.

Пример:

>>> from scrapy import Selector
>>> sel = Selector(text="""
....:     <ul class="list">
....:         <li>1</li>
....:         <li>2</li>
....:         <li>3</li>
....:     </ul>
....:     <ul class="list">
....:         <li>4</li>
....:         <li>5</li>
....:         <li>6</li>
....:     </ul>""")
>>> xp = lambda x: sel.xpath(x).getall()

Это получает все первые элементы <li> под тем, что является его родительским:

>>> xp("//li[1]")
['<li>1</li>', '<li>4</li>']

И это получает первый элемент <li> во всем документе:

>>> xp("(//li)[1]")
['<li>1</li>']

Это получает все первые элементы <li> под родительским элементом <ul>:

>>> xp("//ul/li[1]")
['<li>1</li>', '<li>4</li>']

И это получает первый элемент <li> под родительским элементом <ul> во всем документе:

>>> xp("(//ul/li)[1]")
['<li>1</li>']

Использование текстовых узлов в условии

Если вам нужно использовать текстовое содержимое в качестве аргумента для XPath строковой функции, избегайте использования .//text() и используйте вместо него только ..

Это связано с тем, что выражение .//text() дает множество текстовых элементов — node-set. И когда множество узлов преобразуется в строку, что происходит, когда он передаётся в качестве аргумента строковой функции, такой как contains() или starts-with(), в результате получается текст только для первого элемента.

Пример:

>>> from scrapy import Selector
>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')

Преобразование node-set в строку:

>>> sel.xpath('//a//text()').getall() # take a peek at the node-set
['Click here to go to the ', 'Next Page']
>>> sel.xpath("string(//a[1]//text())").getall() # convert it to string
['Click here to go to the ']

Однако node, преобразованный в строку, объединяет текст самого себя плюс всех его потомков:

>>> sel.xpath("//a[1]").getall() # select the first node
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']
>>> sel.xpath("string(//a[1])").getall() # convert it to string
['Click here to go to the Next Page']

Таким образом, использование набора узлов .//text() в этом случае ничего не выберет:

>>> sel.xpath("//a[contains(.//text(), 'Next Page')]").getall()
[]

Но использование . для обозначения узла работает:

>>> sel.xpath("//a[contains(., 'Next Page')]").getall()
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']

Переменные в выражениях XPath

XPath позволяет ссылаться на переменные в выражениях XPath, используя синтаксис $somevariable. Это несколько похоже на параметризованные запросы или подготовленные операторы в мире SQL, где вы заменяете некоторые аргументы в своих запросах заполнителями, такими как ?, которые затем заменяются значениями, переданными с запросом.

Вот пример сопоставления элемента на основе его значения атрибута «id» без его жесткого кодирования (как было показано ранее):

>>> # `$val` used in the expression, a `val` argument needs to be passed
>>> response.xpath('//div[@id=$val]/a/text()', val='images').get()
'Name: My image 1 '

Вот ещё один пример, чтобы найти атрибут id тега <div>, содержащего пять дочерних элементов <a> (здесь мы передаем значение 5 как целое число):

>>> response.xpath('//div[count(a)=$cnt]/@id', cnt=5).get()
'images'

Все ссылки на переменные должны иметь значение привязки при вызове .xpath() (в противном случае вы получить исключение ValueError: XPath error:). Это делается путём передачи необходимого количества именованных аргументов.

parsel, библиотека, управляющая селекторами Scrapy, содержит более подробную информацию и примеры на XPath переменные.

Удаление пространств имён

При сканировании проектов часто бывает удобно полностью избавиться от пространств имён и просто работать с именами элементов, чтобы написать более простые/удобные XPath. Для этого вы можете использовать метод Selector.remove_namespaces().

Давайте покажем пример, иллюстрирующий это, с помощью атом ленты блога Python Insider.

Сначала мы открываем оболочку с URL-адресом, который хотим очистить:

$ scrapy shell https://feeds.feedburner.com/PythonInsider

Так начинается файл:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet ...
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/"
      xmlns:blogger="http://schemas.google.com/blogger/2008"
      xmlns:georss="http://www.georss.org/georss"
      xmlns:gd="http://schemas.google.com/g/2005"
      xmlns:thr="http://purl.org/syndication/thread/1.0"
      xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0">
  ...

Вы можете увидеть несколько объявлений пространств имён, включая объявление по умолчанию «http://www.w3.org/2005/Atom» и ещё одно с префиксом «gd:» для «http://schemas.google.com/g/2005» .

Оказавшись в оболочке, мы можем попробовать выбрать все объекты <link> и убедиться, что это не работает (потому что пространство имён Atom XML запутывает данные узлы):

>>> response.xpath("//link")
[]

Но как только мы вызываем метод Selector.remove_namespaces(), все узлы становятся доступны напрямую по их именам:

>>> response.selector.remove_namespaces()
>>> response.xpath("//link")
[<Selector xpath='//link' data='<link rel="alternate" type="text/html" h'>,
    <Selector xpath='//link' data='<link rel="next" type="application/atom+'>,
    ...

Если вы задаетесь вопросом, почему процедура удаления пространства имён не всегда вызывается по умолчанию вместо того, чтобы вызывать её вручную, это происходит по двум причинам, которые, в порядке значимости, таковы:

  1. Для удаления пространств имён требуется выполнить итерацию и изменить все узлы в документе, что является достаточно затратной операцией для выполнения по умолчанию для всех документов, просматриваемых Scrapy

  2. Могут быть случаи, когда действительно требуется использование пространств имён, если имена некоторых элементов конфликтуют между пространствами имён. Однако такие случаи очень редки.

Использование расширений EXSLT

Созданные на основе lxml, селекторы Scrapy поддерживают некоторые расширения EXSLT и поставляются с данными предварительно зарегистрированными пространствами имён для использования в выражениях XPath:

префикс

пространство имён

использование

re

http://exslt.org/regular-expressions

регулярные выражения

множество

http://exslt.org/sets

манипуляция множеством

Регулярные выражения

Например, функция test() может оказаться весьма полезной, когда недостаточно XPath starts-with() или contains().

Пример выбора ссылок в элементе списка с атрибутом «класс», заканчивающимся цифрой:

>>> from scrapy import Selector
>>> doc = """
... <div>
...     <ul>
...         <li class="item-0"><a href="link1.html">first item</a></li>
...         <li class="item-1"><a href="link2.html">second item</a></li>
...         <li class="item-inactive"><a href="link3.html">third item</a></li>
...         <li class="item-1"><a href="link4.html">fourth item</a></li>
...         <li class="item-0"><a href="link5.html">fifth item</a></li>
...     </ul>
... </div>
... """
>>> sel = Selector(text=doc, type="html")
>>> sel.xpath('//li//@href').getall()
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
>>> sel.xpath('//li[re:test(@class, "item-\d$")]//@href').getall()
['link1.html', 'link2.html', 'link4.html', 'link5.html']

Предупреждение

Библиотека C libxslt изначально не поддерживает регулярные выражения EXSLT, поэтому реализация lxml использует хуки для модуля Python re. Таким образом, использование функций регулярного выражения в выражениях XPath может немного снизить производительность.

Установить операции

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

Пример извлечения микроданных (образец содержимого, взятый с https://schema.org/Product) с группами областей элементов и соответствующих элементов-строк:

>>> doc = """
... <div itemscope itemtype="http://schema.org/Product">
...   <span itemprop="name">Kenmore White 17" Microwave</span>
...   <img src="kenmore-microwave-17in.jpg" alt='Kenmore 17" Microwave' />
...   <div itemprop="aggregateRating"
...     itemscope itemtype="http://schema.org/AggregateRating">
...    Rated <span itemprop="ratingValue">3.5</span>/5
...    based on <span itemprop="reviewCount">11</span> customer reviews
...   </div>
...
...   <div itemprop="offers" itemscope itemtype="http://schema.org/Offer">
...     <span itemprop="price">$55.00</span>
...     <link itemprop="availability" href="http://schema.org/InStock" />In stock
...   </div>
...
...   Product description:
...   <span itemprop="description">0.7 cubic feet countertop microwave.
...   Has six preset cooking categories and convenience features like
...   Add-A-Minute and Child Lock.</span>
...
...   Customer reviews:
...
...   <div itemprop="review" itemscope itemtype="http://schema.org/Review">
...     <span itemprop="name">Not a happy camper</span> -
...     by <span itemprop="author">Ellie</span>,
...     <meta itemprop="datePublished" content="2011-04-01">April 1, 2011
...     <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
...       <meta itemprop="worstRating" content = "1">
...       <span itemprop="ratingValue">1</span>/
...       <span itemprop="bestRating">5</span>stars
...     </div>
...     <span itemprop="description">The lamp burned out and now I have to replace
...     it. </span>
...   </div>
...
...   <div itemprop="review" itemscope itemtype="http://schema.org/Review">
...     <span itemprop="name">Value purchase</span> -
...     by <span itemprop="author">Lucas</span>,
...     <meta itemprop="datePublished" content="2011-03-25">March 25, 2011
...     <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
...       <meta itemprop="worstRating" content = "1"/>
...       <span itemprop="ratingValue">4</span>/
...       <span itemprop="bestRating">5</span>stars
...     </div>
...     <span itemprop="description">Great microwave for the price. It is small and
...     fits in my apartment.</span>
...   </div>
...   ...
... </div>
... """
>>> sel = Selector(text=doc, type="html")
>>> for scope in sel.xpath('//div[@itemscope]'):
...     print("current scope:", scope.xpath('@itemtype').getall())
...     props = scope.xpath('''
...                 set:difference(./descendant::*/@itemprop,
...                                .//*[@itemscope]/*/@itemprop)''')
...     print(f"    properties: {props.getall()}")
...     print("")

current scope: ['http://schema.org/Product']
    properties: ['name', 'aggregateRating', 'offers', 'description', 'review', 'review']

current scope: ['http://schema.org/AggregateRating']
    properties: ['ratingValue', 'reviewCount']

current scope: ['http://schema.org/Offer']
    properties: ['price', 'availability']

current scope: ['http://schema.org/Review']
    properties: ['name', 'author', 'datePublished', 'reviewRating', 'description']

current scope: ['http://schema.org/Rating']
    properties: ['worstRating', 'ratingValue', 'bestRating']

current scope: ['http://schema.org/Review']
    properties: ['name', 'author', 'datePublished', 'reviewRating', 'description']

current scope: ['http://schema.org/Rating']
    properties: ['worstRating', 'ratingValue', 'bestRating']

Здесь мы сначала перебираем элементы itemscope, и для каждого из них ищем все элементы itemprops и исключаем те, которые находятся внутри другого itemscope.

Другие расширения XPath

Селекторы Scrapy также предоставляют сильно упущенную функцию расширения XPath has-class, которая возвращает True для узлов, которые имеют все указанные классы HTML.

Для следующего HTML:

<p class="foo bar-baz">First</p>
<p class="foo">Second</p>
<p class="bar">Third</p>
<p>Fourth</p>

Вы можете использовать это так:

>>> response.xpath('//p[has-class("foo")]')
[<Selector xpath='//p[has-class("foo")]' data='<p class="foo bar-baz">First</p>'>,
 <Selector xpath='//p[has-class("foo")]' data='<p class="foo">Second</p>'>]
>>> response.xpath('//p[has-class("foo", "bar-baz")]')
[<Selector xpath='//p[has-class("foo", "bar-baz")]' data='<p class="foo bar-baz">First</p>'>]
>>> response.xpath('//p[has-class("foo", "bar")]')
[]

Таким образом, XPath //p[has-class("foo", "bar-baz")] примерно эквивалентен CSS p.foo.bar-baz. Обратите внимание, что в большинстве случаев он работает медленнее, потому что это функция на чистом Python, которая вызывается для каждого рассматриваемого узла, тогда как поиск CSS переводится в XPath и, таким образом, работает более эффективно, поэтому с точки зрения производительности его использование ограничено ситуации, которые нелегко описать с помощью селекторов CSS.

Parsel также упрощает добавление ваших собственных расширений XPath.

Справочник по встроенным селекторам

Селектор объектов

SelectorList объекты

Примеры

Примеры селектора в ответе HTML

Вот несколько примеров Selector, иллюстрирующих несколько концепций. Во всех случаях мы предполагаем, что уже существует Selector, созданный с объектом HtmlResponse:

sel = Selector(html_response)
  1. Выбрать все элементы <h1> из тела ответа HTML, возвращая список объектов Selector (т. е. объект SelectorList):

    sel.xpath("//h1")
    
  2. Извлечь текст всех элементов <h1> из тела ответа HTML, вернув список строк:

    sel.xpath("//h1").getall()         # this includes the h1 tag
    sel.xpath("//h1/text()").getall()  # this excludes the h1 tag
    
  3. Перебрать все теги <p> и распечатать их атрибуты класса:

    for node in sel.xpath("//p"):
        print(node.attrib['class'])
    

Примеры селектора в ответе XML

Вот несколько примеров, иллюстрирующих концепции для объектов Selector, созданных с помощью объекта XmlResponse:

sel = Selector(xml_response)
  1. Выберать все элементы <product> из тела ответа XML, возвращая список объектов Selector (т. е. объект SelectorList):

    sel.xpath("//product")
    
  2. Извлечь все цены из XML-фид Google Base, который требует регистрации пространства имён:

    sel.register_namespace("g", "http://base.google.com/ns/1.0")
    sel.xpath("//g:price").getall()