XPath作为在XML与HTML文档中定位节点的标准查询语言,其核心能力不仅体现在路径步的轴选择上,更体现在对节点集的精细过滤。谓词(Predicate)就是附着在路径步之后、用方括号包裹的条件表达式,它的作用是对该步选出的每一个节点逐一测试,只有使谓词结果为真(或非零数字)的节点才会留在结果集中。可以把谓词理解成SQL里的WHERE子句,只不过它是内嵌在路径表达式内部、紧贴着节点测试的过滤器。

一、谓词的基本语法与执行原理
在XPath中,一个典型的带谓词的表达式形如 节点测试[条件] 。这里的“节点测试”可以是元素名、通配符 * 或是 text() 等,方括号中的“条件”就是谓词。处理器在求值时,会先按轴与节点测试得到初始节点集,然后针对集合里的每个节点,在将该节点作为上下文节点的环境下计算谓词表达式。若结果是布尔值true,节点保留;若是数字,则仅当该数字等于节点的上下文位置(从1开始计)时才保留;若是节点集,则当集非空时保留。
这种“逐节点上下文求值”的机制,意味着谓词内部可以引用当前节点的属性、子元素甚至祖先。例如 book[price > 30] 会对每本book计算其price子元素值是否大于30。需要留意的是,谓词可以连续叠加,如 book[price > 30][author] 表示先筛出价高书籍,再要求必须有author子节点,顺序执行、层层收窄。
1.1 位置谓词与最后一项
最常见的简单谓词是数字,用来选取第N个节点。 items/item[1] 取第一个item, items/item[last()] 取最后一个。 last() 是XPath内置函数,返回上下文节点集的大小。还可以写 item[position() mod 2 = 0] 挑出偶数位置节点,其中 position() 给出当前节点在上下文里的序号。
位置谓词非常适用于列表页抽取首条或末条记录,但要注意它依赖文档顺序而非排序后的视图。如果先用了排序函数(某些XPath扩展)再过滤,语义会随处理器实现而不同,标准XPath 1.0并没有排序步。
二、用属性与函数构造过滤条件
谓词不仅能看位置,更能深入节点内容。通过 @属性名 可在谓词中访问属性,例如从导航栏中挑出当前页: //a[@class = 'active'] 表示所有a元素中class属性等于active的那些。若属性可能含多个值,可用 contains(@class, 'active') 做包含判断。
字符串与节点集函数也常出现在谓词里。 //div[contains(text(), '优惠')] 找出文本含“优惠”的div; //ul[count(li) > 3] 选出子项超过三个的列表。 count() 返回节点集数目,使过滤摆脱写死位置的脆弱性,更贴合实际结构变动。
2.1 嵌套路径谓词
谓词内部还能再写路径,形成嵌套判断。比如要找那些拥有特定 grandchildren 的节点: //section[.//img[@alt = 'chart']] 表示自身后代中任意位置有alt为chart的img的section。点号 . 代表当前节点,使相对搜索局限在候选节点范围内,不会误伤整棵文档。
这种写法在爬取卡片式布局时极有用:先框定卡片容器,再用谓词确认其内部是否具备“封面图”“价格块”等标志性子结构,从而排除广告位或空白模板。
三、代码示例:从XML中抽取目标书籍
下面是一段XML及对应的XPath谓词用法。假设我们有图书数据,需要找出价格高于30且出版年份在2010年之后的书,并只取前两本。
<library>
<book>
<title>XPath入门</title>
<price>25</price>
<year>2008</year>
</book>
<book>
<title>高级抓取</title>
<price>45</price>
<year>2015</year>
</book>
<book>
<title>数据清洗</title>
<price>39</price>
<year>2012</year>
</book>
</library>
对应的XPath表达式可以写作:
/library/book[price > 30 and year > 2010][position() <= 2]
上述表达式中,第一个谓词用 and 连接两个条件,过滤出贵且新的书;第二个谓词限制最多取前两本。在Python的lxml中调用方式如下:
from lxml import etree
xml_data = '''<library>
<book><title>XPath入门</title><price>25</price><year>2008</year></book>
<book><title>高级抓取</title><price>45</price><year>2015</year></book>
<book><title>数据清洗</title><price>39</price><year>2012</year></book>
</library>'''
tree = etree.fromstring(xml_data.encode('utf-8'))
# 使用带谓词的XPath
nodes = tree.xpath("/library/book[price > 30 and year > 2010][position() <= 2]")
for n in nodes:
print(n.find('title').text)
# 输出:高级抓取、数据清洗
可以看到,谓词把原本需要Python循环加if判断的逻辑,压缩进了一条声明式表达式,既易读又方便非程序员维护规则。
四、常见误区与性能注意
一个容易混淆的概念是:谓词里的数字指的是“上下文位置”而非某种全局序号。若先写 //book 得到全集,再 [1] 是每棵子树下的第一个book还是全集第一个,取决于前面轴返回的集合结构。通常 //book[1] 在XPath 1.0里表示每个父节点下的第一个book,而非文档中第一个book;要拿文档首个需用 (//book)[1] 。
性能方面,过于宽泛的轴(如 // )配合复杂谓词会导致全文档扫描。应尽量缩小基准集,比如用 /library/book[...] 代替 //book[...] 。另外,在谓词中调用 contains() 等函数虽灵活,但在超大文档上反复求值也耗时,必要时可先抽出子集再在内存中二次过滤。
五、总结
谓词是XPath过滤节点集的核心语法,它以方括号包裹条件,在路径每一步上对候选节点做布尔或位置判定。通过属性比对、内置函数、嵌套路径以及位置函数,开发者可以用极短的规则完成精准抽取,远胜手动遍历。掌握其上下文求值特性与位置语义差异,便能写出稳定、高效且易维护的文档查询表达式。