XPAth html запросы

XPath — это мощный язык, который часто используется при парсинге сайтов. Он позволяет обращаться к узлам (node, ноды) или высчитывать значения из XML и HTML. Похожие функции используют CSS селекторы, но XPath позволяет делать гораздо больше.

С XPath вы можете парсить данные на основе текстовых элементов и не только в структуре страницы. Поэтому когда вам нужно спарсить достаточно «кривой» сайт, XPath может сохранить много вашего времени.

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

Для своих экспериментов с XPath вы можете использовать этот сервис.

Основы основ

Допустим, у нас есть такой документ:

<!-- html -->
<title>This is page</title>
 <h2>Go to my cool <a href="#">page</a></h2>
  <p>It is the first paragraph.</p>
  <p>This is the second paragraph.</p>

Xpath представляет любой XML/HTML документ как дерево элементов (узлов). Коренная нода (root) не является частью элемента, но считается родительским узлом начального элемента в документе (для HTML это — «). Примерно так это выглядит:

XPAth html запросы представляются в виде дерева элементов

Как видите, в дереве XPath есть несколько типов для узлов.

  • Элемент: представляет HTML-элемент, например, тег <h2>.
  • Атрибут: представляет атрибут элемента, например, атрибут href в теге <a href="http://www.mysite.com">mysite</a>.
  • Текстовый узел: представляет текст внутри элемента, например, mysite в <p>mysite</p>.
  • Комментарий: представляет комментарий в документе («).

Важно осознавать разницу между этими узлами. А теперь давайте окунемся в XPath.

Вот так с помощью XPath выделяется элемент:

/html/head/title

Такие пути называются location path. Они позволяют указывать путь относительно контекстного узла (в данном случае root). Этот путь состоит из трех частей, разделенных cлэшами. Оно означает «начиная с элемента html, ищи внутри элемент head, а внутри него title«. Контекстный узел меняется каждый шаг, так он будет равняться head на последнем шаге.

Обычно мы не знаем (или нам просто лень) точный путь узел-в-узел, мы можем использовать поиск по всему документу:

//title

