Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月7日

发布于:2026年9月2日

XPath “前一个同级元素”:工作原理(附示例)

`preceding-sibling` 选择树中与上下文节点处于同一层级且位于其之前的节点。这看起来很简单——直到你编写了 ``preceding-sibling::td[1]`` 代码,却得到了与预期不同的单元格。 原因在于,`preceding-sibling` 是一个反向轴,而反向轴上的位置编号是倒序的。 本指南将介绍该方法的选择范围、编号陷阱、它与 `preceding` 的区别,以及它旨在解决的提取模式。

我们的立场很明确:我们是 Geonode,我们向从事数据抓取的人出售代理服务,因此 XPath 与我们的业务密切相关。值得强调的是,选择器失效绝非代理问题。 如果您的数据抓取停止工作,且是由于网站更改了标记结构所致,那么无论增加多少带宽或如何轮换IP地址都无法解决此问题。这两者常被混淆,因为它们都表现为“我的抓取程序不再返回数据”——但代理故障会导致响应被阻断并显示错误页面,而选择器故障则会返回成功响应却没有任何数据。 在检查其他任何内容之前,请先检查原始 HTML。如果页面存在,而你的 XPath 返回的是空节点集,那么本文就与你相关,且你的代理没有问题。

“前置兄弟节点”实际上选择什么

XPath 1.0 规范 用一句话对其进行了定义:preceding-sibling 轴“包含上下文节点的所有前置兄弟节点;如果上下文节点是属性节点或命名空间节点,则 preceding-sibling 轴为空”。

其中两个词汇承载了关键含义。同级节点指具有相同父节点的节点——不包括“表亲”节点、祖先节点,也不包括位于不同层级的任何节点。前置指在文档顺序中位于更靠前的位置。

<div>
  <p>First</p>
  <p>Second</p>
  <span id="here">Context</span>
  <p>Third</p>
</div>

以 span 作为上下文节点:

preceding-sibling::p        → First, Second
following-sibling::p        → Third
preceding-sibling::*        → both p elements

关于属性节点的注意事项值得记住,因为它解释了一类空结果的情况。如果你导航到了一个属性——//@class——那么从该处开始的 preceding-sibling 根据定义是空的,无论该属性所属元素周围有什么内容。属性与任何节点都不是兄弟节点。

“反向轴”陷阱:[1]

并不意味着“第一个” 这是导致结果错误的最常见原因,规范中对此给出了确切的解释。

XPath 将 ancestor 、ancestor-or-self 、preceding 和 preceding-sibling 归类为 反向轴。对于反向轴,邻近位置是根据文档中节点的 反向 顺序来确定的。

因此,在 preceding-sibling 中,位置 1 是指最近的前置同级节点——即紧邻上下文节点之前的那个节点——而不是文档中的第一个节点。

<div>
  <p>Alpha</p>
  <p>Beta</p>
  <p>Gamma</p>
  <span id="here">Context</span>
</div>
preceding-sibling::p[1]     → Gamma   (nearest)
preceding-sibling::p[2]     → Beta
preceding-sibling::p[3]     → Alpha   (furthest)
(preceding-sibling::p)[1]   → Alpha   (first in document order)

括号的出现改变了一切。如果没有括号,[1] 是一个沿反向轴应用的谓词,表示“最近”。 加上括号后,轴的结果首先按文档顺序收集到一个节点集中,而 [1] 则在此集合中进行索引,意为“第一个”。

对比 following-sibling ,这是一个正向轴,其中两者结果一致:

following-sibling::p[1]     → the next one
(following-sibling::p)[1]   → also the next one

正是这种不对称性,导致那些在 following-sibling 上学习的人会被 preceding-sibling 所困扰。习惯会延续,但语义却不会。

实际上,不加括号的情况几乎总是你想要的。“紧接在该值之前的标签”是通常的要求,即 preceding-sibling::label[1] 。只有当你真正指“文档中的第一个”时才需要使用括号,而这种情况比听起来要少见得多。

preceding-sibling 与 preceding

这两个轴的名称极为相似,容易混淆,但它们之间的区别至关重要。

规范将preceding定义为包含“与上下文节点位于同一文档中、且在文档顺序中位于上下文节点之前的所有节点,不包括任何祖先节点,也不包括属性节点和命名空间节点”。

