Skip to content

Latest commit

 

History

History
472 lines (351 loc) · 26.7 KB

xpath.markdown

File metadata and controls

472 lines (351 loc) · 26.7 KB

XPath

XPath --- способ извлечения информации из xml-документа.

  • Выборка/адресация нужных нод. Результат --- nodeset.
  • Вычисление выражений. Результат --- string, number, boolean.

Несмотря на то, что в названии xpath есть буква x, синтаксис xpath-выражений не xml'ный. XPath --- это просто строка символов. С другой стороны, path указывает на сходство с другими технологиями: пути в файловой системе, урлы и т.д.

Location Paths --- простейшие xpath'ы

Пример xml'я:

<page name="index" xmlns:lego="https://lego.yandex-team.ru">
    <lego:l-head>
        <lego:b-head-logo>
            <lego:name>Видео</lego:name>
        </lego:b-head-logo>
    </lego:l-head>
</page>

Самый простой --- /. Выбирает document-ноду.

Простой путь: /page/lego:l-head. Т.е. мы выбираем все элементы с именем lego:l-head, которые являются непосредственным потомком элемента page, который, с свою очередь, является непосредственным потомком document-ноды.

Более длинный путь --- /page/lego:l-head/lego:b-head-logo/lego:name. Т.е. location path --- это несколько имен элементов, разделенных символом /, который обозначает отношение "непосредственный потомок".

Практически полный аналог --- это файловая система. Например, /usr/local/www/lego --- обозначает путь в файловой системе. Мы начинаем с корня --- /. Затем в корне мы ищем папка с именем usr, если она существует, то в ней ищем папку local. И т.д.

Основной отличие: в файловой системе на каждом уровне может быть только один файл с заданным именем, в xml на любом уровне может быть несколько нод --- результатом является множество нод --- nodeset.

Пример:

<page name="index" xmlns:lego="https://lego.yandex-team.ru">
    <lego:l-head>
        <lego:b-head-logo>
            <lego:name>Видео</lego:name>
        </lego:b-head-logo>
    </lego:l-head>
    <lego:l-head>
        <lego:b-head-logo>
            <lego:name>Видео</lego:name>
        </lego:b-head-logo>
    </lego:l-head>
</page>

Применяем тот же xpath --- /page/lego:l-head/lego:b-head-logo/lego:name. Получаем множество, состоящее из двух нод lego:name.

/page/lego:b-head-logo --- является валидным xpath'ом, несмотря на то, что у элемента page нет непосредственного потомка с именем lego:b-head-logo. Такой путь просто вернет пустое множество нод.

Еще один аналог --- css-селекторы. Например: div span a b. Этот селектор выбирает все элементы b, которые находятся внутри элементов a, которые находятся внутри span... Правда в xpath разделитель / означает непосредственный потомок, а в css-селекторах пробел означает просто потомок.

Поэтому аналогом xpath'а /page/lego:l-head/lego:b-head-logo/lego:name будет селектор div > span > a > b.

Разделитель // означает отношение просто потомок (не обязательно непосредственный). Селектор /page/lego:b-head-logo возвращает пустой нодесет, но /page//lego:b-head-logo вернет все элементы lego:b-head-logo, находящиеся внутри корневых элементов page.

Путь //lego:name вернет все элементы lego:name в документе, независимо от их расположения в документе.

Вроде как проще и удобнее писать просто //lego:name вместо длинного пути /page/lego:l-head/lego:b-head-logo/lego:name. Но удобство дается не даром, а за счет падения производительности. Чтобы выполнить первый xpath, нужно проверить каждый элемент в дереве --- а не является ли он элементом с именем lego:name? Второй путь намного более специфичен и количество нод, которые нужно проверить намного меньше. Как правило мы избегаем использовать //.

Разделители / и // можно смешивать в одном и том же xpath'е: /page/lego:l-head//lego:name.

Контекст

Команду cd /usr/local/www/lego можно разбить на две части: cd /usr/local; cd www/lego. При этом вторая часть (cd www/lego) будет выполняться от результата действия первой.

XPath /page/lego:l-head/lego:b-head-logo/lego:name можно выполнять в два захода. Спервы выбрать ноды, соответствующие xpath'у /page/lego:l-head, а затем к тому, что получилось, применить lego:b-head-logo/lego:name.

Или же можно сказать, что мы выполняем lego:b-head-logo/lego:name в контексте результата действия /page/lego:l-head.

В примере с директориями контекст это текущая директория:

$ cd /usr/local
$ pwd
/usr/local
$ cd www/lego
$ pwd
/usr/local/www/lego

При этом, выполняя команду cd /usr/local или применяя xpath /page/lego:l-head, мы получаем одинаковый результат независимо от текущего контекста, т.к. адресуемся от корня.

Относительные и абсолютные пути

XPath'ы, начинающиеся с символа / называются абсолютными, все остальные --- относительными. Абсолютные пути не зависят от контекста, относительные наоборот.

Результат выполнения команды cd www/lego или же xpath'а lego:b-head-logo/lego:name зависят от текущего контекста.

Выбор нод без указания имени

Для выбора ноды можно указывать не только конкретное имя.

* соответствует любому имени, например //* выберет все ноды элементов.

Кроме того, можно использовать функции, тестирующие тип ноды:

  • comment() --- соответствует нодам комментариев
  • text() --- соответствует текстовым нодам
  • processing-instruction() --- соответствует нодам инструкций процессора
  • node() --- соответствует ноде любого типа

Атрибуты и родители

@attr --- выбирает ноду атрибута с именем attr. Например, /page/@name.

Чтобы выбрать родительскую ноду используется конструкция ... В абсолютных путях это не имеет смысла, поэтому .. обычно применятеся в относительных путях: /page/some/other/../tag это тоже самое, что и /page/some/tag.

<page name="index" type="normal" xmlns:lego="https://lego.yandex-team.ru">
    <lego:l-head>
        <lego:b-head-logo>
            <lego:name>Видео</lego:name>
        </lego:b-head-logo>
        <lego:b-head-tabs>
            ...
        </lego:b-head-tabs>
        ...
    </lego:l-head>
</page>

Если мы находимся в контексте lego:b-head-logo, то путь ../lego:b-head-tabs совпадает с /page/lego:l-head/lego:b-head-tabs, а ../../@name с /page/@name.

Выражение /page/@* выбирает все атрибуты элемента /page.

/.. --- всегда возвращает пустой нодесет. Потому что у document-ноды нет родителя. В некоторых ситуациях бывает удобно гарантированно получить пустой нодесет.

Текстовые ноды и строковые значения нод

/page/lego:l-head/lego:b-head-logo/lego:name/text() --- текстовая нода, она немного проще чем нода элемента, например у неё нет имени и никаких потомков.

Приведение ноды элемента к тексту проходит с помощью конкатенации значений дочерних текстовых нод и приведения к тексту вложенных нод элементов.

Например, строковое значение такого элемента <item>bla1<subitem>bla2</subitem>bla3</item> будет вычисляться как 'bla1' (значение первой текстовой ноды), склеенное с 'bla2' (результат приведения к строке subitem), склеенное с 'bla3' (значение второй текстовой ноды).

Атрибуты, это простые ноды, у которых нет потомков, в частности нет дочерних текстовых нод ('@attr/text()' несуществует). Значение атрибута хранится в нём самом и используется при приведении атрибута к строке.

При приведении ноды элемента к строке, её атрибуты не учитываются. Например, строковое значение такого элемента <item attr="bla1">bla2</item> будет равно 'bla2'. А строковое значение 'item/@attr' --- 'bla1'.

Поэтому обычно для получения значений элементов мы text() не пишем.