Это выражение означает «просмотри все дерево от начала (//) и найди элемент title«.

Вообще, выражения, которые мы видели выше — это сокращенный синтаксис XPath. Полная версия прошлого выражения будет выглядеть так:

/descendant-or-self::node()/child::title

То есть // это аналог descendant-or-self, что означает текущий узел, или любой уровнями ниже. Эта часть выражения называется осью (axis) и определяет набор узлов, из которых будет выполняться выборка (уровнями ниже, выше или на том же уровне).

Следующая часть выражения — node (), которая называется тестом узла (node test), которая сохраняет выражение, по которому решается следует выбирать текущий узел или нет. В данном случае выделяются узлы всех типов. Затем идет другая ось — child, которая означает «пройди по дочерних узлах, относительно текущего», а тест узла в данном случае title.

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

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

Вот несколько примеров:

  • /Html — выбирает все узлы с именем html относительно коренного элемента.
  • /Html/head — выбирает узлы названы head в узле html.
  • //title — выбирает все узлы title в документе.
  • //h2 a — выбирает все узлы a в документе, вложенные в узел h2.

А вот некоторые примеры выделения по типу:

  • //comment() — выделяет только узлы комментариев.
  • //node() — выделяет все узлы в дереве.
  • //text() — выделяет только текстовые узлы.
  • // * — выделяет все узлы, за исключением комментариев и текстовых узлов.

И, конечно, мы можем комбинировать эти способы:

//p/text()

Это выражение выделяет текстовые узлы внутри всех элементов p. В HTML, который мы показывали выше, это выражение выделит «It is the first paragraph.» и «This is the second paragraph.».

А теперь давайте рассмотрим как мы можем фильтровать результаты. Пусть, у нас есть такой документ:

<ul>
      <li>Line 1</li>
      <li>Line 2 with <a href="...">link</a></li>
      <li>Line 3 with <a href="...">second link</a></li>
      <li><h2>Line 4 title</h2></li>
    </ul>

Выделить первый пункт списка мы можем вот так:

//li[position() = 1]

Выражение в квадратных скобках называется предикатом. Он и фильтрует узлы, возвращаются по выражению //li. В данном случае он проверяет позицию каждого узла, используя функцию position(), которая возвращает позицию текущего узла в результате (наборе узлов). Заметьте, что нумерация начинается с 1. Иначе это выражение можно записать так:

//li[1]

Оба выражения вернут это:

<li class="line">Line 1</li>

Вот несколько примеров предикатов:

  • //li[position()%2=0] — выделяет элементы li на четных позициях.
  • //li[a] — выделяет элементы li, в которых есть элемент a.
  • //li[h2 or a] — выделяет элементы li, в которых есть элемент h2 или a.
  • //li[a[text()="link"]] — выделяет элементы li, в которых элемент a с текстом «link». Также можно записать как //li[a/text()="link"].
  • //li[last()] — выделяет последний элемент li в документе.

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

//li[ 4 ]/h2[ text() = "Line 4 title" ]

А вот аналог без сокращений:

/descendant-or-self::node()
    /child::li[ position() = 4 ]
        /child::h2[ text() = "Line 4 title" ]

Также мы можем объединить два выражения в один с помощью оператора |. Например, мы можем выделить все элементы a и h2 в документе.

//a | //h2

Теперь рассмотрим следующий документ:

    <ul>
      <li id="pr-lt"><a href="https://examplesite.com">examplesite</a></li>
      <li><a href="https://examplesitestack.com">examplesitestack</a></li>
      <li><a href="https://blog.examplesite.com">domhtmlstack blog</a></li>
      <li id="sh-cl"><a href="http://hub.toexamplesite.com">hub to examplesite</a></li>
    </ul>

Представим, что нам нужно получить все ссылки на HTTPS URL’ом. Мы можем сделать это проверкой атрибута href:

//a[starts-with(@href, "https")]

Это выражение сначала выделяет все ссылки на странице, а затем проверяет начинается атрибут href с https. Доступ к атрибуту осуществляется с помощью синтаксиса @attributename.

И еще несколько примеров.

  • //a[@href="https://examplesite.com "] — выделяет элементы a, ведущие к https://examplesite.com.
  • // a / @ href — выделяет адреса, на которые ведут ссылки на странице.
  • // li [@id] — выделяет только те элементы li, для которых задано id.

Больше про ось

До этого мы видели два вида осей:

  • descendant-or-self
  • child

Но их гораздо больше. Рассмотрим такой документ:

    <p>First paragraph</p>
    <h2>Brand #1</h2>
    <p>Another paragraph #1</p>
    <p>Random one #1</p>
    A second paragraph, with no markup
    <h2>Brand #2</h2>
    <p>Another paragraph #2</p>
    <p>Random one #2</p>
    A third paragraph, with no markup
    <div><p>Footer data</p></div>

Теперь мы хотим добыть только первый параграф после каждого заголовка. Чтобы сделать это, мы можем использовать ось following-sibling, которая выделяет все элементы на том же уровне после текущего элемента.

//h1/following-sibling::p[1]

В данном примере контекстной нодой, к которой применялось following-sibling была h2.

А если мы хотим выделить текст перед футером? Мы можем использовать preceding-sibling:

//div[@id='footer']/preceding-sibling::text()[1]

В данном случае мы выделяем первую текстовую ноду перед футером («A third paragraph, with no markup»).

XPath также позволяет нам выделять элементы, основываясь на их текстовом контенте. Мы можем использовать эту фичу вместе с осью parent, чтобы получить родительский узел элемента ‘p’ с текстом «Footer data».

//p[ text()="Footer data" ]/..

В результате мы получим элемент «Footer data». Как видите, «..» используется как сокращение для оси parent.

Альтернативой выражения выше может быть такое выражение:

//*[p/text()="Footer data"]

Оно выделяет все элементы в которые вложены элементы p с текстом «Footer data».

Вы можете найти всю спецификацию здесь.

Оставьте комментарий

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