scrapy/docs/topics/xpath-tutorial.rst

2325 lines
66 KiB
ReStructuredText
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

==============
XPath Tutorial
==============
Part 1: What is XPath?
======================
XPath is a language
-------------------
.. epigraph::
*"XPath is a language for addressing parts of an XML document"*
-- `XML Path Language 1.0 <https://www.w3.org/TR/xpath/>`__
This abstract from the official specifications says it all:
- *"XPath is a language"*: an XPath expression is a character string...
- *"for addressing parts of an XML document"*: ...a string that that
you pass to an XPath engine acting over an XML (or HTML) document,
outputting parts of it, and following the data model explained below.
Why learn XPath?
----------------
- with XPath, you can navigate **everywhere** inside a DOM tree
- it's a must-have skill for accurate web data extraction
- XPath is more powerful than CSS selectors
- it allows selection and filtering with a fine-grained look at the
text content
- XPath allows complex conditioning with axes
- XPath is extensible with custom functions (we wont cover that in
this tutorial though)
XPath data model
----------------
XPath's `data model <http://www.w3.org/TR/xpath/#data-model>`__ is a
tree of nodes representing a document. Nodes can be either:
- **element nodes** (``<p>This is a paragraph</p>``),
- or **attribute nodes** (``href="page.html"`` inside an ``<a>`` tag),
- or **text nodes** (``"I have something to say"``),
- or **comment nodes** (``<!-- a comment -->``),
- (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:
::
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type">
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Here is an ASCII tree representation of our toy HTML document for an
XPath engine, according to the data model:
::
# 0--(ROOT)
+-- # 1--<html>
+-- # 2--(TXT): '\n'
+-- # 3--<head>
| +-- # 4--(TXT): '\n '
| +-- # 5--<title>
| | +-- # 6--(TXT): 'This is a title'
| +-- # 7--(TXT): '\n '
| +-- # 8--<meta>
| | +-- # 9--(ATTR): content: 'text/html; charset=utf-8'
| | +-- #10--(ATTR): http-equiv: 'content-type'
| +-- #11--(TXT): '\n'
+-- #12--(TXT): '\n'
+-- #13--<body>
| +-- #14--(TXT): '\n '
| +-- #15--<div>
| | +-- #16--(TXT): '\n '
| | +-- #17--<div>
| | | +-- #18--(TXT): '\n '
| | | +-- #19--<p>
| | | | +-- #20--(TXT): 'This is a paragraph.'
| | | +-- #21--(TXT): '\n '
| | | +-- #22--<p>
| | | | +-- #23--(TXT): 'Is this '
| | | | +-- #24--<a>
| | | | | +-- #25--(ATTR): href: 'page2.html'
| | | | | +-- #26--(TXT): 'a link'
| | | | +-- #27--(TXT): '?'
| | | +-- #28--(TXT): '\n '
| | | +-- #29--<br>
| | | +-- #30--(TXT): '\n Apparently.\n '
| | +-- #31--(TXT): '\n '
| | +-- #32--<div>
| | | +-- #33--(ATTR): class: 'second'
| | | +-- #34--(TXT): '\n Nothing to add.\n Except maybe this '
| | | +-- #35--<a>
| | | | +-- #36--(ATTR): href: 'page3.html'
| | | | +-- #37--(TXT): 'other link'
| | | +-- #38--(TXT): '. \n '
| | | +-- #39--(COMM): ' And this comment '
| | | +-- #40--(TXT): '\n '
| | +-- #41--(TXT): '\n '
| +-- #42--(TXT): '\n'
+-- #43--(TXT): '\n'
You can see various tree branches and leaves:
- e.g. ``<div>`` or ``<p>``: these are element nodes
- ``(TXT)`` represent text nodes
- ``(ATTR)`` represent attribute nodes
- ``(COMM)`` represent comment nodes
The ``#<number>`` are the document orders of each node.
.. note::
You can also notice that **text with only whitespace** (space and
newlines in our example) **are proper nodes**, they do have their
document order and can be selected with XPath.
In-browser widget and using parsel
----------------------------------
To illustrate and learn XPath, we will use an in-browser widget
allowing you to play around with XPath expressions and see the output
live.
We will also illustrate some Python pattern for data extraction with
XPath using the `parsel <https://github.com/scrapy/parsel>`__ library
which powers Scrapy selectors under the hood.
It is a Python module written on top of `lxml <http://lxml.de/>`__.
.. note::
lxml itself is built using the C library `libxml2 <http://www.xmlsoft.org/>`__,
which has a conformant XPath 1.0 engine.
You should be able to run the same XPath expressions with
any XPath 1.0 engine, and get the same results.
This tutorial only showcases XPath 1.0. (`XPath has reached version 3
<https://www.w3.org/TR/xpath-3/>`__, but you can already do a
lot with XPath 1.0 and Python. And there's no XPath>1.0 implementation
in Python today.)
When showing Python code snippets using Parsel, we assume that we have
a ``Selector`` -- called ``doc`` -- created with the HTML content, similarly
to the following:
.. code:: python
import parsel
htmlsample = '''<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>'''
#
# Below is a small "hack" to change the representation of extracted
# nodes when using parsel.
# This is to represent return values as serialized HTML element or
# string, and not parsel's wrapper objects.
#
parsel.Selector.__str__ = parsel.Selector.extract
parsel.Selector.__repr__ = parsel.Selector.__str__
parsel.SelectorList.__repr__ = lambda x: '[{}]'.format(
'\n '.join("({}) {!r}".format(i, repr(s))
for i, s in enumerate(x, start=1))
).replace(r'\n', '\n')
doc = parsel.Selector(text=htmlsample)
XPath return types
------------------
When applied over a document, an XPath expression can return either:
- a node-set -- this is the most common case, and often it's a set of
element nodes
- a string
- a number (floating point)
- a boolean
.. note::
**When an XPath expression returns a node-set, you do get a set of
nodes, even if there's only one node in the set.**
With parsel, you get a ``list`` of nodes though, not a Python ``set``.
XPath expressions
-----------------
We will now take a look at some example XPath expressions to get a
feeling of how they work. We'll explain the syntax in more details later
on.
XPath expressions are passed to an XPath engine as strings.
Selecting the root node of a document (warning: special case)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The root node is a special node: this is a quote from XPath 1.0 specs:
*"The root node is the root of the tree. A root node does not occur
except as the root of the tree. The element node for the document
element is a child of the root node."*
Selecting the root node of a document with XPath is one of the shortest
XPath expressions: ``"/"`` (a string with only a forward slash).
.. xpathdemo:: /
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
This is very similar to ``cd /`` in a shell within a Unix filesystem
(going to the root directory).
.. warning::
Unfortunately, this ``"/"`` expression does not work as expected
with parsel. We get an empty list instead of the root node.
It is a limitation of lxml apparently, because
it works with libxml2 directly. In practice though, this doesn't matter
much because the root node is virtually never used directly.
Selecting elements
~~~~~~~~~~~~~~~~~~
Elements build the structure and hierarchy of the document. An element
in HTML (and XML) is what you see in the source code between an opening
and corresponding closing tag, and everything in between.
- ``<title>This is a title</title>`` is a ``title`` element,
- ``<p>Is this <a href="page2.html">a link</a>?</p>`` is a ``p``
(paragraph) element.
Selecting elements is probably the most common use-case for XPath on
HTML documents.
Elements can have children -- the root node being the ancestor of them
all. Their children can also have children and so on. Sometimes,
elements only have one child. This hierarchy forms a family tree of nodes.
.. note::
**Text nodes are not elements.** (They are still nodes, obviously.)
They do not have children nodes, but they are always children
of some element.
Therefore, text nodes are always leaves of the document tree.
We said earlier that the document element is a child of the root node.
In fact, the document element is the only child of the root node. And
for our sample HTML document, it's the top-level ``<html>...</html>`` element.
Still, selecting it will return a single-node node-set, the XPath expression
being ``/*``:
.. xpathdemo:: /*
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
The asterisk here, ``*``, means "any element". And ``/*`` means "any
element under the root node". HTML documents have only one element like
this: the ``<html>`` element.
Another example: how to get ``<title>`` elements? Use ``/html/head/title``:
.. xpathdemo:: /html/head/title
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Again, if you are familiar with the Unix filesystem, you probably
intuitively understand what this does:
* start from the root (of the document)
* select the ``<html>`` node (with ``/html``)
* select the ``<head>`` node under the ``<html>`` node
(appending ``/head``)
* select the ``<title>`` node under the ``<head>`` node
(appending ``/title``)
In other words, the XPath expression represents the path from the root
node down to the target node(s). Parts of this path are read **from left to right**,
and represent a top-to-bottom direction in the document tree.
Much like a Unix filepath represents the path from the filesystem's root
to the target file(s) or directory(ies).
There's one major difference with a Unix filesystem though: in an HTML
or XML document, an element can have multiple children with the same name.
For example, the ``<div>`` just under the ``<body>`` has 2 ``<div>`` children:
.. xpathdemo:: /html/body/div/div
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Another example is getting the paragraphs inside the first child of that
``<div>`` under ``<body>``, there are two of them:
.. xpathdemo:: /html/body/div/div[1]/p
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Here we're introducing a **positional predicate**, ``[1]``. The ``div[1]``
part means *"the first <div> child under its parent"*.
If you recall, earlier we used a ``*`` asterisk to mean *any element*.
There are other elements with those two paragraphs under that very
``<div>``. Let's try and select all of them, regardless of their name:
.. xpathdemo:: /html/body/div/div[1]/*
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
.. note::
Continuing the filesystem anamogy, ``*`` is similar in effect to what
you can do in a Unix shell to find files or directories without explicit
full names.
See the ``<br/>`` 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 ``<p>`` and ``<br>`` elements, you may have noticed
that the ASCII tree representation from the beginning also shows some text after the
``<br/>`` 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()
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
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 ``<div>`` as a Python string as it appears in the
HTML source::
#
# text node #1 text node #2 text node #3
# <------> <------> <------>
'<div>\n <p>This is a paragraph.</p>\n <p>Is this <a href="page2.html">a link</a>?</p>\n <br>\n Apparently.\n </div>'
We've marked the first three text nodes before the non-whitespace only
text node.
Another example is to get the text nodes of ``<title>`` elements
(remember that ``<title>`` is an element, and that it happens it
contains a text node, with the string content "This is a title"):
.. xpathdemo:: /html/head/title/text()
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
.. note::
Again, there's only one ``<title>``, and it contains only one text node,
but selecting text nodes in ``<title>`` returns a single string-value
in a list, not one string.
Selecting nodes without a full, explicit path
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
What we did until now is tell the XPath engine how to get to nodes,
node by node, from parent to child, from root node down to target nodes.
This assumes that you know the hierarchy of nodes beforehand.
This *can* be the case, but most often than not,
either you do not know or you do not want to indicate all the steps from
the root node down to the node(s) you are interested in (this can be
very error prone -- have you put enough ``div/div/div...``?).
XPath provides a handy shortcut when you do not know at what level you
expect your target node to be.
Say for example that we want to select all ``<p>`` paragraph elements
inside the ``<body>``. We don't *a-priori* know what their parent node is.
(For all we know, they can be anywhere under the ``<body>`` element.)
The shortcut to use is ``//`` (two forward slashes).
Let's try this: ``//body//p``
.. xpathdemo:: //body//p
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
So we got 2 paragraphs, what we expected.
This also works for text nodes (there are a lot of them in our sample
document!). Try ``//body//text()``:
.. xpathdemo:: //body//text()
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Selecting attributes
~~~~~~~~~~~~~~~~~~~~
Elements can also have attributes.
In our sample document, we have two ``<a>`` elements, each with a
``href`` attribute. There's also a ``<meta>`` element with two
attributes: ``content`` and ``http-equiv``.
This is how you can select these attributes, with an ``@`` prefix before
the attribute name:
.. xpathdemo:: //a/@href
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
.. xpathdemo:: //meta/@*
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
The ``*`` (asterisk) here after ``@`` means the same thing as in ``/*``
exept that this is for attributes, and not elements: meaning that you
want any attributes, whatever their name.
Get a string representation of an element
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The XPath language also comes with a few string functions, that you can
wrap around an XPath expression selecting elements:
.. xpathdemo:: string(/html/head/title)
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
This example uses ``string(<xpathexpression>)``, one of several handy
`functions <https://www.w3.org/TR/xpath/#section-String-Functions>`__ in XPath.
``string()`` will concatenate all text content from the selected node
and all of its children, recursively, effectively stripping HTML tags.
You may wonder what's the difference between ``string(/html/head/title)``
and ``/html/head/title/text()`` from earlier? Here, in fact, you get the same
result because ``<title>`` only has one child text node.
(Concatenating this list of one text node is the same as getting it
directly with ``text()`` at the end.)
But string functions can be very handy when you apply them on nodes that
have multiple children and multiple text node children or descendant.
What happens when you apply ``string()`` on the document ``<body>`` for example?
You get a text representation of the document, without the tags:
.. xpathdemo:: string(//body)
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Counting elements
~~~~~~~~~~~~~~~~~
We said earlier that XPath expressions could also return numbers.
One example of this is counting the number of paragraphs in the
document:
.. xpathdemo:: count(//p)
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
.. note::
With parsel, you get a floating point number back, and in the form of a
string. This is specific to parsel. Another XPath engine might return a
native floating point number.
Another example: get the number of attributes in the document (whatever
their parent element):
.. xpathdemo:: count(//@*)
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Boolean operations
~~~~~~~~~~~~~~~~~~
XPath expressions can also return booleans. This is not that usueful
by itself, but it becomes handy when used in predicates (that we will
cover a bit later).
For example, testing the number of paragraphs:
.. xpathdemo:: count(//p) = 2
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
.. xpathdemo:: count(//p) = 42
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Part 2: Location Paths: how to move inside the document tree
============================================================
A **Location path** is the most common XPath expression.
It is used to move in any direction from a starting point (*the context
node*) to any node(s) in the tree:
- It is a string, with a series of **“location steps”**:
``"step1 / step2 / step3 ..."``;
- It represents the **selection and filtering of nodes**, processed step by
step, **from left to right**;
- Each step is of the form ``axis :: nodetest [predicate]*``
- an *axis* (implicit or explicit),
- a *node test*,
- zero or more *predicates*.
So the examples we saw earlier are (or contain) an XPath location path:
``/html/head/title``, ``//body//p`` etc.
.. tip::
Whitespace does NOT matter in XPath.
(Except for ``“//”`` and ``“..”``;
``“/ /”`` and ``“. .”`` are syntax errors.)
For example, the following three expressions produce the same result:
.. code:: pycon
>>> doc.xpath('/html/head/title')
[(1) '<title>This is a title</title>']
.. code:: pycon
>>> doc.xpath('/ html / head /title')
[(1) '<title>This is a title</title>']
.. code:: pycon
>>> doc.xpath('''
... /html
... /head
... /title''')
[(1) '<title>This is a title</title>']
So **dont be afraid to indent your XPath expressions to improve
readability.**
Relative vs. absolute paths
---------------------------
Location paths can be relative or absolute:
- ``"step1/step2/step3"`` is relative
- ``"/step1/step2/step3"`` is absolute
In other words, an absolute path is a relative path starting with "/" (forward slash).
Absolute paths are relative to the root node.
.. tip::
Use relative paths whenever possible. This prevents unexpected
selection of duplicate nodes in loop iterations.
For example, in our sample document, only one ``<div>`` contains
paragraphs. Looping on each ``<div>`` and using the absolute location
path ``//p`` will produce the same result for each iteration: returning
ALL paragraphs in the document everytime.
.. code:: pycon
>>> for div in doc.xpath('//body//div'):
... print(div.xpath('//p'))
[(1) '<p>This is a paragraph.</p>'
(2) '<p>Is this <a href="page2.html">a link</a>?</p>']
[(1) '<p>This is a paragraph.</p>'
(2) '<p>Is this <a href="page2.html">a link</a>?</p>']
[(1) '<p>This is a paragraph.</p>'
(2) '<p>Is this <a href="page2.html">a link</a>?</p>']
Compare this with using the relative ``'p'`` or ``'./p'`` expression
that will only look at children ``<p>`` under each ``<div>``, and only
one of those ``<div>`` will show having paragraphs as shown below:
.. code:: pycon
>>> for div in doc.xpath('//body//div'):
... print(div.xpath('p'))
[]
[(1) '<p>This is a paragraph.</p>'
(2) '<p>Is this <a href="page2.html">a link</a>?</p>']
[]
.. code:: pycon
>>> for div in doc.xpath('//body//div'):
... print(div.xpath('./p'))
[]
[(1) '<p>This is a paragraph.</p>'
(2) '<p>Is this <a href="page2.html">a link</a>?</p>']
[]
Abbreviated syntax vs. full syntax
----------------------------------
What weve seen earlier is in fact the “`abbreviated syntax
<https://www.w3.org/TR/xpath/#path-abbrev>`__” for XPath
expressions. The full syntax is quite verbose (but you sometimes need it):
.. list-table::
:header-rows: 1
* - Abbreviated syntax
- Full syntax
* - ``/html/head/title``
- ``/child::html /child:: head /child:: title``
* - ``//meta/@content``
- ``/descendant-or-self::node() /child::meta / attribute::content``
* - ``//div/div[@class="second"]``
- ``/descendant-or-self::node() /child::div /child::div [attribute::class = "second"]``
* - ``//div/a/text()``
- ``/descendant-or-self::node() /child::div /child::a /child::text()``
What are these ``child::``, ``descendant-or-self::`` and
``attribute::``, you may ask? They are axes.
Axes: moving around
-------------------
.. important::
Remember that each step of an XPath location path is of the form
``AXIS :: nodetest [predicate]*``.
The "axis" is the first part of each location path step. It can be
explicit, or implicit in abbreviated syntax. For example, in
``/html/head/title``, the ``child::`` axis is omitted in each step.
In this section, we'll use explicit axes as much as we can.
**Axes give the direction to go next, one location step at a time.**
- ``self`` (where you are)
- ``parent``, ``child`` (direct hop up or down the document tree)
- ``ancestor``, ``ancestor-or-self``, ``descendant``,
``descendant-or-self`` (multi-hop)
- ``following``, ``following-sibling``, ``preceding``,
``preceding-sibling`` (document order)
- ``attribute``, ``namespace`` (non-element)
Stay where you are: self
~~~~~~~~~~~~~~~~~~~~~~~~
Let's assume that we have selected the first ``<div>`` element in our
sample document, the one just under the ``<body>`` element:
.. code:: pycon
>>> first_div = doc.xpath('//body/div')[0]
>>> first_div
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
The ``self`` axis represents *the context node*, i.e. where you are
currently in the Location Path steps. (This may not sound very useful,
but we will see later when this can be handy.)
.. code:: pycon
>>> first_div.xpath('self::*')
[(1) '<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>']
If you chain ``self::`` steps, you'll stay on the same context node:
.. code:: pycon
>>> first_div.xpath('self::*/self::*/self::*')
[(1) '<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>']
``self::`` is usually seen in abbreviated form, i.e. in '.' (one dot)
which means ``self::node()``.
So you could also use:
.. code:: pycon
>>> first_div.xpath('.')
[(1) '<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>']
.. code:: pycon
>>> first_div.xpath('././.')
[(1) '<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>']
Move up or down the tree: child, descendant, parent, ancestor
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``child`` axis is for immediate children nodes of the context node.
Here, our context node ``<div>`` has two ``<div>`` children:
.. code:: pycon
>>> first_div.xpath('child::*')
[(1) '<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>'
(2) '<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>']
``child`` is in fact the default axis, hence it can be omitted (e.g. we
saw that ``/html/head/title`` is equivalent of
``/child::html/child::head/child::title``.)
The ``parent`` axis is the dual of ``child``: you go up one level in the
document tree:
.. code:: pycon
>>> first_div.xpath('parent::*')
[(1) '<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>']
There's an alias for ``parent::``: it's ``..`` (two dots, much like in a
Unix filesystem):
.. code:: pycon
>>> first_div.xpath('..')
[(1) '<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>']
Let's simplify our ASCII tree representation from earlier to only
consider element nodes:
::
# 0--(ROOT)
+-- # 1--<html>
+-- # 3--<head>
| +-- # 5--<title>
| +-- # 8--<meta>
+-- #13--<body>
+-- #15--<div>
+-- #17--<div>
| +-- #19--<p>
| +-- #22--<p>
| | +-- #24--<a>
| +-- #29--<br>
+-- #32--<div>
+-- #35--<a>
With this simplified tree representation, this is what ``self``,
``child`` and ``parent`` select:
::
# 0--(ROOT)
+-- # 1--<html>
+-- # 3--<head>
| +-- # 5--<title>
| +-- # 8--<meta>
parent::* ---------> +-- #13--<body>
|
self::* -------------> +-- #15--<div>
|
child::*----+-----------> +-- #17--<div>
| | +-- #19--<p>
| | +-- #22--<p>
| | | +-- #24--<a>
| | +-- #29--<br>
+-----------> +-- #32--<div>
+-- #35--<a>
Recursively go up or down
^^^^^^^^^^^^^^^^^^^^^^^^^
The ``descendant`` axis is similar to ``child`` but also goes deeper down
the tree, looking at children of each child, recursively:
.. code:: pycon
>>> first_div.xpath('descendant::*')
[(1) '<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>'
(2) '<p>This is a paragraph.</p>'
(3) '<p>Is this <a href="page2.html">a link</a>?</p>'
(4) '<a href="page2.html">a link</a>'
(5) '<br>'
(6) '<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>'
(7) '<a href="page3.html">other link</a>']
You might guess already what ``ancestor`` is for: it is the dual axis of
``descendant``. It goes to the parent of the context node, the parent of
this parent, the parent of the parent of this parent, etc.
.. code:: pycon
>>> first_div.xpath('ancestor::*')
[(1) '<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type">
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>'
(2) '<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>']
Special case of ``descendant-or-/ancestor-or-self`` axes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The last axes to be aware of when you need to move up or down the document
tree are ``descendant-or-self`` and ``ancestor-or-self``.
They are the same as ``descendant`` or ``ancestor`` except they also
include the context node.
.. code:: pycon
>>> first_div.xpath('./descendant-or-self::node()/text()')
[(1) '
'
(2) '
'
(3) 'This is a paragraph.'
(4) '
'
(5) 'Is this '
(6) 'a link'
(7) '?'
(8) '
'
(9) '
Apparently.
'
(10) '
'
(11) '
Nothing to add.
Except maybe this '
(12) 'other link'
(13) '.
'
(14) '
'
(15) '
']
Move "sideways": children nodes of the same parent
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If nodes can have parents, children, ancestors and descendants, they can
also have siblings (to continue the family analogy). **Siblings are
nodes that have the same parent node.**
Some siblings may come before the context node (they appear before in
the document, their order is lower), or they can come after the context
node. There are two axis for these two directions: ``preceding-sibling`` and
``following-sibling``.
Let's first select this paragraph from our sample document:
``<p>Is this <a href="page2.html">a link</a>?</p>``. It's the 2nd child
of the 1st ``<div>`` of the ``<div>`` we used above:
.. code:: python
paragraph = first_div.xpath('child::div[1]/child::p[2]')[0]
Here we started using 2 new patterns along with the axes:
- ``child::div`` vs. ``child::*``:
- ``*`` means "any element node" (this is a *node-test* that we'll cover afterwards),
- while ``child::div`` means "any child that is a ``<div>`` element".
- ``[1]`` and ``[2]``: which mean *first* and *second* in the current
step's node-set (this is a kind of *predicate* that we'll cover
afterwards also)
.. code:: pycon
>>> paragraph.xpath('preceding-sibling::*')
[(1) '<p>This is a paragraph.</p>']
.. code:: pycon
>>> paragraph.xpath('following-sibling::*')
[(1) '<br>']
Again, let's see which elements were selected in our ASCII tree
representation:
::
# 0--(ROOT)
+-- # 1--<html>
+-- # 3--<head>
| +-- # 5--<title>
| +-- # 8--<meta>
+-- #13--<body>
|
+-- #15--<div>
|
+-- #17--<div>
| |
| |
preceding-sibling::* ----------> +-- #19--<p>
| |
| |
self::* -----------------------> +-- #22--<p>
| | |
| | +-- #24--<a>
| |
| |
following-sibling::* ----------> +-- #29--<br>
|
|
+-- #32--<div>
+-- #35--<a>
Earlier we were also able to get text nodes that were siblings of these
``<p>`` 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) '<br>'
(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 ...
<html><head><title>...
.. code:: pycon
>>> paragraph.xpath('preceding::*')
[(1) '<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type">
</head>'
(2) '<title>This is a title</title>'
(3) '<meta content="text/html; charset=utf-8" http-equiv="content-type">'
(4) '<p>This is a paragraph.</p>']
.. code:: pycon
>>> paragraph.xpath('following::*')
[(1) '<br>'
(2) '<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>'
(3) '<a href="page3.html">other link</a>']
This is what these axes select in our ASCII-tree representation:
::
# 0--(ROOT)
+-- # 1--<html>
| |
+--> +-- # 3--<head>
| | |
+------> +-- # 5--<title>
| | |
+------> +-- # 8--<meta>
| |
| +-- #13--<body>
| |
| +-- #15--<div>
| |
| +-- #17--<div>
| | |
| | |
preceding::* ---+--------------> +-- #19--<p>
| |
| |
self::* -----------------------> +-- #22--<p>
| | |
| | +-- #24--<a>
| |
| |
following::* -----------+------> +-- #29--<br>
| |
| |
+--> +-- #32--<div>
| |
+------> +-- #35--<a>
.. note::
Notice that ``preceding`` does not include ancestors and ``following``
does not include descendants.
This property `is mentioned in XPath specs <https://www.w3.org/TR/xpath/#axes>`__
like this:
*"The ancestor, descendant, following, preceding and self axes
partition a document (ignoring attribute and namespace nodes): they
do not overlap and together they contain all the nodes in the
document."*
In other words::
document == self (ancestor preceding) (descendant following)
(```` denoting the "union" for node-sets.)
Attribute axis
~~~~~~~~~~~~~~
TODO.
Node tests
----------
.. important::
A "node test" is the second part of each step in a location path.
::
axis :: NODETEST [predicate]*
Node tests select node types along the step's axis.
a node test can be:
- a *name test*:
- such as ``p``, ``title`` or ``a`` for elements: ``/html/head/title``
contains 3 steps, each with a *name test* node-test
- or ``href`` or ``src`` for attributes: ``/a/@href`` selects "href"
attributes of ``<a>`` elements
- a *node type test*:
- ``node()``: any node type
- ``text()``: text nodes
- ``comment()``: comment nodes
- ``*`` (an asterisk): the meaning depends on the axis:
- an ``*`` step alone selects any element node
- an ``@*`` selects any attribute node
.. warning::
``text()`` is not a function call that converts a node to it's
text representation, it's just a test on the node type.
Compare these two expressions:
.. code:: pycon
>>> paragraph.xpath('child::text()')
[(1) 'Is this '
(2) '?']
.. code:: pycon
>>> paragraph.xpath('string(self::*)')
[(1) 'Is this a link?']
``child::text()`` selects all children nodes that are also text nodes.
The "a" string is part of the ``<a>`` inside the paragraph, so it's not selected.
It is not a direct child of the ``<p>`` 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 ``<div>`` we looked at
earlier:
.. code:: pycon
>>> doc.xpath('//body/div/div/p')
[(1) '<p>This is a paragraph.</p>'
(2) '<p>Is this <a href="page2.html">a link</a>?</p>']
Let's say that we are not interested in the two paragraphs but only
the first one. You would use ``[1]`` as predicate:
.. xpathdemo:: //body/div/div/p[1]
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
.. warning::
Positions in XPath start from 1, not 0.
If you want the last node in a node-set, you can use ``last()``:
.. xpathdemo:: //body/div/div[last()]
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
.. warning::
Because location paths work step by step, from left to right,
positional predicates are about the **position of a node in a node-set
produced by the current step**,
not about the position of the node in the document tree.
For example, ``//body//div[1]`` is NOT the first ``<div>`` under the
``<body>`` element; it will select **all** ``<div>`` that are the first
child of their parent:
.. xpathdemo:: //body//div[1]
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
This becomes more apparent when you expand the expression to its
full syntax::
/descendant-or-self::node()
/child::body
/descendant-or-self::node()
^
|
# first child of this parent
/child::div[1]
You can however select the first ``<div>`` (in document order)
in a ``<body>`` using parentheses to group nodes into a new node-set:
- first select all ``<div>`` elements -- ``//body//div``,
- then group them -- ``( //body//div )``,
- and finally select the first one -- ``( //body//div ) [1]``,
.. xpathdemo:: ( //body//div ) [1]
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Position ranges
^^^^^^^^^^^^^^^
Sometimes you need more than one node in a node-set but not all of them.
For that you can use boolean expression in your predicate in conjunction
with the ``position()`` function that returns the node's position.
Let's change our sample HTML document a bit to include a list of five items.
Say we need all but the 1st and last one.
You can use ``[position()>1 and position()<last()]``:
.. xpathdemo:: //body//div//li[position()>1 and position()<last()]
<html>
<body>
<div>
<div>
<ol>
<li>first item</li>
<li>second item</li>
<li>third item</li>
<li>fourth item</li>
<li>fifth item</li>
</ol>
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
``//body//div//li[position()>1 and position()<last()]`` correctly
selects the 2nd, 3rd and 4th items.
Location paths as predicates
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Location paths can also serve as predicates within a parent location path.
It can happen that your HTML mamrkup does not distinguish the elements
you are after with any "class" or "id" attributes, but maybe these
elements have a structural feature that you can use to identify them.
For example, a ``<table>`` element may have rows -- ``table/tr`` --
with or without link anchors in them.
Within each ``<tr>`` row, ``td/a`` selects something on some rows,
nothing for others:
.. xpathdemo:: //table/tr [ td/a ]
<html>
<body>
<div>
<div>
<table>
<tr><td>first row</td></tr>
<tr><td>second row with <a href="http://www.example.com/2">a link</a></td></tr>
<tr><td>third row</td></tr>
<tr><td>fourth row with <a href="http://www.example.com/4">another link</a></td></tr>
</table>
</div>
</div>
</body>
</html>
Boolean predicates
~~~~~~~~~~~~~~~~~~
We saw boolean predicates earlier with positional ranges. But you can
craft complex boolean filters based on any features of nodes; structural
information on children or parent nodes, text values, position, etc.
A simple example could be selecting a ``<table>`` that has a specific
number of rows, say, 5. You can simply count the number of rows:
.. xpathdemo:: //table[ count(tr)=5 ]
<html>
<body>
<div>
<div>
<table>
<tr><td>first row</td></tr>
<tr><td>second row</td></tr>
<tr><td>third row</td></tr>
</table>
</div>
<div>
<table>
<tr><td>first row</td></tr>
<tr><td>second row</td></tr>
<tr><td>third row</td></tr>
<tr><td>fourth row</td></tr>
<tr><td>fifth row</td></tr>
</table>
</div>
</div>
</body>
</html>
Special case of string value tests
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
XPath also allows comparing string values of nodes within predicates.
If you use an equality operation with a location path and a string,
each node of the location path will be converted to its string value
and then compared with the string value to match.
This may sound more obscure than it is. Say for example that you have
two tables, with different headers. You know the string value
of the header in the table you want, "The header I want."
::
<div>
<table>
<tr><th>The header I want</th></tr>
<tr><td>second row</td></tr>
<tr><td>third row</td></tr>
</table>
</div>
<div>
<table>
<tr><th>Another header I do NOT want</th></tr>
<tr><td>second row</td></tr>
<tr><td>third row</td></tr>
<tr><td>fourth row</td></tr>
<tr><td>fifth row</td></tr>
</table>
</div>
To select the different headers, you would use ``//table/tr/th``.
You want the ``<table>`` 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" ]
<html>
<body>
<div>
<div>
<table>
<tr><th>The header I want</th></tr>
<tr><td>second row</td></tr>
<tr><td>third row</td></tr>
</table>
</div>
<div>
<table>
<tr><th>Another header I do NOT want</th></tr>
<tr><td>second row</td></tr>
<tr><td>third row</td></tr>
<tr><td>fourth row</td></tr>
<tr><td>fifth row</td></tr>
</table>
</div>
</div>
</body>
</html>
This kind of predicates also works for attribute values, e.g. testing
links to some website::
//body//p [ a/@href="http://www.example.com" ]
Special trick for testing multiple node names
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In a predicate you can also test the current node of the node-set.
For example if you want to test for several wanted element names.
This is when ``self::`` axis can be helpful.
One example is testing different kind of lists, ordered or unordered:
.. xpathdemo:: //body//*[self::ul or self::ol]//li
<html>
<body>
<div>
<div>
<ol>
<li>first ordered item</li>
<li>second ordered item</li>
<li>third ordered item</li>
</ol>
</div>
<div>
<ul>
<li>first unordered item</li>
<li>second unordered item</li>
<li>third unordered item</li>
</ul>
</div>
</div>
</body>
</html>
.. note::
Here we saw that predicates can also appear in the middle of the location
path. Indeed, predicates are (a optional) part of each location step.
Nested predicates
~~~~~~~~~~~~~~~~~
We said that location paths can be used as predicate. And location paths
can have predicates. So it's possible to end up with nested predicates.
(And that's ok.)
.. code:: pycon
>>> # <------predicate --------->
>>> # <-nested predicate->
>>> doc.xpath('//div[p [a/@href="page2.html"] ]')
[(1) '<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br>
Apparently.
</div>']
In fact, the above is equivalent to ``//div[p/a/@href="page2.html"]``
with no nesting:
.. xpathdemo:: //div[p [a/@href="page2.html"] ]
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
.. xpathdemo:: //div[p/a/@href="page2.html"]
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
Order of predicates is important
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can have multiple predicates in sequence per step, each within its
``[]`` brackets, i.e. steps in the form of
``axis::nodetest[predicate#1][predicate#2][predicate#3]...``.
Predicates are processed in order, from left to right. And the output of
one predicate is fed into the next predicate filter, much like steps
produce node-sets for the next step to process.
So **the order of predicates is important.**
The following 2 location paths produce different results:
- ``//div[@class="second"][2]``: will output one ``<div>``
- ``//div[2][@class="second"]``: will select **nothing**
See for yourself:
.. xpathdemo:: //div[2][@class="second"]
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
.. xpathdemo:: //div[@class="second"][2]
<html>
<head>
<title>This is a title</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
</head>
<body>
<div>
<div>
<p>This is a paragraph.</p>
<p>Is this <a href="page2.html">a link</a>?</p>
<br />
Apparently.
</div>
<div class="second">
Nothing to add.
Except maybe this <a href="page3.html">other link</a>.
<!-- And this comment -->
</div>
</div>
</body>
</html>
The second produces nothing indeed. Why is that?
``//div[2][@class="second"]`` looks at ``div`` elements that are the 2nd
child of their parent.
``div`` means ``child::div``, and ``[2]`` will select the 2nd node in the current node-set.
In our document this happens only once.
The final predicate, ``[@class="second"]``, filters nodes that have a
"class" attribute with value "second".
This happens to be valid for that 2nd child ``div``.
On the contrary, ``//div[@class="second"][2]`` will first produce
``//div[@class="second"]``, which only produces single-node node-sets
(again, there's only one ``div`` with "class" attribute with value
"second"). So the subsequent ``[2]`` predicate will never match with
single-node node-sets (you cannot select the 2nd element of a 1-element list)
.. warning::
Beware of ``position()`` in chained predicates.
Much like ``[...][2]`` is different from ``[2][...]``, if you chain
positional predicates, remember that the position is relative to
the node-set processed by the previous predicate.
For example, we saw that ``position()>1`` would filter out the first
nodes in a node-set. Chaining ``[position()>1]`` will remove the first
node each time it's used:
.. xpathdemo:: //ol/li[position()>1][position()>1][position()>1]
<html>
<body>
<div>
<ol>
<li>first item</li>
<li>second item</li>
<li>third item</li>
<li>fourth item</li>
<li>fifth item</li>
</ol>
</div>
</body>
</html>
String functions
----------------
TODO
Part 3: Use-cases for web scraping
==================================
TODO
Text extraction
---------------
TODO
Attributes extraction
---------------------
TODO
Attribute names extractions
~~~~~~~~~~~~~~~~~~~~~~~~~~~
TODO
CSS Selectors
-------------
TODO
Loop on elements (table rows, lists)
------------------------------------
TODO
Element boundaries & XPath buckets (advanced)
---------------------------------------------
TODO
EXSLT extensions
----------------
TODO
Summary of tips
===============
.. tip::
- Use relative XPath expressions whenever possible
- Know your axes!
- Don't forget that XPath has ``string()`` and ``normalize-space()``
functions
- **text() is a node test**, not a function call
- CSS selectors are very handy, easier to maintain, but also less
powerful than XPath