XPath(XML Path Language)是一种在 XML 文档中查找信息的语言,它基于 XML 的树状结构,通过路径表达式在节点之间导航。无论是 XSLT 转换、XML Schema 校验,还是自动化测试中的元素定位,XPath 都扮演着关键角色。理解 XPath 语法,首先需要明确 XML 文档中的节点类型:元素节点、属性节点、文本节点、命名空间节点、处理指令节点和注释节点。本文将以一个典型的书店 XML 文档为例,系统介绍 XPath 的核心语法和实用技巧。

一、XPath 节点模型与路径表达式基础
XPath 将 XML 文档视为一棵节点树,树的根是文档节点(document node),它并不是最外层的元素,而是整个文档的抽象入口。根节点下包含一个元素节点,例如 <bookstore>,这个元素是所有其他元素的祖先。每个元素节点可以拥有属性节点和文本节点作为子节点。需要注意的是,属性节点和命名空间节点并不被视为其父元素的子节点,但可以通过特殊轴来访问。
路径表达式是 XPath 的核心,分为绝对路径和相对路径。绝对路径以正斜杠开头,从根节点开始匹配,例如 /bookstore/book 表示从根节点出发,先找到 bookstore 元素,再选择其下所有 book 子元素。相对路径不以斜杠开头,它相对于当前上下文节点进行定位,例如 book/title 表示选择当前节点下所有 book 子元素的 title 子元素。路径中的每一步都由轴(axis)、节点测试(node test)和谓词(predicate)三部分组成,形式为 轴::节点测试[谓词]。如果省略轴,则默认使用 child 轴。
看一个简单的 XML 文档示例:
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book category="编程">
<title lang="zh">深入理解XPath</title>
<author>张三</author>
<price>59.00</price>
</book>
<book category="小说">
<title lang="zh">示例故事</title>
<author>李四</author>
<price>29.90</price>
</book>
</bookstore>
在这个文档中,根节点是文档节点,<bookstore> 是文档元素。通过 /bookstore/book 可以选取两个 book 元素。如果需要提取所有书名,可以使用 //title/text(),其中 // 是 /descendant-or-self::node()/ 的简写,表示从任意位置向下查找所有匹配的节点。
二、轴、节点测试与谓词的高级用法
轴定义了节点之间的相对关系,XPath 提供了 13 个轴,其中最常用的包括 child、descendant、parent、ancestor、following-sibling、preceding-sibling、attribute 和 self。理解轴的方向对于编写精准表达式至关重要。例如,child::book 等价于 book,只选择当前节点的直接子元素;而 descendant::book 会选择所有后代中的 book,无论层级多深。反向轴如 parent::* 可以选取当前节点的父元素。
节点测试用于筛选轴上的节点,可以是具体的名称,如 book,也可以是通配符 *,表示任意元素。还可以使用命名空间前缀限定,如 ns:book。对于文本节点和注释节点,则使用 text() 和 comment() 这样的节点类型测试。谓词则放在方括号中,用于对节点集合进行进一步过滤,例如 book[1] 选择第一个 book,book[last()] 选择最后一个,book[price>30] 选择价格大于 30 的书。注意在 XML 文档中直接写谓词时,大于和小于符号需要转义为 > 和 <,但在 XPath 表达式字符串中通常直接使用符号。
谓词内部可以使用 XPath 函数和运算符来构造复杂条件。比如 //book[@category='编程'] 会选择 category 属性等于“编程”的 book 元素。这里的 @ 是 attribute:: 轴的简写,表示选择属性节点。如果要同时满足多个条件,可以使用 and 和 or 连接,例如 //book[@category='小说' and price<30]。在 XPath 2.0 及以上版本中,还可以使用 for、if 等表达式,但主流应用中 XPath 1.0 仍占据很大比例。
三、常用函数与实战查询示例
XPath 内置了大量函数,大致分为节点集函数、字符串函数、数值函数和布尔函数。节点集函数中,position() 返回当前节点在上下文中的位置,常与谓词配合使用,例如 //book[position()=2] 选取第二个 book。count() 可以统计节点数量,比如 count(//book) 返回书店中书的总数。id() 则根据 DTD 中定义的 ID 属性快速定位元素。
字符串函数在 XML 数据处理中非常实用。contains() 判断一个字符串是否包含另一个字符串,例如 //title[contains(., 'XPath')] 会选择标题中包含“XPath”的 title 元素。starts-with() 和 ends-with() 分别用于匹配前缀和后缀(注意 ends-with() 在 XPath 2.0 中才提供)。substring() 可以截取字符串,string-length() 返回字符串长度。使用这些函数能够实现模糊匹配和文本提取,比单纯的节点名称匹配灵活得多。
下面展示一个在 Python 中使用 lxml 库执行 XPath 查询的示例,帮助读者理解如何将 XPath 与编程语言结合:
from lxml import etree
xml_data = '''<bookstore>
<book category="编程">
<title lang="zh">深入理解XPath</title>
<author>张三</author>
<price>59.00</price>
</book>
<book category="小说">
<title lang="zh">示例故事</title>
<author>李四</author>
<price>29.90</price>
</book>
</bookstore>'''
root = etree.fromstring(xml_data)
# 查询所有价格大于30的书名
titles = root.xpath('//book[price>30]/title/text()')
print(titles) # 输出 ['深入理解XPath']
# 查询第二个book的作者
author = root.xpath('//book[2]/author/text()')
print(author) # 输出 ['李四']
在上述代码中,root.xpath() 方法接收一个 XPath 表达式字符串并返回结果列表。由于 XPath 表达式写在 Python 字符串中,大于号不需要转义,但如果直接写在 XML 或某些上下文中,则需要使用 >。这一点在跨平台使用时容易出错,需要特别注意。
四、常见误区与书写建议
许多初学者在写 XPath 时习惯使用绝对路径,例如 /bookstore/book/title,这种方式虽然直观,但一旦 XML 结构发生轻微调整,比如增加了一层 catalog 包装,表达式就会失效。相对路径和双斜杠 // 提供了更好的容错性,但也要避免滥用 // 导致查询范围过大、性能下降。合理的做法是尽量限定父级路径,只在必要时使用后代轴。
另一个常见误区是混淆属性节点和元素节点。属性值要用 @ 加属性名来获取,比如 //book/@category 返回的是属性值集合,而 //book/category 返回的是子元素节点。在 XPath 1.0 中,属性节点和文本节点的数据类型都按字符串处理,如果需要进行数值比较,最好用 number() 函数显式转换,避免隐式转换带来的意外结果。
为了提高 XPath 的可读性和可维护性,建议遵循以下原则:优先使用明确的轴而不是默认轴,例如用 child::book 代替 book 可以在复杂表达式中清晰表达意图;谓词尽量简洁,多个条件可以拆分成多个表达式分别调试;对于频繁使用的路径,可以定义变量或常量。编写完成后,可以使用在线的 XPath 测试工具快速验证结果,确认选取的节点集合符合预期。