XPath cheat sheet
A scannable XPath reference: 26 short snippets across 12 topics, each linking back to the lesson it came from.
At a glance
| Topic | What it covers | |
|---|---|---|
| Location paths and axes | XPath addresses an XML document as a tree of nodes. An expression is evaluated against a context node, and the result | lesson |
| Predicates and functions | A predicate in square brackets keeps only the nodes for which the expression is true. Inside a predicate, the context | lesson |
| XPath in XSLT and in code | In XSLT almost every attribute is an XPath expression. select chooses nodes to process, test makes a decision, and | lesson |
| The XPath data model: nodes, order and values | An XPath expression is evaluated against a tree, not against text. The tree is built by the parser and normalised | lesson |
| Namespaces and the default namespace trap | An unprefixed name in XPath matches a node in no namespace. When the document declares a default namespace, every | lesson |
| Selecting attributes, text and special nodes | The example above is the single most common scraping bug in XPath: text()="Overview" only compares the first text node | lesson |
| XPath from the shell: xmllint and xmlstarlet | Two command-line tools turn XPath into something you can pipe, diff and run in a build script, including on namespaced | lesson |
| XPath in the browser: document.evaluate | The DOM XPath API is older than querySelector but still the only way to walk upwards and to match by text, and its | lesson |
| XPath 1.0, 2.0 and 3.1: what changed | The version you are allowed to use is decided by your engine, not your preferences, and the jump from 1.0 changes what | lesson |
| Robust XPath for scraping and testing | Every positional step is a promise that nothing will ever be inserted before it. Live pages break that promise | lesson |
| XPath performance: cost, indexes and // | Descendant search is the expensive part of XPath. Knowing which constructs force a full scan tells you where to spend | lesson |
| CSS selectors, JSONPath and jq as alternatives | XPath is not always the right tool: CSS is faster and simpler for downward-only selection, and JSON needs an entirely | lesson |
Quick snippets
Location paths and axes
The data model behind a path
<catalog>
<book id="bk-101"><title>Alpha</title><price>39.95</price></book>
<book id="bk-102"><title>Beta</title><price>12.00</price></book>
</catalog>
Axes worth knowing
/catalog/book absolute: books that are children of the root
//book/title any title element anywhere
catalog/book/@id attribute axis, abbreviated with @
//book[1] first book child of each parent
(//book)[1] the first book in the document - note the brackets
//book/ancestor::catalog the catalog that encloses each book
//title/following-sibling::price the price that comes right after a title
//book[@id = 'bk-102']/title/text() the text node of a matching title
//comment() every comment node
Node tests and multiple results
# try expressions from the command line before putting them in code
xmllint --xpath 'count(//book)' catalog.xml; echo
xmllint --xpath 'string(//book[1]/title)' catalog.xml; echo
xmllint --xpath '//book/@id' catalog.xml; echoFull lesson: Location paths and axes →
Predicates and functions
Predicates filter a node-set
//book[price > 20] value test
//book[@id] has the attribute
//book[not(@id)] lacks the attribute
//book[position() = 1] same as //book[1], per parent
//book[last()] the last book child of each parent
//book[position() <= 3] the first three
//book[title = 'Alpha'] exact text comparison
//book[contains(title, 'lph')] substring test
//book[starts-with(@id, 'bk-10')] prefix test
//book[price > 20 and @available='true'] boolean combination
The functions you will actually use
count(//book) how many books
sum(//book/price) total price as a number
round(sum(//book/price) * 100) / 100 two-decimal total
normalize-space(//book[1]/title) trim and collapse spaces
concat(//book[1]/title, ' - ', //book[1]/price) join into one string
substring-before(//book[1]/title, ' ') text before the first space
translate(//book[1]/title, 'abc', 'ABC') character-by-character map
string-length(//book[1]/title) > 4 boolean resultFull lesson: Predicates and functions →
XPath in XSLT and in code
In Python and Java
from lxml import etree
tree = etree.parse("catalog.xml")
ns = {"s": "http://example.com/shop"}
books = tree.xpath("//s:book[s:price > 20]", namespaces=ns)
for b in books:
print(b.get("id"), b.xpath("string(s:title)", namespaces=ns))
# lxml returns smart strings; cast explicitly when you need plain values
print(float(books[0].xpath("number(s:price)", namespaces=ns)))
In Python and Java
import javax.xml.xpath.*;
import org.w3c.dom.*;
XPath xp = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xp.evaluate("//book[price > 20]",
document, XPathConstants.NODESET);
System.out.println(xp.evaluate("count(//book)", document));
// namespaces must be resolved through a context, as prefixes are local
xp.setNamespaceContext(new SimpleNamespaceContext("s", "http://example.com/shop"));
In the browser and on the command line
xmllint --xpath '//book[price > 20]/title/text()' catalog.xml; echo
xmllint --xpath 'string(sum(//book/price))' catalog.xml; echo
# xmllint cannot bind a prefix directly; use local-name() or the interactive shell
xmllint --xpath '//*[local-name()="book"]/*[local-name()="title"]/text()' catalog.xml; echo
printf 'setns s=http://example.com/shop\nxpath //s:book/s:title\nexit\n' | xmllint --shell catalog.xmlFull lesson: XPath in XSLT and in code →
The XPath data model: nodes, order and values
Document order and the context node
# Every expression is evaluated with respect to a context node.
# Inside a predicate, position() counts siblings from that context.
xmllint --xpath '//ul/li[position() = 1]' page.html # first li of each ul
xmllint --xpath '(//ul/li)[1]' page.html # first li in the whole doc
xmllint --xpath '//ul/li[last()]' page.html # last li of each ul
xmllint --xpath '//ul/li[position() < 3]' page.html # first two of each ul
Values, conversions and comparison
from lxml import etree
doc = etree.fromstring("<r><n>10</n><n>9</n><s>abc</s></r>")
# string() collapses an element to text; number() converts it
print(doc.xpath("number(//n[1])")) # 10.0
print(doc.xpath("//n[text() > '9']")) # text comparison, not numeric
# Compare with a number to force numeric comparison: only the 10 survives
print(doc.xpath("//n[. > 9]")) # [<Element n>]
print(doc.xpath("count(//n)")) # 2.0
print(doc.xpath("boolean(//missing)")) # FalseFull lesson: The XPath data model: nodes, order and values →
Namespaces and the default namespace trap
The trap
<catalog xmlns="urn:example:catalog" xmlns:dc="http://purl.org/dc/elements/1.1/">
<book><dc:title>Pragmatic XPath</dc:title></book>
</catalog>
The trap
xmllint --xpath 'count(//book)' catalog.xml
# XPath error: no result / returns nothing useful
xmllint --xpath 'count(//*[local-name()="book"])' catalog.xml
# 1 -- this works, because it ignores the namespace URI
Binding a prefix in each environment
from lxml import etree
ns = {"c": "urn:example:catalog", "d": "http://purl.org/dc/elements/1.1/"}
doc = etree.parse("catalog.xml")
print(doc.xpath("//c:book/d:title/text()", namespaces=ns)) # ['Pragmatic XPath']Full lesson: Namespaces and the default namespace trap →
Selecting attributes, text and special nodes
Text, comments and processing instructions
from lxml import etree, html
doc = html.fromstring("<div><h2>Over<span>view</span></h2></div>")
# text() sees the heading and its span as separate text nodes
print(doc.xpath("//h2/text()")) # ['Over']
print(doc.xpath("//h2//text()")) # ['Over', 'view']
print(doc.xpath("string(//h2)")) # 'Overview'
print(doc.xpath("//h2[text() = 'Overview']")) # [] -- the trap
print(doc.xpath("//h2[normalize-space(.) = 'Overview']")) # [<Element h2>]
Whitespace-only text nodes
# Indented markup produces text nodes that look empty
xmllint --xpath 'count(//ul/li/text())' page.html
# Inspect what is actually there
xmllint --xpath '//ul/li/text()' page.html
# XML parsers can drop them for you
xmllint --noblanks --xpath 'count(//ul/li/text())' page.htmlFull lesson: Selecting attributes, text and special nodes →
XPath from the shell: xmllint and xmlstarlet
xmlstarlet
# select: print values, one per line (the default template for -t is -v)
xmlstarlet sel -t -m '//item' -v 'title' -n feed.xml
# select with namespace binding
xmlstarlet sel -N c=urn:example:catalog -t -v 'count(//c:book)' catalog.xml
# CSV out, useful in a report script
xmlstarlet sel -t -m '//item' \
-v 'concat(title, ",", pubDate)' -n feed.xml
# edit in place: rename an element and set an attribute
xmlstarlet ed -L -r '//legacyName' newName -a '//book[1]' -t attr -n 'lang' -v 'en' catalog.xmlFull lesson: XPath from the shell: xmllint and xmlstarlet →
XPath in the browser: document.evaluate
Namespaces and the comparison with CSS
// XPath on an XML document needs a resolver function
const NS = { svg: "http://www.w3.org/2000/svg", xlink: "http://www.w3.org/1999/xlink" };
const resolver = (prefix) => NS[prefix] || null;
const svg = await fetch("/diagram.svg").then(r => r.text());
const xml = new DOMParser().parseFromString(svg, "image/svg+xml");
const first = xml.evaluate("//svg:rect[1]", xml, resolver,
XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
first.setAttribute("fill", "tomato");
Namespaces and the comparison with CSS
// Selecting the table row that contains a label, then walking up
const row = document.evaluate(
"//td[normalize-space(.)='Total']/ancestor::tr[1]",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
).singleNodeValue;
console.log(row.querySelector("td:last-child").textContent);Full lesson: XPath in the browser: document.evaluate →
XPath 1.0, 2.0 and 3.1: what changed
What XPath 1.0 cannot express
<!-- Locating a book whose price is a two-decimal number: not possible in 1.0,
where every value is a string or a double and a trailing zero is lost -->
Sequences, functions and types
<!-- XPath 2.0 and later -->
for $b in //book[price > 20]
return string-join(($b/title, $b/author), ' by ')
if (count(//book) gt 10) then 'long' else 'short'
//book[matches(title, '^The\s', 'i')]
//book[upper-case(@lang) eq 'EN']
distinct-values(//book/@lang)
xs:date(//book[1]/published) lt current-date()
//book[price castable as xs:decimal]
Sequences, functions and types
# Saxon gives you XPath 3.1 on the command line for a one-off query
java -cp saxon-he.jar net.sf.saxon.Query -qs:"count(//book[matches(title,'^The')])" catalog.xml
# xmllint and xmlstarlet remain XPath 1.0 and silently differ on edge casesFull lesson: XPath 1.0, 2.0 and 3.1: what changed →
Robust XPath for scraping and testing
Why structural paths break
# A path generated by a browser devtools "Copy XPath"
/html/body/div[3]/div[2]/table/tbody/tr[4]/td[2]
# What the same thing looks like after one wrapper div is added
/div[1]/div[3]/div[2]/table/tbody/tr[4]/td[2] # now brokenFull lesson: Robust XPath for scraping and testing →
XPath performance: cost, indexes and //
What actually costs time
# Measure rather than guess
time xmllint --xpath 'count(//item)' big.xml
# Narrow the context node so // has less to scan
time xmllint --xpath 'count(//feed/item)' big.xml
# A path that never leaves the top level is the cheapest form
time xmllint --xpath 'count(/*/*/item)' big.xml
Making a query fast
<!-- Keys give you a hash index you control -->
<xsl:key name="byId" match="item" use="@id"/>
<xsl:template match="/">
<!-- one key call per lookup, instead of scanning every item -->
<xsl:value-of select="key('byId', '42')/title"/>
</xsl:template>Full lesson: XPath performance: cost, indexes and // →
CSS selectors, JSONPath and jq as alternatives
CSS versus XPath
from lxml import html
doc = html.fromstring("<ul><li class='on'>a</li><li>b</li></ul>")
print(doc.xpath("//li[@class='on']/text()")) # XPath
print(doc.cssselect("li.on")[0].text) # same result, shorter
# The one thing CSS cannot do here
print(doc.xpath("//li[.='b']/preceding-sibling::li/text()")) # ['a']
JSONPath and jq
import json
from jsonpath_ng.ext import parse
data = json.load(open("books.json"))
expr = parse("$.items[?(@.price > 20)].title")
print([m.value for m in expr.find(data)])Full lesson: CSS selectors, JSONPath and jq as alternatives →
FAQ
Is this XPath cheat sheet free to use?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
XML XSLT SOAP RESTful APIs RSS & Atom
Last refreshed 2026-09-27.