XPath作为XML和HTML文档查询的主流语言,除了沿着文档树向下查找子节点,还提供了沿树向上回溯的能力。parent::轴就是其中最常用的一个,它可以选择当前节点的直接父节点。很多刚接触XPath的人习惯只用//div/a这类自上而下的路径,一旦遇到需要从子元素反推外层容器的场景就束手无策。这篇文章系统讲解parent::轴的语法、用法和常见坑点,并结合实例演示它与其他轴的配合方式。

一、先理解XPath的节点关系模型
XPath把文档看成一棵节点树,每个节点在树中有明确的位置关系:children(子节点)、parent(父节点)、sibling(同级节点)、ancestor(祖先节点)、descendant(后代节点)。parent::轴所指的就是当前节点的直接上一层节点,注意不是所有祖先,而是紧贴着的那一个。任何一个节点的parent轴结果集最多只包含一个节点,这一点和ancestor轴返回一串节点完全不同。
XPath表达式的完整语法是“定位步骤”的串联,每个定位步骤由三部分组成:轴、节点测试、谓词。写法为轴名称::节点测试[谓词]。比如parent::div[@class='wrap']中,parent::是轴,div是节点测试,[@class='wrap']是谓词。理解这个结构后,你就能灵活拼接各种复杂表达式。
另外XPath提供了一个等价缩写:..表示parent::node()。也就是说//span/..和//span/parent::node()结果一致。缩写简洁但语义不够明确,在实际项目中显式写出parent::往往可读性更好,尤其当团队协作维护选择器时。
二、parent::轴的基本用法示例
假设有这样一段HTML结构,我们以此为例演示各种写法:
<div class="item">
<h3>商品标题</h3>
<span class="price">99元</span>
</div>如果想选中价格所在的整个商品块,可以写//span[@class='price']/parent::div。这个表达式的执行逻辑是:先找到所有class为price的span,然后取每个span的直接父节点,再用div做节点测试过滤,只保留div类型的父节点。如果span的父节点恰好是li或者其他标签,这个表达式就不会命中。
如果不确定父节点是什么标签,可以用通配符parent::*,例如//span[@class='price']/parent::*会选中任意类型的父节点。还可以用name()函数在谓词里判断父节点标签名:
<!-- 选中父节点是li的span --> //span[name(parent::*) = 'li'] <!-- 选中父节点class包含active的任意元素 --> //span[contains(@class, 'active')]/parent::*[contains(@class, 'item')]
第二种写法在实际爬虫中非常实用:先从特征明显的子元素入手,再通过parent轴跳到包含上下文信息的容器上,把标题、价格、链接等兄弟元素一并取出来。这种“由内向外”的定位思路,在页面结构不稳定、外层容器没有稳定属性时特别有效。
三、parent轴与ancestor轴的区别及组合技巧
parent和ancestor是最容易混淆的两个轴。parent只取直接父节点,ancestor会取从直接父节点一直到根节点的所有祖先。例如//span/ancestor::div可能返回多层div,而//span/parent::div最多返回一层。当你只想回退一层时用parent,想跳到某个特定层级的祖先容器时用ancestor配合谓词。
两者还可以与position类谓词组合。比如//span[@class='price']/parent::*[1]这种写法其实没有意义,因为parent结果集只有一个节点。但ancestor::div[1]表示离当前节点最近的那个div祖先,ancestor::div[last()]表示最外层的div祖先,这些位置约定在ancestor轴中十分常用,写爬虫定位时要格外注意序号的含义方向。
再演示一个多层回溯的组合场景:
<!-- 先定位到目标文本,回退到div,再进入其下一个兄弟节点 --> //td[text()='订单号']/parent::tr/td[2] <!-- 使用缩写形式的等价写法 --> //td[text()='订单号']/../td[2]
上面第一个表达式在解析表格时极为好用:先锚定“订单号”这个单元格,回退到所在行tr,再选中该行的第二个td,通常就是要提取的具体值。..缩写让表达式更短,两种写法在主流解析库中性能几乎没有差别,选择哪种主要看团队规范。
四、实际使用中的常见坑点
第一个坑是把parent和ancestor记混,导致表达式漏选或多选。如果发现parent::div取不到节点,先检查目标节点的直接父级是不是真的div,浏览器开发者工具里看到的“外层div”可能中间还隔着ul、li等层级。
第二个坑与谓词作用对象有关。表达式//div[../div]表示选中那些父节点下存在div子元素的div,而//div/parent::div是选父节点本身,两者语义完全不同。写谓词时要想清楚条件是加在当前节点上,还是加在parent轴的结果上,位置放错结果就会南辕北辙。
第三个坑是不同解析器的兼容性。lxml、浏览器原生document.evaluate、以及各种在线XPath测试工具对某些函数的支持略有差异,name()、contains()基本都是通用的,但个别高级函数在某些实现里不可用。建议写完表达式先在浏览器控制台验证,再放进代码。
from lxml import etree
html = """
<div class="item">
<span class="price">99元</span>
</div>
"""
tree = etree.HTML(html)
# 选中price节点的父节点并取出class属性
result = tree.xpath("//span[@class='price']/parent::*[1]/@class")
print(result) # 输出: ['item']掌握parent::轴之后,配合ancestor、following-sibling等轴,基本可以应对绝大多数“由子元素反推上下文”的定位需求。写XPath时养成先锚定稳定特征、再回溯扩展的思路,选择器的健壮性会明显提升。