因此,preceding是指文档中位于当前节点之前的所有节点(无论深度如何),但不包括祖先节点。而preceding-sibling仅指与当前节点共享同一父节点的节点。

<body>
  <header><h1>Title</h1></header>
  <div>
    <p>One</p>
    <span id="here">Context</span>
  </div>
</body>

摘自《span》:

preceding-sibling::*    → the p only
preceding::*            → the p, the h1, and the header

排除祖先节点这一规定往往令人感到意外。包含 span 标签的 div 节点 并不 属于 preceding,尽管其起始标签在源代码中出现得更早。根据定义,祖先节点会被排除,因为该轴关注的是位于你之前的节点,而非包含你的节点。

何时使用哪种方法。 preceding-sibling 适用于结构化关系——标签及其值、标题及其下方的段落、同一行中的单元格。preceding 适用于真正松散的关系,例如“无论嵌套情况如何,该元素上方最近的标题”。preceding 的匹配范围更广,速度明显较慢,且极易匹配到你未预期的内容。

标签-值模式

这就是为什么在网页抓取工作中会存在“preceding-sibling

”这种模式,而且值得好好掌握,因为大多数实际的标记结构都是其变体。

问题在于:你想要获取一个值,而该值只能通过其旁边的标签来识别。该值本身没有有用的类名、没有ID,也没有任何区别特征。

定义列表:

<dl>
  <dt>Price</dt>
  <dd>£42.00</dd>
  <dt>Stock</dt>
  <dd>In stock</dd>
</dl>

提取价格意味着找到 dd

,其最近的前置 dt

显示为“Price”:

//dd[preceding-sibling::dt[1] = 'Price']

反向解读:对于每个 dd

,取其最近的前置 dt

;若该文本为“Price”,则保留 dd

。 请注意,[1]

至关重要。如果没有它,只要任何前面的 dt

匹配,preceding-sibling::dt = 'Price'

就会为真,因此第二个 dd

也会符合条件。这是一个真实且常见的错误。

表格单元格:

<tr>
  <td>SKU</td>
  <td>ABC-123</td>
</tr>
//td[preceding-sibling::td[1] = 'SKU']

或者来自两列规格表中某行首列:

//th[normalize-space() = 'Weight']/following-sibling::td[1]

当标签为th

时,通常更推荐使用第二种形式,因为它按正向读取,且与表格的结构相匹配。

鲁棒性改进,使这些表达式能够处理真实的标记:

//dd[preceding-sibling::dt[1][normalize-space() = 'Price']]

normalize-space()

该方法会压缩内部空格并截断两端,从而处理那些经过美化排版的HTML——否则这些HTML会导致精确字符串比较失败。 这是在 XPath 抓取中价值最高的单一函数。

对于部分匹配,contains()

更宽容,但相应地精度也较低:

//dd[preceding-sibling::dt[1][contains(., 'Price')]]

请注意:contains(., 'Price')

也会匹配“不含增值税的价格”和“历史价格”。如果页面上有多个此类标签,你会得到多个结果,而你的代码会默认静默地取第一个结果。

标题及其后续内容,这是该模式的反向应用:

//h2[normalize-space() = 'Specifications']/following-sibling::table[1]

该标题后的第一个表格。这是文档和产品页面中非常常见的需求,且没有等效的 CSS 表达方式能表示“该特定标题后的第一个表格”。

与谓词和条件的组合

preceding-sibling

可与XPath的其他部分组合使用,其中有几种组合值得了解。

计数同级元素 — 用于查找第一个或最后一个元素,或检查位置:

//li[count(preceding-sibling::li) = 0]     first li
//li[count(preceding-sibling::li) < 3]     first three

存在性测试 —— 在布尔上下文中,节点集若非空则为真:

//p[preceding-sibling::h2]                 paragraphs with an h2 somewhere before
//p[not(preceding-sibling::p)]             first paragraph among its siblings

轴的链式调用:

//span[@class='value']/preceding-sibling::*[1]/text()

无论标签为何,其紧邻的前一个元素及其文本。

多个条件:

//td[preceding-sibling::td[1] = 'Status'][normalize-space() != '']

