XPath 解析测试

把 XML 或 HTML 粘进来,写一条 XPath 看看它到底选中了哪些节点、文本和属性是什么。命中的第一条会连同它的序列化片段一起显示。

解析和求值都用浏览器自带的 DOMParser 和 XPath 1.0 引擎,不上传内容,也不请求任何接口。切到 HTML 模式时浏览器会按 HTML5 规则补全结构并把标签名转成小写。

使用说明

XPath 把文档当成一棵节点树,表达式沿着这棵树往里走。斜杠表示从根节点出发,双斜杠表示在任意深度去找后代节点,所以 //title 是「不管藏在哪一层,把所有 title 都给我」;写成 /catalog/book/title 则要求路径一级不差。方括号是谓词,用来筛这一层里满足条件的节点,既可以是下标,也可以是返回真假的表达式,这就是 //a[@class="btn-ghost"] 和 //div[2] 能写在同一个位置的原因。

最容易踩的坑是命名空间。XML 里的默认命名空间不算「没有命名空间」,它只是没写前缀而已;而 XPath 1.0 规定,表达式里不带前缀的名字只匹配没有命名空间的节点。于是给根元素写了 xmlns 的 RSS、SVG、DOCX 文档,用 //item 一条都选不中,看起来像浏览器的 bug,其实是规范如此。绕开的办法是 local-name()://*[local-name() = "item"] 只比对元素的本地名,忽略命名空间。结果区在命中为零时会检测根元素的命名空间并给出这句提示。

属性也是节点,只是类型不同。//a/@href 返回的不是 a 元素,而是一组属性节点,所以文本那一列是属性的值,序列化那一列写成 href="..."。属性节点不能往下继续选子节点,要取它的值直接用 string(@href)。相反,//title/text() 返回的是文本节点,它比元素少一层包装——用 //title 时文本内容是把所有后代文本拼起来的,用 //title/text() 拿到的才是那个最直接的内容节点。

关于解析模式:XML 用严格解析,标签没闭合、属性没引号、有多个根节点都会失败。失败时 DOMParser 不抛异常,而是返回一棵根节点为 parsererror 的文档,页面去读它的文本,把解析器给的行列位置和出错行的源码一起显示出来,这个位置是解析器第一次无法继续的地方,不一定是错误的起点。HTML 用宽松解析,几乎不会失败,代价是浏览器会按 HTML5 规则补全缺失的 html、head、body,并把标签名转成小写,所以 HTML 模式下 //DIV 匹配不到,要先改成小写。

这一页的引擎是浏览器内置的 XPath 1.0,没有 2.0 之后的函数和语法:没有序列匹配、没有 matches()、没有 for,只有 contains、starts-with、substring、normalize-space、last()、position() 这些 1.0 就能用的东西。所有计算都在本地完成,源文本不会上传,也不会修改文档。要格式化或校验 XML 用XML 格式化,要在 JSON 上做类似的结构查询用JSONPath 测试,用正则做文本层面的提取用正则测试。

常见问题

表达式语法没错,为什么一个节点都选不中?

最常见的原因是文档带了默认命名空间。XPath 1.0 里不带前缀的元素名只匹配「没有命名空间」的节点,而 RSS、SVG、Office 文档这些 XML 几乎都给根元素绑了默认命名空间,于是 //title 一条都命中不了。改用 //*[local-name() = "title"] 就能绕过前缀问题,结果区在命中为 0 时会直接把这句话提示出来。

用 HTML 模式解析后,标签名变成了小写,还多出了 html 和 body。

这是 text/html 解析器的正常行为:HTML 不区分大小写,解析时标签名统一小写,缺省的 html、head、body 也会被自动补齐。所以 //DIV 这种写法匹配不到任何东西,要写成 //div。如果只想要源码原样的结构,用 XML 模式解析(前提是源码本身是良构的 XML)。

//div[2] 取的是第二个 div 吗?

是「同一个父节点下、同为 div 的兄弟里的第二个」,不是文档里的第二个 div,也不是第 2 个子节点的位置。序号从 1 开始数,这一点和写成数组下标 0 的直觉正好相反。要按文档顺序取全部 div 再挑第二个,写成 (//div)[2],括号很关键。

//a/@href 选出来的算节点吗?

算,XPath 里的属性也是节点,只是类型是属性节点而不是元素。结果表里它会显示属性名和值,序列化片段写成 href="..." 的形式。因为属性节点没有子节点,想拿它的文本得用 string(@href) 或者在后端取出 nodeValue。

count()、string() 这类表达式为什么看不到节点列表?

因为它们返回的根本不是节点集合,而是数字、字符串或布尔值。页面会先探一次结果类型,是标量就直接把值显示出来,不再尝试去列节点。同理,//title/text() 选出来的是文本节点,类型那一列会写“文本”,文本内容就是节点的值。

XML 解析报错时给的行列号准吗?

给的是解析器第一次无法继续的位置,不一定是错误的起点。少写一个结束标签时,报错常常落在后面某个本不该出现的标签上;要往回找最近的未闭合标签。报错文本里还会带上出错那一行的源码和一个指向具体字符的尖角标记,配合一起看定位最快。

这一页支持 XPath 2.0 或 3.0 的函数吗?

不支持。页面用的是浏览器内置的 XPath 1.0 引擎,所以没有序列、没有 matches()、也没有 for 表达式,只有 contains、starts-with、substring、normalize-space 这些 1.0 函数。要测 2.0 以上的表达式得用专门的解析器或后端库。

这里的 XPath 能直接用到网页抓取里吗?

选择器的写法可以直接搬去 lxml、Scrapy 这类库,但要注意浏览器给的是「补全后的 DOM」而不是原始 HTML 文本,标签顺序和自动闭合的结果可能和抓取库不一致。另外这页只读取、不修改文档,也不发任何网络请求;要格式化或校验 XML 用 XML 格式化,要在 JSON 上做类似的事用 JSONPath 测试。

相关:XML 格式化JSONPath 测试正则测试