在处理XML或HTML文档树时,我们经常遇到这样的需求:已经定位到某个具体节点,但想要获取它的外层容器,或者想把它本身以及下面所有层级的元素都挑出来。XPath里的ancestor轴和descendant轴就是专门用来做这种跨层级选择的。掌握这两个轴,可以让你写的抽取规则更简洁,也不容易因为页面结构微调就失效。
ancestor轴:向上回溯所有祖先
ancestor轴代表当前节点的所有祖先节点,顺序是先父节点,再祖父节点,一直追溯到文档根。它和parent轴不同,parent只取直接父亲,而ancestor会把整条向上的链路都返回。在网页抓取里,如果你定位到了一个<span>里的价格文字,想找包着它的商品卡片<div>,用ancestor就非常自然。
使用时语法是ancestor::节点名,也可以加条件过滤。比如只要带class的祖先div,可以写成ancestor::div[@class]。要注意ancestor返回的是一个节点集合,越靠近当前节点的排在前面。下面是一段Python使用lxml做提取的例子:
from lxml import etree
html = '''
<div class="card">
<div class="info">
<span class="price">99元</span>
</div>
</div>
'''
tree = etree.HTML(html)
# 先找到价格span
span = tree.xpath('//span[@class="price"]')[0]
# 向上找所有祖先div
ancestors = span.xpath('ancestor::div')
for a in ancestors:
print(a.get('class'))
# 输出 info 然后 card
从输出能看到,先拿到的是直接父div(info),再是外层card。这种顺序在需要“最近匹配”的时候很有用,比如取第一个祖先容器当作用户区间。如果你只想要最近的那个,可以用ancestor::div[1]来限定。
不过ancestor轴也有缺点:当文档很深时,它会返回很多层节点,若不加过滤可能拖慢解析。建议在轴后紧跟标签名或属性条件,减少无用节点。另外在严格XML里,祖先包含根元素,别误把根当业务容器。
descendant轴:向下穿透所有后代
descendant轴和ancestor方向相反,它返回当前节点里面的所有后代节点,不论嵌套多少层。它和child轴的区别是,child只拿直接子节点,descendant会一直钻到最底层。比如一个<section>里套了多层<div>,再里面才有<a>,用descendant::a就能一次性全捞出来。
常见写法如descendant::p表示所有后代段落,descendant::*[@id]表示所有带id的后代。下面用一段HTML示例展示如何从一个容器取全部链接:
<div id="box">
<p>简介</p>
<div>
<a href="https://ipipp.com/a">A</a>
<ul>
<li><a href="https://ipipp.com/b">B</a></li>
</ul>
</div>
</div>
对应的XPath可以是//div[@id='box']/descendant::a,这样不论a藏在哪一层都会被选中。下面是用JavaScript在浏览器里运行的等效代码:
const box = document.getElementById('box');
const xpath = 'descendant::a';
const result = document.evaluate(xpath, box, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < result.snapshotLength; i++) {
console.log(result.snapshotItem(i).textContent);
}
// 输出 A 和 B
descendant轴很适合做“区域内全量采集”,比如抓取评论区所有图片、文章块里全部外链。但它的返回量可能很大,如果只要直接子级就别用descendant,改用child省资源。另外descendant包含当前节点自身吗?不包含,它只针对后代,当前节点需用self::或者上下文节点本身。
祖先与后代轴配合使用
实际业务中,经常先向下找到某类节点,再向上反查容器,或者先锁定容器再向下全扫。组合使用ancestor和descendant能让表达式表达更清晰的意图。例如先找到所有命中关键词的<em>,再取它们所属卡片,再取卡片内全部文本。
看一个组合示例,从任意深层链接回溯到商品卡片,再取出卡片里所有文字节点:
//a[contains(@href,'ipipp.com')]/ancestor::div[@class='card']/descendant::text()
这条路径先筛出链接,再用ancestor跳到class为card的div,最后用descendant::text()拿里面全部文字。相比写死多层div路径,这种写法对中间插入广告层不敏感。要注意ancestor可能匹配到多个card,XPath会分别处理,不会漏。
在调试这类长表达式时,建议拆开一步步在工具里验证:先测前半段返回什么,再逐步加轴。这样能快速定位是哪一步没匹配上,而不是对着一整条规则发愁。
性能与易错点小结
用ancestor和descendant时,尽量在轴后面写具体标签或谓语,避免ancestor::*、descendant::*这种全量扫描,尤其在大型文档里差别明显。另外这两个轴返回顺序不同:ancestor从近到远,descendant从文档顺序从上到下。
| 轴名称 | 方向 | 是否包含自身 | 典型用途 |
|---|---|---|---|
| ancestor | 向上 | 否 | 找外层容器、归属区块 |
| descendant | 向下 | 否 | 区域内全量提取、批量采集 |
还有一个易错点:在HTML里根节点之上没有东西,但ancestor会包含html元素本身;有些人以为ancestor只到业务父级,结果取到html后属性为空。写条件时可以用ancestor::div[not(@class='wrapper')]之类排除干扰层。
总的来说,ancestor和descendant是XPath里处理层级跳跃的核心轴。理解它们返回的集合特性和顺序,就能写出既健壮又易读的定位规则,少依赖页面精确结构。
XPathancestordescendant修改时间:2026-08-01 22:03:36