返回博客
指南
Mihai MaximLast updated on May 13, 20264 min read

用于网络抓取的 XPath Cheat Sheet:语法、轴和实际代码

用于网络抓取的 XPath Cheat Sheet:语法、轴和实际代码
简而言之:这份 XPath 速查表涵盖了网页抓取中真正需要的语法、谓词、轴和函数,还附带了 CSS 到 XPath 的转换表以及可运行的 Puppeteer 和 Scrapy 示例。下次当你依赖的某个网站上 CSS 选择器悄然失效时,不妨将其作为参考手册。

前言

您来到这里,可能是因为某个选择器失效了,页面布局发生了变化,或者团队里有人告诉您“直接用XPath”,而您希望在一页内搞定。这很合理。这份XPath速查表正是为这样的时刻而设计:当爬虫开发者在项目进行中,需要语法、示例以及一些容错规则,却不想费力翻阅教程时。

XPath(全称 XML Path Language)是一种查询语言,它使用路径风格的表达式来遍历 XML 及类似 XML 的文档(包括 HTML)。在爬取场景中,它能提供 CSS 选择器无法实现的功能:沿树结构向上遍历、根据可见文本匹配,以及像小型谓词逻辑那样链式组合条件。 我们将涵盖核心语法、谓词、轴、最实用的函数、浏览器端测试、CSS 到 XPath 的转换表,以及两个简短的 Puppeteer 和 Scrapy 实战示例,这些内容你今天就可以直接应用到项目中。学习完毕后,你将能够解析 HTML 片段,编写能适应轻微 DOM 变化的选择器,并在发布前对其进行验证。

什么是 XPath 以及它为何对网页抓取至关重要

XPath即XML路径语言:一种利用路径式表达式遍历文档树以识别节点的查询语法。浏览器将HTML DOM呈现为类似XML的树结构,这正是XPath能用于网页抓取的原因——尽管HTML并非严格的XML。它是XSLT标准的核心组成部分,广泛应用于各类XML工具及大多数抓取技术栈中。

那么,为何抓取开发者会选择它?原因有三。首先,XPath 可在树结构中上下移动,而 CSS 选择器仅能向下或横向移动。其次,它能根据节点可见文本进行匹配,当类名随机变化但标签保持稳定时,这一特性尤为宝贵。第三,它允许在单个表达式中链式组合条件。这份 XPath 速查表涵盖了您在生产环境中运用这些功能所需的全部基础知识。

XPath 如何理解网页:路径、节点与步骤

XPath 表达式是由步骤组成的路径。每个步骤包含一个轴(搜索方向)、一个节点测试(匹配的节点类型)以及零个或多个谓词(方括号内的过滤条件)。大多数情况下轴是隐含的,因为 child:: 是默认值,这也是为什么像 //div/span 能正常工作。

//div[@class='quote']/span[1]为例。 //descendant-or-self:: 轴的缩写,因此这句话的含义是:找到任意 div 且其 class 属性等于 quote,然后取其第一个 span 子节点。你需要关注三种节点类型:元素节点(div, span)、属性节点(@class, @href)以及通过 text()访问的文本节点。谓词采用从 1 开始的索引,因此 [1] 是第一个匹配项,而不是第二个。

在浏览器中实时测试 XPath 表达式

在编写任何爬虫代码之前,请先验证表达式在浏览器中是否有效。本速查表的测试部分包含两种工作流程。

在“元素”面板中使用 Ctrl+F(Cmd+F)。在 Chrome 或 Firefox 中打开开发者工具,点击“元素”选项卡,然后按 Ctrl+F 或 Cmd+F。搜索栏支持 CSS 选择器和 XPath 表达式,页面会高亮显示 DOM 树中的每个匹配项,并显示计数(如“12 个中的 3 个”)。这是测试选择性的最快方法。

$x() 控制台辅助工具。切换到“控制台”标签页并运行 $x("//div[@class='quote']")。该辅助工具会将匹配结果作为 JavaScript 数组返回,因此您可以使用 $x("//a/@href").length。根据当前厂商文档, $x() 该功能已内置于基于 Chromium 的开发者工具和 Firefox 开发者工具中,但您仍应在目标浏览器版本中进行验证。正如某篇工程文章所述,这“是测试表达式的好方法,既无需在代码编辑器上花费时间,也不会给网站服务器带来任何压力”。

若需深入了解该语言本身,MDN XPath 文档是最可靠的入门起点。

XPath 与 CSS 选择器:快速决策框架

CSS 选择器通常更简洁且易于阅读,因此许多开发者将其视为默认选择,仅在 CSS 无法表达查询时才使用 XPath。这种观点虽属主观判断而非客观标准,但确实反映了大多数生产环境代码库的演变规律。真正的准则在于:选择在 DOM 发生微小变化时仍能保持有效的最简单选择器。

