Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月7日

发布于:2026年9月2日

XPath contains():附示例的完整指南

`contains()` 这是人们使用最频繁却理解最少的XPath函数。它看起来像是一个子字符串测试,但只有当你传入字符串时,它才会像子字符串测试那样工作。 当给定一个节点集(而大多数表达式生成的正是节点集)时,它会默默地只使用第一个节点,并忽略其余节点。 本指南将探讨这一陷阱、那个初学者往往难以一次掌握的类匹配惯用法,以及XPath 1.0中本应简单却被处理得过于复杂的空格和大小写处理问题。

先简要说明一下我们为何撰写这篇文章。我们是 Geonode;我们向从事数据提取的人士出售代理服务,因此经常收到关于选择器的提问。相关的免责声明很简短:选择器漏洞和代理问题表现出的症状截然不同,将两者混淆会白白浪费数小时。 被拦截的请求会返回验证页面或错误状态码。而contains()出错时,则会返回一个完全正常的页面,但结果为空。如果HTML页面存在,而您的表达式未能检索到任何内容,那么本文正是您需要参考的指南,且网络连接没有问题。

函数本身

XPath 1.0 规范 表述简洁:

如果第一个字符串参数包含第二个字符串参数,则 contains 函数返回 true;否则返回 false。

两个参数均为字符串。两个参数,一个布尔结果,区分大小写,不支持通配符,不支持正则表达式。

//a[contains(@href, 'download')]
//div[contains(@class, 'product')]
//p[contains(text(), 'Price')]

真正有趣的行为完全体现在当你提供的参数不是字符串时会发生什么——而这种情况占绝大多数。

节点集陷阱:contains()

仅识别第一个节点 这就是导致“我的 XPath 在某些页面上能正常工作,而在其他页面上却不行”这一问题的 bug,规范对此给出了精准的解释:

节点集通过返回该节点集中按文档顺序排在首位的节点的字符串值来转换为字符串。如果节点集为空,则返回空字符串。

因此,当你编写代码生成一个节点集并将其传递给 contains() 时,XPath 会默默地忽略除第一个节点以外的所有内容。

<div>
  <p>Introduction</p>
  <p>Price: £42</p>
  <p>Availability</p>
