This is a paragraph
``), - or **attribute nodes** (``href="page.html"`` inside an ```` tag), - or **text nodes** (``"I have something to say"``), - or **comment nodes** (````), - (or root nodes, or namespace nodes, or processing instructions nodes but we will not cover them here.) In XPath's data model, everything is a node : elements, attributes, comments... (**but not all nodes are elements.**) And nodes have an order, the **document order**: the order in which they appear in the XML/HTML source. In effect, this data model allows you to represent everything inside an XML or HTML document, in a structured, ordered and hierarchical way. Throughout this tutorial, we'll use the following sample HTML page to illustrate how XPath works: ::| | | | +-- #20--(TXT): 'This is a paragraph.' | | | +-- #21--(TXT): '\n ' | | | +-- #22--
| | | | +-- #23--(TXT): 'Is this '
| | | | +-- #24--
| | | | | +-- #25--(ATTR): href: 'page2.html'
| | | | | +-- #26--(TXT): 'a link'
| | | | +-- #27--(TXT): '?'
| | | +-- #28--(TXT): '\n '
| | | +-- #29-- ``: these are element nodes
- ``(TXT)`` represent text nodes
- ``(ATTR)`` represent attribute nodes
- ``(COMM)`` represent comment nodes
The ``# Is this a link? `` and `` `` paragraph elements
inside the `` This is a paragraph. Is this a link? This is a paragraph. Is this a link? This is a paragraph. Is this a link? `` under each `` This is a paragraph. Is this a link? This is a paragraph. Is this a link?
| +-- #22--
| | +-- #24--
| +-- #29--
| | +-- #22--
| | | +-- #24--
| | +-- #29-- This is a paragraph. Is this a link? Is this a link? This is a paragraph.
| |
| |
self::* -----------------------> +-- #22--
| | |
| | +-- #24--
| |
| |
following-sibling::* ----------> +-- #29-- `` elements. Why did they not get selected?
The reason is that ``child::*`` means "any child *element*", not "any node."
(Remember that text nodes are not elements.)
To also get text node siblings, you need to use either ``child::text()``
or ``child::node()``. (But we may be getting ahead of ourselves with *node tests*.)
.. code:: pycon
>>> paragraph.xpath('following-sibling::node()')
[(1) '
'
(2) ' This is a paragraph.
| |
| |
self::* -----------------------> +-- #22--
| | |
| | +-- #24--
| |
| |
following::* -----------+------> +-- #29-- `` element.
Whereas ``string(self::*)`` applies to the paragraph (the context node,
selected with ``self::*``) and recursively gets text content of
children, children of children and so on.
Abbreviation cheatsheet
-----------------------
.. list-table::
:header-rows: 1
* - Abbreviated step
- Meaning
* - ``*`` (asterisk)
- all **element** nodes (i.e. not text nodes, not attribute nodes).
Remember that ``.//*`` is not the same as ``.//node()``.
Also, there's no ``element()`` node test.
* - ``@*``
- ``attribute::*`` (all attribute nodes)
* - ``//``
- ``/descendant-or-self::node()/`` (exactly this, nothing more, nothing less)
so ``//*`` is not the same as ``/descendant-or-self::*``
* - ``.`` (a single dot)
- ``self::node()``, the context node; useful for making XPaths relative,
e.g. ``.//tr``
* - ``..`` (2 dots)
- ``parent::node()``
TODO: explain why ``//*`` is not the same as ``/descendant-or-self::*``
Predicates
----------
.. important::
Predicates are the last part of each step in a location path. Predicates
are optional.
::
axis :: nodetest [PREDICATE]*
They are used to further filter nodes on properties that cannot be
expressed with the step's axis and node test.
Remember that XPath location paths work step by step. Each step produces
a node-set for each node from the previous step's node-set, with
possibly more than 1 node in each node set.
You may not be interested in all nodes from a node test. And predicates
are used to tell the XPath engine the condition(s) they should meet.
The syntax for predicates is simple: just surround conditions within
square brackets. What's inside the square brackets can be:
- a number (see positional predicates below)
- a location path: the predicate will select nodes for which the
location path matches at least a node
- a boolean operation: for example to test a condition on text content
or count of children
Positional predicates
~~~~~~~~~~~~~~~~~~~~~
The first use-case is selecting nodes based on their position in a
node-set. (Node-sets order depends on the axis, but let's consider that
the order of a node in a node-set is the document order.)
Remember the two paragraphs in the `` This is a paragraph. Is this a link?
| | | +-- #30--(TXT): '\n Apparently.\n '
| | +-- #31--(TXT): '\n '
| | +-- #32--
`` being selected? It's an empty element (i.e. with node children)
but it is there nonetheless.
Selecting text nodes
~~~~~~~~~~~~~~~~~~~~
If we stay around these ``
`` elements, you may have noticed
that the ASCII tree representation from the beginning also shows some text after the
``
`` break: the string ``"Apparently."``. It is a text node.
Selecting text nodes is a bit different than selecting elements:
you use the special ``text()`` syntax. Let's try it by replacing the last
part of our last XPath expression, forming ``/html/body/div/div[1]/text()``:
.. xpathdemo:: /html/body/div/div[1]/text()
+-- #32--
+-----------> +-- #32--
'
(6) '
']
Again, let's see which elements were selected in our ASCII tree
representation:
::
# 0--(ROOT)
+-- # 1--
+-- # 3--
| +-- # 5--
|
|
+-- #32--
'
(3) '
Apparently.
']
.. code:: pycon
>>> paragraph.xpath('following-sibling::text()')
[(1) '
'
(2) '
Apparently.
']
Nodes before and after, in document order
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``preceding`` and ``following`` are two special axes that do not look at
the tree hierarchy, but work on the document order of nodes.
.. important::
Remember, all nodes in XPath data model have an order, called the
*document order*. Node 1 is the first node in the HTML source, node 2 is
the node appearing next etc.
::
#1 #2 #3 ...
'
(2) '
| |
| |
+--> +-- #32--
`` row, ``td/a`` selects something on some rows,
nothing for others:
.. xpathdemo:: //table/tr [ td/a ]
first row second row with a link third row fourth row with another link `` that has a specific
number of rows, say, 5. You can simply count the number of rows:
.. xpathdemo:: //table[ count(tr)=5 ]
first row second row third row
first row second row third row fourth row fifth row
The header I want second row third row
Another header I do NOT want second row third row fourth row fifth row `` so you can move the ``tr/th`` part inside
a predicate and compare it with string "The header I want".
.. xpathdemo:: //table[ tr/th = "The header I want" ]
The header I want second row third row
Another header I do NOT want second row third row fourth row fifth row