XPath 是什么?XML 数据查询从入门到实战
XPath 是 XML 世界里的"查询语言"——你有一堆结构化的 XML 数据,想从中精确提取某个节点的值、过滤满足条件的元素、或者统计某个属性出现的次数,XPath 就是干这个的。几乎所有需要处理 XML 的场景都会用到它:Java 解析配置文件、Python 爬虫提取网页数据、XSLT 转换文档格式,底层都依赖 XPath 定位节点。
如果把 XML 文档比作一栋大楼,那 XPath 就是楼里的导航系统——告诉你"3 楼东侧第二个房间"在哪,而不是让你挨个门去找。
XPath 把 XML 看成一棵树
拿到一份 XML 文档后,XPath 要做的第一件事是把它当成一棵"节点树"。每种 XML 组成部分对应一种节点类型:
- 元素节点:XML 中的标签,比如
<book> - 属性节点:标签里的属性,比如
category="web" - 文本节点:标签之间的文字内容
- 文档节点:整份 XML 的根,也叫根节点
剩下的命名空间节点、处理指令节点、注释节点用得少,知道就行。关键理解一点:XPath 的所有查询操作,本质上都是在"在这棵树上找路"。
路径表达式:XPath 的基本语法
拿一份常见的 XML 举例:
xml<bookstore> <book category="web"> <title lang="en">XML Guide</title> <author>John Doe</author> <price>39.95</price> </book> <book category="database"> <title lang="en">SQL Basics</title> <author>Jane Smith</author> <price>29.99</price> </book> </bookstore>
绝对路径和相对路径
xpath/bookstore → 根元素 bookstore /bookstore/book → bookstore 下所有 book 子元素 //book → 文档中任意位置的 book 元素 bookstore//book → bookstore 后代中所有 book 元素
/ 开头是绝对路径,从根节点出发;// 表示"不管在哪一层,只要匹配就选出来",类似文件系统的递归搜索。
一个性能细节://book 看起来方便,但它会遍历整棵树,文档大的时候性能开销明显。如果知道节点的大致位置,用 /bookstore/book 这种更精确的路径更快。
谓词:加条件过滤
谓词写在方括号 [] 里,用来筛选满足特定条件的节点。可以把谓词理解为 SQL 的 WHERE 子句——都是给查询加过滤条件:
xpath/bookstore/book[1] → 第一个 book /bookstore/book[last()] → 最后一个 book /bookstore/book[position()<3] → 前两个 book //book[@category='web'] → category 属性为 web 的 book //book[price>35] → price 大于 35 的 book
实际开发中大部分 XPath 查询都离不开谓词。一个实用技巧:多个条件可以用 and/or 组合,比如 //book[@category='web' and price<40]。
通配符
xpath* → 任何元素节点 @* → 任何属性节点 node() → 任何类型的节点
用得不多,但在写通用查询时很方便,比如 //book/* 取出 book 下所有子元素,不用逐个写子元素名称。
轴:指定搜索方向
轴定义了"从当前节点往哪个方向找"。默认轴是 child,所以 /bookstore/book 其实是 /child::bookstore/child::book 的简写。
常用的轴:
xpathparent → 父节点(简写 ..) child → 所有子节点(默认,可省略) descendant → 所有后代节点 ancestor → 所有祖先节点 following-sibling → 之后的同级节点 preceding-sibling → 之前的同级节点 self → 自身(简写 .)
完整语法是 轴名::节点测试,比如 ancestor::book 表示找所有叫 book 的祖先节点。日常开发中 parent、child、descendant、following-sibling 这几个占了 90% 的使用场景。
内置函数:让查询更灵活
XPath 内置了一批函数,可以直接在谓词和表达式中调用。
字符串函数——出场率最高
xpathcontains(title, 'XML') → title 包含 "XML" starts-with(@lang, 'en') → lang 属性以 "en" 开头 substring(price, 1, 4) → 截取 price 的前 4 个字符 normalize-space(text) → 去掉多余空白 string-length(title) → 标题长度
contains 是日常开发中出场率最高的函数,做模糊匹配全靠它。一个常见场景:在配置文件里找所有包含特定关键字的节点。
聚合和数值函数
xpathcount(//book) → 统计 book 元素数量 sum(//book/price) → 所有 price 求和 floor(3.7) → 3(向下取整) ceiling(3.2) → 4(向上取整) round(3.5) → 4(四舍五入)
布尔函数
xpathnot(@category='web') → category 不是 web boolean(//book) → 是否存在 book 元素(判空用)
boolean() 配合 not() 可以判断"某个节点是否存在",在做数据校验时很有用。
在各语言中实际使用 XPath
Java
javaXPathFactory factory = XPathFactory.newInstance(); XPath xpath = factory.newXPath(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document doc = dbf.newDocumentBuilder().parse(new File("books.xml")); // 查询单个值 String title = xpath.evaluate("//book[@category='web']/title/text()", doc); // 查询节点列表 NodeList books = (NodeList) xpath.evaluate("//book", doc, XPathConstants.NODESET); for (int i = 0; i < books.getLength(); i++) { Element book = (Element) books.item(i); System.out.println(book.getAttribute("category")); }
踩坑提醒:Java 默认的 XPath 实现是串行执行的,大文件查询会很慢。如果性能敏感,考虑换用 Saxon-HE 等第三方实现。另外,DocumentBuilderFactory.newInstance() 默认不启用命名空间支持,需要 dbf.setNamespaceAware(true) 才能用命名空间相关的 XPath 查询。
Python(lxml 库)
pythonfrom lxml import etree tree = etree.parse("books.xml") # 提取文本 titles = tree.xpath("//book[@category='web']/title/text()") # 提取属性 categories = tree.xpath("//book/@category") # 用函数做统计 total = sum(tree.xpath("//book/price/text()"))
Python 爬虫中 lxml + XPath 是黄金组合,比 BeautifulSoup 的 CSS 选择器更灵活——尤其是处理不规则的 HTML 结构,XPath 的 contains() 和轴查询能解决很多 CSS 选择器搞不定的问题。
JavaScript(浏览器环境)
javascriptconst parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "text/xml"); const result = xmlDoc.evaluate( "//book[@category='web']/title", xmlDoc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); for (let i = 0; i < result.snapshotLength; i++) { console.log(result.snapshotItem(i).textContent); }
浏览器环境下的 document.evaluate 可以直接对 HTML DOM 执行 XPath 查询,不限于 XML 文档。做自动化测试或油猴脚本时很实用。
XPath 和 XQuery 的关系
XQuery 基于 XPath 构建,但能力更强:
- XPath:定位和选择节点,是"找东西"的工具
- XQuery:不仅能找,还能构造新的 XML 结构、做 FLWOR 查询(类似 SQL 的 for-let-where-order-return)
如果只是从 XML 里提取数据,XPath 够用。如果需要查询后重新组织输出格式,才需要 XQuery。
常见坑和最佳实践
命名空间陷阱——新手第一大坑
XML 声明了命名空间后,直接用 /root/child 可能查不到节点。比如:
xml<root xmlns="http://example.com/ns"> <child>hello</child> </root>
此时 //child 返回空,因为 child 已经属于一个命名空间了。必须在代码中注册命名空间前缀,然后用前缀查询:
python# Python lxml 示例 tree.xpath("//ns:child/text()", namespaces={"ns": "http://example.com/ns"})
这是 XPath 新手最常遇到的"明明节点在,就是查不到"的问题。
// 的性能问题
前面说过,// 会遍历全树。几百 KB 的文档无所谓,几十 MB 的文档就会明显卡顿。能写精确路径就别用 //,尤其是循环里反复执行 XPath 的时候。
文本节点的空白陷阱
XML 中的换行和缩进会被解析为文本节点,//text() 可能返回一大堆空白字符串。用 normalize-space() 过滤,或者直接用 /text() 取特定层级的文本。
XPath 1.0 vs 2.0/3.0
大多数语言内置的是 XPath 1.0,不支持 for 循环、条件表达式(if-then-else)、正则匹配等 2.0+ 特性。需要高级功能时:
- Java → 用 Saxon 替换默认实现
- Python → lxml 的扩展函数,或换用
xml.etree.ElementTree的有限 XPath 支持 - C# → .NET 3.5+ 支持 XPath 1.0,更高级需要第三方库
特殊字符转义
路径中包含单引号或双引号时,需要用 concat() 拼接,比如 //book[title=concat("He said '", "'", "s book")]。XPath 1.0 没有原生的转义语法,这是它的一个设计缺陷。XPath 2.0+ 支持双引号内转义单引号,但 1.0 环境下只能用 concat() 绕路。
查询结果缓存
如果同一条 XPath 会被反复执行(比如在循环里),考虑编译一次、重复执行:
java// Java 编译 XPath 表达式 XPathExpression expr = xpath.compile("//book[@category='web']"); NodeList result = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
编译一次比每次都 evaluate() 快得多,尤其是复杂表达式。