请将下表作为我们关于 XPath 与 CSS 选择器讨论的最终裁决依据:

场景

选择

稳定 id 或唯一类

CSS

需要遍历到父元素或祖先元素

XPath

按可见文本内容匹配

XPath

:nth-of-type 样式定位选择

无论哪种,CSS 更简洁

对单个元素设置多个条件

XPath(谓词链)

针对已知结构的快速一行代码

CSS

深度嵌套或不规则的标记

XPath

CSS 选择器没有 parent:: 方向属性,也无法根据文本进行筛选,因此任何“点击包含‘Active’的行”的模式自然属于XPath的范畴。

XPath 核心速查表:语法基础

这些是本 XPath 速查表中其他所有部分构建的基础。一个 XPath 表达式至少包含一个标签名,可选地包含一个属性,以及该属性的可选。谓词和函数构建在这些基础上。

符号

含义

示例

读作

/

从根节点选择

/html/body

位于 body 位于根节点正下方的 html

//

后代或自身的简写形式

//a

文档中 a 文档中的任意位置

.

当前节点

.//span

一个 span 从上下文节点派生

..

当前节点的父节点

//span/..

任何 span

@

属性选择器

//img/@src

src 每个 img

*

任何元素节点

//div/*

任何 div

tag[@attr='value']

通过属性相等性过滤的标签

//a[@rel='nofollow']

所有 arel 等于 nofollow

`expr1 \

expr2`

两个表达式的并集

`//h1 \

//h2`

全部 h1h2 元素组合

一个有用的思维模型: / 锚点定位位置,标签名命名步骤,谓词过滤步骤。实际抓取工具中最健壮的 XPath 语法通常是一串用 /分隔的属性谓词链,而非从根节点开始的位置路径。

谓词:通过索引、属性和条件进行过滤

谓词是括号内的条件,它们将一个通用步骤转化为精确的步骤。它们可以串联,且从左到右进行求值。请记住,XPath 谓词采用 1 为起始的计数方式,因此 //li[3] 是第三个 li,而非第四个。

您将反复使用的常见模式:

谓词

作用

[n]

第 n 个匹配元素。 //tr[1] 是第一行。

[last()]

最后一个匹配项。 //li[last()] 是列表的最后一项。

[position()<=3]

前三个匹配项。

[@class='quote']

属性完全相等。

[contains(@class,'btn')]

属性上的子字符串匹配。

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

对属性的前缀匹配。

[not(@disabled)]

否定任何谓词。

[@type='text' and @name='q']

对单个元素设置多个条件。

您可以将谓词进行链式组合: //div[@class='quote'][position()<=5] 列出前五个引用。若要深入探讨谓词链与 CSS 伪类在主题层面的选择,请参阅我们关于 XPath 与 CSS 选择器的配套文章。

轴:沿 DOM 向上、向下及横向遍历

这正是XPath优于CSS之处。轴指定了从上下文节点出发的遍历方向,而显式的 axis::node-test 形式解锁了 CSS 无法实现的移动方式。本页的 XPath 速查表将 XPath 轴作为重点章节,每行列举一个实际应用案例。

返回值

抓取示例

child::

直接子节点(默认轴)

//ul/child::li 列出直接 li 子节点。

descendant::

所有后代

//article/descendant::a 获取文章中的所有链接。

descendant-or-self::

// 简写形式

//section//h3 会遍历到任何 h3 内部 section.

parent::

直接父元素

//span[@class='price']/parent::div 返回包装器 div.

ancestor::

直至根节点的每个祖先

//a[@class='sku']/ancestor::tr 从链接跳转到其行。

ancestor-or-self::

同上,外加节点本身

当匹配结果可能是包装器或子节点时,这很方便。

following-sibling::

该节点之后的兄弟节点

//dt[.='Price']/following-sibling::dd[1] 读取一个值。

preceding-sibling::

该节点之前的同级节点

//h2[.='Specs']/preceding-sibling::p[1] 获取上方的段落。

attribute::

节点的属性(@ 简写形式)

//img/attribute::alt 是每张图片的替代文本。

self::

当前节点

用于谓词: *[self::h2 or self::h3].

您将经常使用的两个是 ancestor::following-sibling::. following:: 以及 preceding:: 轴也存在,但很少用到。

通配符与节点测试

通配符可以放宽步骤的节点测试条件,当标签不同但结构相同时,这非常有用。

Token

匹配

*

任何元素节点。 //div/* 返回任何 div.

@*

任何属性节点。 //img/@* 列出每个 img.

node()

Any 节点上的所有属性,包括文本和注释。 //div/node() 返回完整的子节点序列。

text()

仅限文本节点。 //p/text() 是段落文本,不包括子元素。

comment()