</div>
contains(//p, 'Price')      → false

错误,因为 //p 是一个包含三个节点的节点集,字符串转换只取第一个节点——“Introduction”——而该节点并不包含“Price”。其余两个段落从未被考虑在内。

解决方法是让谓词针对每个节点分别应用,而不是对集合进行转换:

//p[contains(., 'Price')]   → the second paragraph

在此,contains() 会针对每个 p 分别进行一次求值,其中 . 即为该特定节点。这相当于询问“集合是否包含此项?”与“集合中的哪些成员包含此项?”之间的区别,而通常人们所指的只有后者。

在 text() 中也存在同样的陷阱,它同样是一个节点集:

//div[contains(text(), 'Price')]

text() 会返回 所有 直接的文本节点子节点,而字符串转换会取其中的第一个。如果该元素的文本跨多个节点——这在存在嵌套标记时总是会发生——那么你实际上只测试了第一个片段。

.

与 text()

:同一问题的另一面 规范将元素的字符串值定义为:

该元素节点所有文本子节点的字符串值按文档顺序连接而成的字符串

这就是这两种形式之间的关键区别。

<p>Total: <strong>£42.00</strong> including VAT</p>
contains(., '£42.00')          → true   (all descendant text, concatenated)
contains(text(), '£42.00')     → false  (first direct text node only: "Total: ")

.

会深入到嵌套元素中。text()

仅查看直接子元素,且仅取其中的第一个。

**默认情况下请使用 .

。** 它匹配的是读者所理解的“该元素的文本”,并且能够经受住标记变更的影响,例如有人将值包裹在 <span>

中。

**当您明确希望排除嵌套内容时,请有意识地使用 text()

** —— 例如,匹配一个标签时,不匹配其子元素(如徽章或工具提示)内部的文本。

对于“任意文本节点包含此内容”的情况,正确的写法是将谓词应用于文本节点本身:

//div[text()[contains(., 'Price')]]

虽然冗长,但正确。

空格:normalize-space() 并非可选

真正的 HTML 会进行美化显示,且空格会被包含在字符串值中。

<td>
    In stock
</td>

该单元格的字符串值为 "\n In stock\n",因此与 'In stock' 进行精确比较时会失败。

规范中定义了修复方法:

normalize-space 函数返回经过空格规范化的字符串参数,具体操作包括:去除首尾空格,并将连续的空格字符替换为单个空格。

//td[normalize-space() = 'In stock']
//td[contains(normalize-space(), 'In stock')]

请注意,不带参数的 normalize-space() 会作用于上下文节点,这正是你在谓词内部所需要的。

就 contains() 而言,空格在两端的影响较小,而在中间部分则影响很大。对 'In stock' 的搜索会因 "In stock" 而失败,除非你先进行规范化处理。如果你只在选择器中养成一个习惯,那就务必将文本比较包裹在 normalize-space() 中。

正确匹配类

这是对 ``contains()`

` 最常见的误用,也是最容易在不知不觉中出错的情况。

//div[contains(@class, 'btn')]

这会匹配 ``class="btn"`

。它还会匹配 ``class="btn-primary"

、``class="unbtn"

和 ``class="sidebar-btn-group"

。由于 ``@class

是一个由单个空格分隔的字符串,而 ``contains()

` 是一个普通的子字符串测试,因此它无法识别单词边界。

正确的写法是在属性和目标两端都添加空格,以确保仅匹配完整的令牌:

//div[contains(concat(' ', normalize-space(@class), ' '), ' btn ')]

可理解为:获取类属性,规范其空格,用空格将其包裹,使每个令牌两端都由空格分隔,然后查找被空格包围的目标。class="btn-primary"

变为 " btn-primary "

,该字符串不包含 " btn "

。class="icon btn large"

变为 " icon btn large "

,该字符串包含 。

虽然看起来很丑,但这是正确的,也是每个成熟的爬虫代码库最终都会采用的方式。将其封装在辅助函数中:

def has_class(name):
    return (f"contains(concat(' ', normalize-space(@class), ' '), ' {name} ')")

当需要匹配两个类时:

//div[contains(concat(' ', normalize-space(@class), ' '), ' btn ')
      and contains(concat(' ', normalize-space(@class), ' '), ' primary ')]

此时,CSS 选择器——div.btn.primary

——可读性要高得多,而且能完全实现预期效果。 如果仅需匹配类名而无需其他条件,请使用 CSS。 XPath 的价值在于需要文本匹配或向后导航时,而非用于 CSS 已经处理得很好的场景。我们在 XPath 前置兄弟节点 中对比了这两者。

区分大小写与translate()的变通方案

contains() 区分大小写,而 XPath 1.0 没有 lower-case() 函数。浏览器和 Selenium 都实现了 XPath 1.0,因此这一限制在关键场景中始终存在。

该变通方案使用 translate(),其定义为返回“将第一个参数字符串中出现的、属于第二个参数字符串的字符,替换为第三个参数字符串中对应位置的字符”:

//p[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
                          'abcdefghijklmnopqrstuvwxyz'), 'price')]

逐字符转写,仅限 ASCII 字符。 除非将两个字符串都扩展以涵盖带重音的字符,否则它不会将带重音的字符转为小写,而这样做很快就会变得难以管理。

如果有条件,还有两个更好的选择:

匹配不区分大小写的子字符串。 如果页面上出现“Price”或“PRICE”,但从未出现“price”,那么匹配 'rice' 虽然写法不美观,但确实有效。 这通常是更务实的解决方案。

使用支持更丰富 XPath 功能的库。 诸如 lxml 之类的服务器端解析器支持 EXSLT 扩展,包括用于正则表达式的 re:test(),该扩展可处理大小写及其他多种情况。浏览器不支持这些功能,因此你找到的 lxml 代码片段可能正因这个原因在 Selenium 中无法正常运行。

如果你发现自己写出了冗长的translate(),这说明对于此任务而言,XPath 1.0 已无法满足需求。