依次应用两个谓词:位于“Status”标签后的单元格,且该单元格不为空。

关于 ..

这种快捷写法的一点说明,它通常比使用轴更简洁:

//dt[.='Price']/following-sibling::dd[1]

对于相同的结果,这种写法通常比 preceding-sibling

更易于阅读,且符合文档的书写方向。当锚点是标签时,建议优先使用这种写法。

CSS 选择器在哪些情况下可以替代它、在哪些情况下无法替代

“CSS 无法回溯”这一传统观点需要更新了。

CSS 现在可以通过 :has() 实现同级元素条件。 该特性在现代浏览器中得到广泛支持,允许选择器基于同级元素关系进行条件筛选:

dt:has(+ dd)          a dt immediately followed by a dd
li:has(~ li.active)   an li with a later sibling that is active

CSS 目前仍无法实现的是:

根据文本内容进行选择。 CSS 中没有与 [text() = 'Price'] 相当的表达式。仅此一点就足以说明,标签-值模式仍属于 XPath 的范畴,因为标签匹配本质上是文本匹配。

导航到任意祖先元素。 :has() 提供了一种父元素选择方式,但尚无通用的祖先轴。

沿反向轴索引。 没有 CSS 结构能表示“此类元素中最近的前一个元素”。

而 CSS 更擅长的是: 所有简单操作。类和 ID 选择、后代关系、属性匹配。CSS 选择器更易于阅读,得到工具更好的支持,且在大多数引擎中运行更快。

明智的做法是默认使用 CSS,仅在需要文本匹配或向后导航时才使用 XPath。在同一个代码库中混合使用这两种方式是可行的,与完全依赖其中一种相比,一个 90% 的选择器使用 CSS、仅将 10% 的复杂情况交给 XPath 的爬虫,其维护起来会更加容易。

性能与脆弱性

两个实际限制。

性能。 preceding-sibling 的性能受同级节点数量的限制,而该数量通常较小——这没问题。preceding 会扫描文档中上下文节点之前的所有内容,在大型页面上这会消耗大量资源,且在循环内部其时间复杂度为二次。 如果使用 preceding 的选择器运行缓慢,原因几乎可以肯定就是这个。通常的解决方法是改用 preceding-sibling,并从更靠近的上下文节点进行选择。

脆弱性。 基于同级元素的选择器依赖于文档结构,而这恰恰是重新设计所改变的部分。一旦有人插入一列,preceding-sibling::td[1] 就会悄无声息地失效。不会出现任何错误提示;选择器会匹配到不同的单元格,而你的数据则在不知不觉中出错。

以下三种缓解措施确实有效:

在可能的情况下,以文本而非位置作为锚点。 //dt[.='Price']/following-sibling::dd[1] 能在列表重新排序后依然有效,而 (//dd)[3] 则无法做到。

验证提取的内容。 如果价格应符合某种货币格式,请进行检查。除非有验证机制,否则当选择器开始返回股票状态而非价格时,这一问题将无法被察觉。

如有嵌入式结构化数据,请优先使用。 如果页面在 <script type="application/ld+json"> 块中包含 JSON-LD,请优先解析该数据。它专为机器可读而设计,在页面重构过程中稳定性远高于其他方式,并且能彻底消除选择器易碎性这一问题。在编写任何 XPath 之前花三十秒检查一下,绝对值得。

隐性结构错误属于我们在为何测试代理至关重要中描述的那类问题——请求成功、解析成功,但数据却有误。

何时不应使用它

当元素具有可用的标识符时。 如果存在 id、class 或 data 属性,请使用它们。依赖于结构的选择器,其可靠性绝对不如依赖于开发者刻意选择的名称的选择器。

当有结构化数据可用时。 JSON-LD、微数据(microdata)、XHR 请求中的 JSON 有效载荷。这些方法都比解析渲染后的 HTML 更优。

当关系确实松散时。 如果你发现自己写的是 preceding::*[5],说明结构实际上并未提供任何有意义的信息,且该选择器在下次部署时就会失效。请重新考虑这种做法。

当 CSS 足以满足需求时。 对于简单的选择器,CSS 更易于阅读且支持更广泛。将 XPath 保留用于文本匹配和向后导航。

