在XML或HTML文档的解析场景中,经常需要挑出那些“内部含有某个具体子元素”的节点。XPath作为专门在树形结构中定位节点的查询语言,天然支持这类包含关系的判断,而不必先把所有节点取出来再用程序循环过滤。

使用子元素名称作为谓词条件
最直观的方式是在节点测试之后加上方括号,里面直接写子元素的名称。这相当于告诉处理器:只保留那些至少有一个该子元素的节点。这种写法依赖默认的child轴,也就是当前节点的直接子级。
例如下面这段XML数据,我们想找出所有包含<price>子元素的<product>节点:
<catalog>
<product id="1">
<name>键盘</name>
<price>99</price>
</product>
<product id="2">
<name>鼠标垫</name>
</product>
</catalog>
对应的XPath表达式为:
//product[price]
这个表达式会选中id为1的product,而忽略没有price的product。它的本质是判断child::price轴至少返回一个节点。如果子元素嵌套较深,可以使用相对路径:
//product[details/price]
这种语法的优点是简洁、执行效率高,因为XPath引擎会在底层用索引或流式判断完成过滤。缺点是只能判断“存在”,不能直接表达“仅有一个”或“恰好两个”这类数量约束,那种情况需要配合count()函数。
结合属性与文本做更精确的包含判断
实际业务中,往往不是只要有子元素就行,还需要子元素满足某些特征。XPath允许在谓词内部继续写条件,形成嵌套筛选。
假设我们要选中的是包含<user>且其中<role>文本为admin的节点,XML如下:
<system>
<group>
<user><role>admin</role></user>
</group>
<group>
<user><role>guest</role></user>
</group>
</system>
表达式可以写成:
//group[user/role='admin']
这里user/role='admin'会在每个group节点上下文中求值。如果子元素带属性,也可以这样:
//item[meta[@lang='en']]
这表示选中所有至少带一个lang属性为en的meta子元素的item。要注意属性判断必须用@符号,且字符串比较区分大小写。在HTML抓取时,这类写法能快速定位到含有特定标注块的容器节点,省去后续用代码判空。
使用descendant轴处理任意层级子元素
前面写的都是直接子元素。如果只确定“下面某处有这个子元素”,但不确定在第几层,就要用descendant轴或者缩写//。
比如一份不规则的文档,warning可能出现在section内部任意深度,我们想选中所有祖先section:
<root>
<section>
<block>
<warning>内存不足</warning>
</block>
</section>
<section>
<note>正常</note>
</section>
</root>
用子元素谓词只能看直接孩子,所以应改为:
//section[.//warning]
其中.//warning等价于descendant::warning,表示从当前section出发向下任意层寻找。这个点号代表当前节点上下文,避免写成//section//warning那样选成warning自己。此写法在日志结构化抽取时非常实用,能整段提取出存在异常标记的模块。
不过要注意,descendant搜索范围大,在超长文档上性能低于限定层级的写法。如果业务能确定深度,尽量写明确路径如section/block/warning来减少回溯。
常见误区与调试建议
初学者容易把包含子元素和匹配子元素本身搞混。例如//price[product]是想选price,但price根本不是product的父级,结果永远为空。应当反过来写//product[price]。
另一个误区是在HTML解析库里用Browser XPath时,标签名大小写敏感规则不同。HTML文档经某些解析器转成XML会保留小写,而原始XPath若写大写就会漏选。建议统一用小写并先用单个节点测试再扩到集合。
当表达式变复杂,可以分步骤验证:先写//product看总数,再加[price]看过滤后数量,逐步叠加条件。这样能快速定位是哪一层谓词写错,而不是面对空结果盲目猜测。
最后提醒,XPath 1.0中谓词里的表达式返回节点集即视为真,所以不存在的子元素返回空集就是假,不需要显式写count()>0,除非要限制具体数量。