XPath --- способ извлечения информации из xml-документа.
- Выборка/адресация нужных нод. Результат --- nodeset.
- Вычисление выражений. Результат --- string, number, boolean.
Несмотря на то, что в названии xpath есть буква x, синтаксис xpath-выражений не xml'ный. XPath --- это просто строка символов. С другой стороны, path указывает на сходство с другими технологиями: пути в файловой системе, урлы и т.д.
Пример 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 > 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() <= 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'
и т.д.
Помимо =
мы можем использовать операторы !=
, <
, <=
, >
, >=
:
/items/item[@id != 1]
/items/item[@id > 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(*) > 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()) > 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() > 1]
При этом второй предикат --- position() > 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
, если слева и справа
хотя бы по одной ноде с одинаковым строковым значением.