相关的字符串函数

contains()

属于一个小型函数族,该族中的其他函数通常更为精确。

**starts-with()

** — “如果第一个参数字符串以第二个参数字符串开头,则返回 true”。该函数比 contains()

更具体,因此出现过度匹配的可能性也相应更低:

//a[starts-with(@href, 'https://')]

XPath 1.0 中没有 ends-with()

函数。 该变通方案使用了 substring()

和 string-length()

,但操作起来相当繁琐,因此通常采用其他方法更为妥当。

**substring-before()

和 substring-after()

** —— 前者“返回第一个参数字符串中位于第二个参数字符串首次出现之前的部分……如果第一个参数字符串不包含第二个参数字符串,则返回空字符串”。 用于在表达式内部拆分值:

substring-after(//span[@class='price'], '£')

**normalize-space()

** — 已在上文介绍,这也是你最常使用的那个。

**translate()

** — 忽略大小写,并通过将字符映射为空值来去除字符:

translate(., ',', '')

**string-length()

** — 过滤掉空值或截断值:

//td[string-length(normalize-space()) > 0]

组合使用才能发挥最大价值:

//tr[contains(normalize-space(td[1]), 'Weight')]/td[2]

任何行中,第一列提及“Weight”的第二列内容,不区分空格。

频繁出现的模式

以下表达式涵盖了大多数实际的数据提取工作,值得随时备查。

查找标签旁边的值。 这是抓取结构化页面时最常见的需求:

//dt[contains(normalize-space(), 'Price')]/following-sibling::dd[1]
//th[contains(normalize-space(), 'Weight')]/following-sibling::td[1]

请注意 [1]

—— 若省略该条件,following-sibling::td

会返回该行中所有后续单元格,而你的代码会默默地提取第一个单元格,却误以为它是唯一的。

根据链接的可见文本而非 href 属性查找链接:

//a[contains(normalize-space(), 'Download')]

当 URL 是哈希标识符时,这种方法比匹配 URL 更稳健;但当网站被翻译时,这种方法则更不稳定。 根据哪个更常变化来选择。

根据容器内部的内容查找容器:

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')][.//span[contains(., 'Sold out')]]

两个依次出现的谓词:一张卡片,其中包含一个标有“售罄”的 span 标签。这种组合方式比试图用一个条件来表达更流畅,也更易于阅读。

采用排除法而非包含法。 通常这样表述更清晰:

//tr[not(contains(@class, 'header'))]
//li[not(contains(normalize-space(), 'Advertisement'))]

**匹配按钮,无论其是 button

还是 a

:**

//*[self::button or self::a][contains(normalize-space(), 'Continue')]

查找包含特定值的行,并提取另一列:

//tr[td[contains(normalize-space(), 'SKU-1234')]]/td[3]

从内向外读取:先查找包含 SKU 的单元格所在的行,再取该行的第三个单元格。 这就是表查找模式,与对整个表进行绝对索引相比,它在列重新排序时具有更强的鲁棒性。

防范空值匹配。 一个用于过滤空值的谓词无需额外开销,却能避免后续处理中的一类混淆:

//td[string-length(normalize-space()) > 0][contains(., 'Ltd')]

何时不应使用 contains()

当你需要精确匹配时。 contains(., 'Price') 也会匹配“Historic Price”和“Price excluding VAT”。如果你只想匹配该标签,请使用 normalize-space() = 'Price'。过度匹配不会触发任何提示——你的代码会取第一个结果,而完全不知道其实有三个结果。

当你仅需匹配类名且不涉及其他内容时。 CSS 能以正确且易于阅读的方式实现这一点。参见上文。

当你需要正则表达式时。 XPath 1.0 并不支持正则表达式。如果必须使用,请先通过 contains() 提取数据,然后在宿主语言中应用正则表达式,这样你还能直观地看到匹配结果。

当存在可用的标识符时。 ID、data 属性或嵌入的 JSON-LD 比任何文本匹配都更稳定。文本属于内容,而内容会发生变化——重新设计、翻译或内容编辑都会导致基于文本匹配的选择器失效,且没有任何警告提示。

当字符串来自用户输入时。 将不可信的文本插值到 XPath 表达式中即为 XPath 注入。如有提供,请使用库中的变量绑定功能;若无,请进行适当的转义——特别是引号,因为 XPath 1.0 没有用于字符串字面量中引号的转义序列,你必须使用 concat() 来构建一个。

大家还问

在 XPath 中,contains() 函数的作用是什么?

当第一个字符串参数包含第二个字符串作为子字符串时,该函数返回 true。两个参数均为字符串,比较区分大小写,且不支持通配符或正则表达式。它在数据提取中的常用作用是根据元素的文本或属性值的一部分来匹配该元素。

为什么我的 XPath contains() 找不到任何结果?

最常见的原因是您传入了一个节点集。XPath 会按文档顺序取第一个节点并忽略其余部分,从而将节点集转换为字符串,因此 contains(//p, 'x') 只会检查第一个段落。请改用针对每个节点应用谓词的方式://p[contains(., 'x')]。

contains(.) 和 contains(text()) 有什么区别?

. 使用元素的字符串值,规范将其定义为所有后代文本节点的拼接结果——因此它能遍历嵌套的标记。text() 返回直接的文本节点子元素,而字符串转换仅保留第一个。 除非您明确希望排除嵌套内容,否则请使用 .。

如何使用 XPath 匹配类名?

请使用 contains(concat(' ', normalize-space(@class), ' '), ' name '),该方法会在属性值两端添加空格,确保仅匹配完整的令牌。普通的 contains(@class, 'btn') 也会匹配 btn-primary 和 unbtn。如果您仅需匹配类名,使用 CSS 选择器通常更清晰且默认更准确。

XPath 的 contains() 是否区分大小写?

是的,而且 XPath 1.0 没有 lower-case() 函数。标准的解决方法是使用 translate(),并显式指定大写和小写字母,但该方法仅支持 ASCII 字符。lxml 等服务器端库支持 EXSLT 正则表达式;而浏览器和 Selenium 则不支持。

如何使用 contains() 进行多条件匹配?

使用 and 和 or 组合谓词://div[contains(@class, 'card') and contains(., 'In stock')]。每个 contains() 都是针对同一上下文节点进行的独立布尔测试。

XPath 是否有以“开头”或“结尾”为条件的函数?

starts-with() 确实存在,在适用情况下,它比 contains() 更值得优先使用,因为它产生的过度匹配更少。XPath 1.0 中没有 ends-with() —— 解决方法是使用 substring() 配合 string-length(),但这种方法相当繁琐,通常采用其他方法会更好。

为什么 contains() 匹配的元素比预期的更多?

因为这是一种不考虑单词边界的子字符串测试。contains(., 'Price') 也会匹配 "Historic Price" 和 "Price excluding VAT"。若需进行相等性比较,请使用 normalize-space() = 'Price';若需匹配类标记,请使用带填充符的连接语法。

总结

contains() 在规范上看似简单,但在实际使用中却暗藏诸多陷阱,而这些陷阱几乎都源于同一点:XPath 通过提取第一个节点并舍弃其余节点,将节点集转换为字符串。 正是这一条规则解释了为什么 contains(//p, 'x') 会给出一个看似确凿却错误的结果,为什么 contains(text(), 'x') 会遗漏分散在多个节点上的文本,以及为什么同一个表达式在某个页面上能正常工作,而在另一个页面上却会失败。

避免这些问题的技巧并不多。请在谓词内部使用 contains(),以便按节点逐个进行求值。除非有特殊原因,否则请使用 . 而不是 text()。 将文本比较用 normalize-space() 包裹起来,因为真正的 HTML 会进行美化显示。至于类匹配,要么使用带填充的连接语法,要么——更好的做法是——使用 CSS 选择器,它正是为此设计的。

将 XPath 保留用于其独有的功能:文本匹配和向后导航。这些是 CSS 无法替代的真正功能,因此值得使用其语法。若将其用于选择 div.card,则等同于付出代价却未获得相应收益。