Fruits
Apple
Banana
Vegetables
Carrot
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()``
Why ``//*`` is not ``/descendant-or-self::*``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The abbreviation ``//`` is a frequent source of confusion. It does **not**
expand to ``/descendant-or-self::``; it expands to
``/descendant-or-self::node()/`` -- note the ``node()`` node test *and* the
trailing slash, which introduces another step.
So ``//*`` is really::
/descendant-or-self::node()/child::*
which reads as "every element that is the child of any node", i.e. every
element in the document.
``/descendant-or-self::*``, on the other hand, is a **single step**: "the
context node or any of its descendants, provided it is an element".
From the root node these two happen to select the same set (the root is not an
element, so both return every element). The difference becomes visible as soon
as you use them **relative to an element**, because ``descendant-or-self``
includes the context node itself, while the ``child::*`` step at the end of
``.//*`` never can:
.. code:: pycon
>>> paragraph = doc.xpath("//p[2]")[0]
>>> paragraph.xpath(".//*") # descendants only
[(1) 'a link']
>>> paragraph.xpath("descendant-or-self::*") # the itself, then its descendants
[(1) ' Is this a link? This is a paragraph. Is this a link?
| | | +-- #30--(TXT): '\n Apparently.\n '
| | +-- #31--(TXT): '\n '
| | +-- #32--
``,
````, ````, ``
``) and keep tags balanced, or you will get
a parse error. parsel's HTML parser is much more forgiving and happily
accepts the sloppy HTML found on real pages (including the sample document
above). For the same reason the widget's output is cosmetically slightly
different from parsel's: void elements come back self-closed (``
``
rather than ``
``) and numbers without a trailing ``.0`` (``2`` rather
than ``'2.0'``).
.. note::
lxml itself is built using the C library `libxml2
`` being selected? It's an empty element (i.e. with no child
nodes) 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()
You may have expected only one text result, the last one, ``"Apparently."``.
But we got four! And three of them are blank even. Why is that?
In fact, HTML authors usually indent their tags with whitespace for
readability. This does not usually change the layout in your browser.
But this **whitespace counts as text nodes** for XPath's data model,
it is not stripped nor filtered.
Let's represent that ``
+-- #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