当你在浏览器中依赖 XPath 2.0 功能时。 浏览器通过 document.evaluate 实现 XPath 1.0,而 Selenium 则遵循浏览器的实现。 因此,不支持 matches()、正则表达式、upper-case() 以及序列类型。lxml 等服务器端库的通用 API 也基于 XPath 1.0。如果从网上找到的代码片段无法正常工作,请检查它是否使用了仅存在于后续版本中的函数。

大家还问

在 XPath 中,preceding-sibling 的作用是什么?

它选择所有与上下文节点具有相同父节点,且在文档顺序中位于其前面的节点。它不包括祖先、后代或树中其他层级的节点——仅包括同级节点。如果上下文节点是属性节点或命名空间节点,则该表达式返回空集。

为什么 preceding-sibling[1] 返回的元素有误?

因为 preceding-sibling 是一个反向轴,而反向轴上的位置编号遵循文档顺序的逆序。因此,[1] 表示最近的先前同级节点,而非文档中的第一个。若要获取文档顺序中的第一个节点,请将轴用圆括号括起来:(preceding-sibling::p)[1]。

“preceding” 和 “preceding-sibling” 有什么区别?

preceding-sibling 仅涵盖具有相同父节点的节点。preceding 涵盖文档中任何深度下所有位于该节点之前的节点,但不包括祖先节点和属性节点。preceding 的匹配范围更广,速度明显更慢,且更容易匹配到非预期的内容。

如何在 XPath 中根据标签选择一个值?

先按文本匹配标签,然后获取相邻元素://dt[normalize-space()='Price']/following-sibling::dd[1];或者从值的一侧开始://dd[preceding-sibling::dt[1]='Price']。[1] 至关重要——如果没有它,只要任何前置标签匹配,谓词就为真。

CSS 选择器能实现 preceding-sibling 的功能吗?

部分可以。现代浏览器中的 :has() 提供了带条件的兄弟节点选择功能。但 CSS 目前仍无法根据文本内容进行选择,也无法沿反向轴进行索引,而文本匹配正是标签-值模式所必需的。对于简单的选择,请使用 CSS;若需要文本匹配或反向导航,则使用 XPath。

“前置兄弟”在 Selenium 和浏览器中是否有效?

是的。浏览器通过 document.evaluate 实现了 XPath 1.0,而 Selenium 则使用浏览器的引擎。该轴属于 XPath 1.0 规范,且普遍可用。 无法使用的是 XPath 2.0 及后续版本中的任何功能——不支持正则表达式、不支持 matches(),也不支持 upper-case()。

“前置同级元素”操作是否很慢?

通常不会。其性能受同级节点数量的限制,而该数量通常较少。真正耗时的是 preceding 轴,因为它需要扫描文档中所有位于该节点之前的节点,且在循环中该操作会呈现二次时间复杂度。如果基于同级节点的选择器运行缓慢,请检查是否实际使用了 preceding。

如何降低 XPath 选择器的脆弱性?

尽可能基于文本而非位置进行锚定,使用 normalize-space() 来应对空格差异,优先使用标识符和数据属性而非结构,在解析 HTML 之前先检查是否嵌有 JSON-LD,并验证所提取内容的结构,确保匹配错误元素时选择器会明确报错而非静默失败。

总结 `

`preceding-sibling是一个功能单一的工具,其唯一用途是向后遍历同一级别的节点。需要牢记的是,这是一个反向轴,因此[1]`` 表示“最近的”而非“第一个”,而添加括号则会完全颠倒这一含义。正是这一细微差别,导致了人们使用该方法时出现的大部分错误结果。

其真正的价值在于“标签-值”模式——提取一个只能通过其旁边的文本来识别的字段。CSS 通过 :has() 填补了部分空白,但它仍然无法根据文本内容进行选择,而文本正是标签的本质。在这种情况下,XPath 依然是正确的选择,而非仅因习惯而用的工具。

值得警惕的是,结构选择器会“静默”失效。 新增一列、重新排序的列表、包裹的 div 元素——这些变化都会导致你的选择器匹配到其他内容,而界面外观却看似一切正常。尽可能基于文本进行定位,在编写任何选择器之前先检查嵌入的结构化数据,并验证输出结果——因为你能察觉到的错误,绝非代价最高的那个。