HTML 注释,偶尔对标记驱动的抓取有用。

关键区别在于: //div/* 会跳过文本和注释,而 //div/node() 则包含它们。当您明确需要字符串时,请使用 text()

爬取必备的 XPath 函数

谓词中的函数是处理现实世界中混乱HTML的关键:多余的空格、动态类名、大小写不一致以及计数问题。这些XPath函数大约能覆盖95%的抓取场景。

字符串函数

函数

示例

用例

contains(s, sub)

//div[contains(@class,'product-card')]

匹配动态或复合类名。

starts-with(s, prefix)

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

筛选内部产品链接。

ends-with(s, suffix)

//img[ends-with(@src,'.webp')]

仅限 XPath 2.0。在 XPath 1.0 中请使用 substring.

normalize-space(s)

//h1[normalize-space()='New Arrivals']

在比较文本前去除周围的空格。

translate(s, from, to)

//a[translate(text(),'ABC','abc')='shop']

使用粗略小写转换进行不区分大小写的匹配。

substring(s, start, len)

substring(@data-id, 1, 4)

提取属性值的一部分。

数字和节点函数

函数

示例

使用案例

count(nodes)

count(//tr)

结果页面上的行数。

position()

[position() mod 2 = 0]

选择偶数索引的节点。

last()

//li[last()]

无论长度如何,取最后一个项目。

name()

[name()='figure']

按标签动态筛选。

not(expr)

//input[not(@disabled)]

跳过已禁用的输入项。

对于仅输出属性的情况,使用类似 string(@href) 可确保您获得的是字符串形式的值,而不是属性节点,这在某些抓取器中,当与正则表达式或修剪步骤串联时非常重要。

CSS 到 XPath 快速转换表

如果您的团队习惯使用 CSS 思维,因为您来自 BeautifulSoup 或 Cheerio,那么此表可作为快速迁移的辅助工具。XPath 速查表中的等效表达式未必更短,但总是更具表现力。

CSS

XPath

#main

//*[@id='main']

.btn

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

div.btn

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

a[href]

//a[@href]

a[href='/x']

//a[@href='/x']

a[href^='/']

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

ul > li

//ul/li

ul li

//ul//li

li:first-child

//li[1]

li:nth-of-type(2)

//li[2]

li:last-child

//li[last()]

h2 + p

//h2/following-sibling::p[1]

:not([disabled])

[not(@disabled)]

虽然 concat(' ',normalize-space(@class),' ') 这种模式虽然看起来不美观,但在 XPath 中模仿 CSS 类语义却是最安全的方法,因为 XPath 1.0 没有原生的“空格分隔列表中的标记”运算符。如需按 CSS 特性分类的并列入门指南,请参阅我们的 CSS 选择器速查表

实例演示:使用 Puppeteer 和 Scrapy 提取数据

为了证明同一表达式在不同栈中同样有效,这里有两个简短的爬虫程序,它们访问 quotes.toscrape.com 并提取所有引文及其作者。我们在开发者工具中确认, //div[@class='quote'] 在撰写本文时,该表达式在首页上匹配了大约十条引语,这与渲染后的页面显示结果一致。

Puppeteer(Node.js)使用 npm init -ynpm install puppeteer,然后将以下代码放入 index.js。请注意,Puppeteer的最新版本已转向定位器API,因此请确认 page.$x 是否与您 package.json.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://quotes.toscrape.com/');

const rows = await page.$x("//div[@class='quote']");
const out = [];
for (const row of rows) {
  const text = await row.$eval("span[@class='text']", el => el.textContent);
  const author = await row.$eval("small[@class='author']", el => el.textContent);
  out.push({ text, author });
}
console.log(out);
await browser.close();

Scrapy(Python)。在蜘蛛(spider)内部, response.xpath 支持相同的表达式,并提供 .get().getall() 用于单值和多值提取。

def parse(self, response):
    for q in response.xpath("//div[@class='quote']"):
        yield {
            "text": q.xpath(".//span[@class='text']/text()").get(),
            "author": q.xpath(".//small[@class='author']/text()").get(),
        }

根据社区反馈,Cheerio 和 Beautiful Soup 并不直接支持 XPath,因此 Scrapy 或 lxml 是 Python 环境中的常用组合,而 Puppeteer 或 Playwright 则是 JavaScript 环境中的常用组合。在确定采用其中一种方案之前,请先验证您已安装的版本。如需完整的项目操作指南,我们的 Scrapy 和 Puppeteer 指南涵盖了配置、定时任务和代理连接。

在生产环境爬虫中编写健壮 XPath 的技巧

这份速查表以五条规则收尾,这些经验均来自凌晨三点爬虫崩溃的惨痛教训:

  • 避免使用以 /html/body/...。一旦设计师发布首次页面布局变更,它们就会失效。
  • 只要存在稳定的 classdata-* 属性可用时,优先使用属性条件。
  • 将文本比较包裹在 normalize-space() ,以防止首尾空格悄无声息地破坏相等性检查。
  • 使用 contains() 处理动态类名,并使用 or 来处理已知的变体: //a[contains(@class,'btn-primary') or contains(@class,'btn-cta')].
  • 保持选择器的描述性。 //div[@class='quote']/span[@class='text']//span.

关键要点

  • 当需要沿树向上遍历、根据可见文本进行匹配,或在单个选择器中链式连接多个条件时,XPath 优于 CSS。
  • 核心语法精简: /, //, ., .., @,谓词和并集运算符涵盖了本 XPath 速查表中的大部分表达式。
  • 轴是支持从 CSS 切换过来的最有力理由,而 ancestor::, following-sibling::preceding-sibling:: 是您最常使用的三个轴。
  • 在 DevTools 中使用 Ctrl+F 验证每个表达式, $x() ,再将其集成到爬虫中。这比重新部署一个出错的爬虫要快得多。
  • 生产环境安全的 XPath 依赖于属性、 contains()normalize-space(),并远离硬编码的位置路径。

常见问题

XPath在Selenium、Puppeteer、Playwright和Scrapy中的工作原理是否相同?

基本上是,但有两点需要注意。这四个引擎都支持 XPath 1.0 表达式并返回匹配的节点,但封装方法有所不同:Selenium 使用 find_element(By.XPATH, ...),Scrapy 使用 response.xpath(...),Playwright 使用 page.locator("xpath=..."),而 Puppeteer 过去使用 page.$x() ,尽管最新版本更倾向于使用定位器 API。在从旧教程中复制粘贴代码前,请先检查您的库版本。

为什么我的 XPath 在 Chrome 开发者工具中能正常工作,但在 Python 脚本中却返回空结果?

这几乎总是因为页面通过 JavaScript 渲染内容,而您的脚本获取的是原始 HTML,因此浏览器显示的节点在响应中并不存在。请通过 Ctrl+U 查看页面源代码,而不是通过“渲染的元素”标签页。解决方法是使用 Playwright 之类的无头浏览器,或者调用页面正在访问的已文档化的 JSON 接口。

如何编写不区分大小写的 XPath 表达式来匹配文本或属性?

XPath 1.0 没有 lower-case() 函数,因此常用的变通方法是使用 translate() 对字符进行折叠: //a[translate(text(),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='shop']。如果您的引擎支持 XPath 2.0 或 3.1, lower-case()matches() regex 函数会更简洁。请务必先在浏览器中测试两个分支。

如何抓取一个在每次页面加载时类名都会变化的元素?

锚定在某个稳定的元素上:例如 data-idaria-label,一个标签和文本固定的子元素,或者父级地标。如果只有部分类名是稳定的, contains(@class,'product-card') 则适用。当连该部分也是动态的,请向上遍历至稳定的祖先元素,再向下遍历: //section[@aria-label='Results']//article[1] 比任何基于类名的选择器都更可靠。

如何根据某个元素的子元素文本来选择该元素?

使用按后代文本过滤的谓词。例如, //tr[td[normalize-space()='Active']] 返回包含一个修剪后文本等于 Active。若需获取匹配的单元格而非行,可直接将其作为锚点: //td[normalize-space()='Active']。将比较运算符包裹在 normalize-space() ,可确保匹配结果不受空格影响。

结论

一份优秀的 XPath 速查表,与其说关注冷门语法,不如说是一个精简且可重复使用的工具包,让你在紧迫的截止日期压力下也能游刃有余。当 CSS 无法满足需求时,尝试使用 axes 遍历树结构;优先锚定属性与可见文本而非位置路径;并在将表达式投入抓取程序前,务必通过开发者工具进行验证。如果你在接下来的一周编写选择器时始终打开本页,这些模式就会深入骨髓。

XPath 解决了解析问题,但并未解决数据获取问题。 真正的爬取项目,绝大多数时间都耗费在应对速率限制、指纹识别以及渲染版与原始 HTML 不匹配等问题上,而这正是我们开发 WebScrapingAPI 旨在解决的。只需将我们的 Scraper API 指向任意 URL,即可获取干净的 HTML 内容,并使用你在开发者工具中测试过的相同 XPath 表达式进行解析。这样一来,你唯一需要调整的选择器就在解析器端——这才是它该在的地方。

关于作者
Mihai Maxim, 全栈开发工程师 @ WebScrapingAPI
Mihai Maxim全栈开发工程师

米海·马克西姆(Mihai Maxim)是 WebScrapingAPI 的全栈开发工程师,他在产品各领域均有贡献,并协助为该平台构建可靠的工具和功能。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。