Оси

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

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

  • Простые child:: (сокращённо пишется просто parent-item/child-item, но можно писать parent-item/child::child-item) и descendant-or-self:: (сокращённо пишется через // --- parent-item//descendant-item, но можно писать parent-item/descendant-or-self::descendant-item).
  • Атрибуты, это тоже ось, сокращённо пишется через @ --- item/@attr, но можно писать item/attribute::attr.
  • Обращение к родительской ноде через .., это тоже использование сокращения, для оси parent::. Вместо parent-item/child-item/.. можно писать child-item/parent::parent-item.

Кроме этих существуют ещё оси:

  • self:: содержит только контекстную ноду (сокращённо для выбора текущей ноды, вместо self::node() можно писать ., также работает функция current())
  • descendant:: содержит всех потомков в порядке отдаления "родства"
  • ancestor:: содержит всех предков в порядке отдаления "родства"
  • ancestor-or-self:: содержит текущую ноду и всех предков в порядке отдаления "родства"
  • following:: содержит братьев текущей ноды, расположенных после неё в документе, и их потомков, исключая ноды атрибутов и неймспейсов (для атрибутов и неймспейсов эта ось пустая)
  • following-sibling:: содержит только братьев текущей ноды, расположенных после неё в документе, исключая ноды атрибутов и неймспейсов (для атрибутов и неймспейсов эта ось пустая)
  • preceding:: содержит братьев текущей ноды, расположенных перед ней в документе, и их потомков, исключая ноды атрибутов и неймспейсов (для атрибутов и неймспейсов эта ось пустая)
  • preceding-sibling:: содержит только братьев текущей ноды, расположенных перед ней в документе, исключая ноды атрибутов и неймспейсов (для атрибутов и неймспейсов эта ось пустая)
  • namespace:: содержит неймспейсные ноды (не пустая только для нод элементов)

TODO: Хороших примеров following и preceding. http://www.xmlplease.com/axis

Объединение через |

Два xpath-выражения можно объединить с помощью оператора |. Например /page/a | /page/b' будет выбирать и ноду aи нодуb`.

Расширенное понятие контекста

<items>
    <item>1</item>
    <item>2</item>
    <item>3</item>
</items>

Возьмем простой xpath: /items/item --- он выбирает все три элемента item. Эти элементы образуют коллекцию нод и, если мы обрабатываем эти ноды, то мы их обрабатываем не как независимые ноды, а как ноды из этой коллекции. Для каждого элемента контекст состоит не просто из этой одной ноды. Контекст состоит из:

  • Длины коллекции
  • Позиции данного элемента в коллекции
  • Собственно контекстной ноды

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

Приведение типов

До сих пор мы рассматривали xpath'ы, возвращающие нодесет. Но на самом деле результатом xpath'а может быть:

  • string
  • number
  • boolean
  • node-set

Например, вот такие xpath'ы валидны: 2 + 2, 'nop', 2 &gt; 3...

Если где-то требуется один тип, а по факту приходит другой, то случается приведение типов. В частности, абсолютно любой xpath можно привести к типам boolean и string:

  • Пустой нодесет дает false, не пустой --- true.
  • Пустая строка дает false, не пустая --- true.
  • Число 0 дает false, остальное true.

Явным образом можно привести к типу boolean при помощи функции boolean: boolean(/items/item), boolean(2 + 2), boolean('nop')...

Приведение к типу string:

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

Предикаты

Часто бывает нужно выбрать не все ноды, а только часть, удовлетворяющих определенному условию-фильтру.

Предикат --- фильтрующее булевское выражение, вычисляемое в контексте ноды.

Простые примеры с position():

  • /items/item[position() = 1] --- выбрать первый item
  • /items/item[1] --- тоже самое, шоткат
  • /items/item[position() = -1] --- пустой нодесет, т.к. position() всегда положительное число
  • /items/item[position() = last()] или /items/item[last()] --- выбрать последний item
  • /items/item[position() &lt;= 5] --- выбрать первые 5 элементов, если на самом деле элементов меньше, чем указано, будет выбрано сколько есть
  • /items/item[position() mod 2 = 0] --- выбрать все чётные элементы

Более сложные примеры:

<items>
    <item id="1">
        <a name="i1"></a>
        <strong>First <a href="http://en.wikipedia.org/wiki/Element">element</a></strong>
    </item>
    <item id="2">
        <a name="i2"></a>
        Second
    </item>
    <item>
        <a name="i3"></a>
        <strong>Third <a href="http://www.google.com/search?q=element">element</a></strong>
    </item>
    <item id="last">Last</item>
</items>

Предикаты:

  • /items/item[strong] --- выбрать все item, у которых есть ребенок strong
  • /items/item[@id] --- все item, у которых есть атрибут id
  • /items/item[text()] --- все item, у которых ребенком текстовая нода
  • /items/item[.//a] --- все item, которые содержат элемент a

Все эти предикаты --- strong, @id, text(), //a --- являются на самом деле xpath'ами, которые вычисляются в контексте того пути, которые предшествует предикату (в данном случае /items/item).

Более сложные предикаты

  • /items/item[@id = 1]
  • /items/item[text() = 'Last']
  • /items/item[. = 'Last'] --- это не тоже самое, что и предыдущий xpath, . --- это текущий контекстный узел.
  • /items/item[//text() = 'element']
  • /items/item[strong/a = 'element'] или /items/item[strong/a/text() = 'element']

Когда мы сравниваем строку с нестроковым типом, он приводится к строке. Поэтому мы можем писать: . = 'Last', strong/a = 'element' и т.д.

Помимо = мы можем использовать операторы !=, &lt;, &lt;=, &gt;, &gt;=:

  • /items/item[@id != 1]
  • /items/item[@id &gt; 2]

Кроме того, мы можем использовать логические операторы not, or, and:

  • /items/item[(@id = 1 or @id = 3) and strong/a != 'element']

Важное замечание. При всей кажущейся одинаковости, эти два xpath'а разные:

  • /items/item[@class != 'item']
  • /items/item[not(@class = 'item')]

Пример:

<items>
    <item class="item">First</item>
    <item>Second</item>
    <item class="active">Third</item>
    <item class="item">Last</item>
</items>

Первый xpath выберет третий item --- у него есть атрибут class и он не равен 'item'. А второй xpath выберет второй и третий item: для них не верно утверждение о том, что атрибут class для них равен 'item' --- у второй ноды вообще нет атрибута class.

Функции

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

<items>
    <item id="1">
        <a name="i1"></a>
        <strong>First <a href="http://en.wikipedia.org/wiki/Element">element</a></strong>
    </item>
    <item id="2">
        <a name="i2"></a>
        Second <a href="http://yandex.ru/yandsearch?text=google.com">element</a>
    </item>
    <item>
        <a name="i3"></a>
        <strong>Third <a href="http://www.google.com/search?q=element">element</a></strong>
    </item>
    <item id="last">Last</item>
</items>
  • /items/item[count(*) &gt; 1] --- больше одного ребенка.
  • /items/item[contains(//a/@href, 'google.com')] --- item, который содержит ссылку, содержащую в урле 'google.com'.
  • /items/item[starts-with(//a/@href, 'http://www.google.com')] --- ссылка ведет на google.com.
  • /items/item[string-length(text()) &gt; 5] --- все item, у которых есть "длинные" текстовые подноды.
  • //a/@href[substring-after(., 'text=') = 'google.com']
  • //a/@href[substring-before(., '://') = 'http']
  • //a/@href[substring(., 1, 4) = 'http']
  • normalize-space(/items/item[@id = 2]/text()) --- 'Second'
  • concat(substring-before(/items/item[2]//a/@href, '?'), '?text=запрос')

Поскольку предикатом может быть любой xpath, то, в частности, это может быть и xpath с предикатом:

/items/item[strong[a[@href]]]

Предикатов может быть несколько:

/items/item[@id][position() &gt; 1]

При этом второй предикат --- position() &gt; 1 --- вычисляется в контексте результатов предшествущего xpath'а --- /items/item[@id].

Вообще говоря, предикаты некоммутативны: [xpath1][xpath2] и [xpath2][xpath1] не одно и тоже. Если второй предикат использует position(), то результаты будут разные:

<items>
    <item id="1">First</item>
    <item>Second</item>
    <item id="3">Third</item>
</items>
  • /items/item[@id][position() = 2] --- выбирает второй item, у которого есть атрибут id.
  • /items/item[position() = 2][@id] --- пустой нодесет, потому что у второго item'а нет атрибута id.

В случае, когда [xpath1][xpath2] совпадает с [xpath2][xpath1], то это тоже самое, что и просто [xpath1 and xpath2].

Сравнение нодесетов в предикатах

<page>
    <post-ids>
        <id>2</id>
        <id>3</id>
    </post-ids>
    <posts>
        <post id="1">Text #1</post>
        <post id="2">Text #2</post>
        <post id="3">Text #3</post>
    </posts>
</page>

Нужно выбрать все посты, id которых содержатся в списке post-ids.

/page/posts/post[@id = /page/post-ids/id]

В этом предикате сравнивается строка с целым нодесетом. При этом не срабатывает дефолтное правило про приведение нодесета к строке. Вместо этого каждая нода нодесета /page/post-ids/id приводится к строке и сравнивается со строкой @id. Если есть хотя бы одно совпадение, то все выражение вычислится в true. Т.е. сравнивая строку с нодесетом, мы на самом деле проверяем, содержится ли эта строка в нодесете.

Более сложный пример:

<posts>
    <author>nop</author>
    <author>veged</author>
    <post id="1">
        <author>nop</author>
        <author>veged</author>
        <text>Blah by nop and veged</text>
    </post>
    <post id="2">
        <author>vitaly</author>
        <author>veged</author>
        <text>Blah by veged and vitaly</text>
    </post>
    <post id="3">
        <author>vitaly</author>
        <author>tyv</author>
        <text>Blah by tyv and vitaly</text>
    </post>
</posts>

Есть список постов, у каждого поста есть несколько авторов. Плюс есть отдельный список авторов, которым можно писать посты. Нужно выбрать все посты, у которых есть хотя бы один автор, которому можно писать посты.

/posts/post[author = /posts/author]

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