每个爬虫的成败都取决于一个决定:如何在满是标记的页面中定位它想要的元素。搞错了,下次网站调整一个类名或移动一个包装元素,您的脚本就会崩溃;搞对了,同一个解析器可以运行数月。在这里您会用到的两种查询语言是用于网络抓取的 XPath 和 CSS 选择器,大多数实际运行的爬虫会混合使用两者,而不是非此即彼。

本指南是一份实用的选择器参考。我们并排介绍 CSS 选择器和 XPath,展示针对相同元素的两种语言的等效表达式,在真实的 Python 库中运行两者,并讨论何时一种明显优于另一种。读完之后,您将知道针对特定页面应该使用哪种,以及如何编写能在网站改版后继续工作而不是崩溃的选择器。

两种语言概览

CSS 选择器是您在样式表中已经熟悉的模式:.price#headerdiv > span。浏览器不断评估它们,每个抓取库都支持它们,在常见场景下读起来简洁清晰。当页面有合理的类名和 ID 时,它们是"获取这个元素"的最短路径。

XPath("XML Path Language",XML 路径语言)是一种用于导航文档树的完整查询语言。它将页面视为可以在任意方向遍历的节点:向下到子节点、向上到祖先节点、横向到兄弟节点。它可以按文本内容匹配、用布尔条件过滤,并以 CSS 无法表达的方式组合谓词。这种能力的代价是一些冗长,但在杂乱或深度嵌套的页面上值得付出。

两者都针对相同的 DOM。区别在于覆盖范围和人体工程学:CSS 简洁且熟悉,XPath 精确且富有表达力。了解各自的局限性才是关键所在。

CSS 选择器详解

CSS 选择器通过标签、类、ID、属性、关系和位置来定位元素。以下是您每天会用到的构建块,以及每个选择器所针对的标记。

标签、类和 ID。三个最常见的起始点。裸标签名匹配该类型的每个元素,前置点号匹配类,前置井号匹配 ID。

css
a                  /* every anchor on the page */
.product-title     /* any element with class product-title */
#product-price     /* the element with id product-price */
span.price-label   /* span elements that also have class price-label */

后代与子节点。空格表示"内部任意深度"。> 表示"仅直接子节点",只向下一级。当布局在多个深度嵌套相同标签,而您只想要直接那一级时,这个区别很重要。

css
div.price-container span      /* any span inside, at any depth */
div.price-container > span    /* only spans that are direct children */

属性。方括号可以匹配任意属性,不仅仅是类和 ID。精确匹配用 [attr=val],子字符串匹配用 [attr*=val],前缀匹配用 [attr^=val],后缀匹配用 [attr$=val]。属性选择器通常是您最稳定的钩子,因为 data 属性比视觉类更少变动。

css
a[role='link']            /* anchors with role exactly "link" */
[data-testid='price']     /* any element with that test id */
a[href^='/product/']      /* anchors whose href starts with /product/ */

位置。伪类按兄弟节点顺序选择。:first-child:last-child 和万能的 :nth-of-type(n) 让您获取给定标签的第 n 个元素,这是从重复块中提取"第二行"或"第四个列表项"的方法。

css
.product-list li:first-child       /* first item in the list */
ul.specs li:nth-of-type(3)         /* the third li */
table tr:nth-of-type(2) td         /* cells in the second row */

这套工具涵盖了大多数真实提取工作。CSS 力所不及的地方是匹配元素包含的文本,以及从已知节点向遍历树。对于这些情况,您需要用到 XPath。

XPath 详解

XPath 表达的是穿越文档的路径。前置 // 表示"在树中任意位置搜索",单个 / 表示"直接子节点",方括号保存用于过滤匹配节点的谓词。以下是用 XPath 编写的相同类型目标。

标签和后代。双斜杠是您日常最常用的开头;它在任意深度查找匹配元素。

xpath
//div                         (: every div on the page :)
//div[@class='price-container']/span   (: direct span children :)

属性谓词。在方括号内,您用 @ 测试属性。精确匹配用 [@class='x'];对于包含多个空格分隔值的类,contains(@class, 'x') 更安全,因为它在 x 是多个类之一时也能匹配。

xpath
//*[@id='product-price']                  (: by id :)
//*[contains(@class, 'product-title')]    (: class among many :)
//a[@href]                                (: any anchor that has an href :)

文本匹配。这是 XPath 的标志性功能。您可以按元素包含的文本选择它,精确匹配用 text()='...',模糊匹配用 contains(text(), '...')。CSS 没有等效写法。

xpath
//button[text()='Add to Cart']
//span[contains(text(), 'In stock')]
//label[normalize-space()='Email address']

位置。XPath 索引从 1 开始,写在谓词中。您还可以使用 last()position() 等函数从末尾或某个范围选取。

xpath
(//div[@class='product'])[1]      (: first matching product :)
//ul[@class='specs']/li[3]        (: the third li :)
//ul/li[last()]                   (: the final li :)

轴。真正的强大之处。轴让您可以向 CSS 无法到达的方向移动:following-sibling(后续兄弟)、preceding-sibling(前置兄弟)、parent(父节点)和 ancestor(祖先节点)。经典案例是标签-值对,您知道标签文本,想要旁边的值。

xpath
(: the value cell next to the "Founded" label :)
//th[text()='Founded']/following-sibling::td

(: walk up from a price to its product card :)
//span[@class='price']/ancestor::div[@class='card']

最后这两个是完全没有简洁 CSS 等效写法的查询类型,这正是 XPath 始终保留在工具箱中的原因。

并排对比:相同元素,两种写法

将两种语言并排呈现,权衡取舍一目了然。对于常见目标,两者几乎等效,而 CSS 通常更短。

text
Goal                      CSS                          XPath
all anchors               a                            //a
class match               .product-title               //*[contains(@class,'product-title')]
id match                  #product-price               //*[@id='product-price']
tag + class               span.price-label             //span[@class='price-label']
descendant                .box span                    //*[@class='box']//span
direct child              .box > span                  //*[@class='box']/span
attribute exact           a[role='link']               //a[@role='link']
nth of type               li:nth-of-type(3)            //li[3]
text match                (not possible)               //button[text()='Buy']
walk up the tree          (not possible)               //span/ancestor::div[@class='card']

规律很清晰:对于标签、类、ID、属性和位置,选择主要取决于个人偏好,CSS 在简洁性上胜出。最后两行是 XPath 独领风骚的地方。

在 Python 中运行两者

理论只能走这么远;以下是每种语言在代码中的实际样子。我们使用 parsel(Scrapy 基于的选择器库),因为它针对同一个已解析的文档支持 CSS 和 XPath,可以逐行对比。BeautifulSoup 和 lxml 是其他常见选择,后文会提到。

bash
python -m venv selectors_env
source selectors_env/bin/activate

pip install parsel

将一些标记加载一次,然后用两种方式查询它。注意 parsel 的 .css().xpath() 都返回选择器列表,因此无论使用哪种语言,访问模式都是相同的。

python
from parsel import Selector

html = """
<div class="card">
  <h2 class="product-title">Wireless Mouse</h2>
  <span class="price">$24.99</span>
  <a role="link" href="/product/mouse">Details</a>
</div>
"""

sel = Selector(text=html)

# CSS: concise and familiar
title = sel.css("h2.product-title::text").get()
price = sel.css("span.price::text").get()
link  = sel.css("a[role='link']::attr(href)").get()

# XPath: the same three fields
title = sel.xpath("//h2[@class='product-title']/text()").get()
price = sel.xpath("//span[@class='price']/text()").get()
link  = sel.xpath("//a[@role='link']/@href").get()

print(title, price, link)

对于 BeautifulSoup,CSS 路径是 soup.select_one("span.price"),多个元素用 soup.select(...);它不原生支持 XPath。当您专门需要 XPath 时,lxml 是标准工具:在已解析的 lxml.html 文档上使用 tree.xpath("//span[@class='price']/text()")。parsel 是方便的折中方案,因为它在同一个对象上提供了两种 API。

CSS 在底层编译为 XPath

parsel 和 lxml 等库在运行 CSS 选择器之前会将其转换为 XPath(通过 cssselect 包)。这就是为什么您能用 CSS 表达的任何内容都有等效的 XPath,但反过来不成立:文本匹配和向上的轴没有 CSS 形式可供转换。当 CSS 选择器无法表达您的意图时,切换到 XPath 是自然的下一步,而不是变通方法。

XPath 胜出的情况

当页面反击时,就该用 XPath 了。三种情况使其成为明确的选择。

  • 需要按文本匹配。"写着 Add to Cart 的按钮"或"标签为 Founded 的行"只能通过内容来表达。//button[text()='Add to Cart']contains(text(), ...) 没有 CSS 等效写法。
  • 需要向上遍历树。当您能可靠地找到叶子节点(比如一个唯一的价格),但实际想要的是其容器时,ancestor::div[@class='card'] 可以向上攀升。CSS 只能向下和横向,永远无法向上。
  • 需要组合条件。XPath 谓词可以用 andor 组合://div[@class='item' and @data-available='true'],或同时按位置和属性过滤。以这种方式叠加条件在 CSS 中很麻烦甚至不可能。

标签-值对是您最常遇到的情况。在规格表或个人资料侧边栏中,您想要的字段位于稳定标签旁边的单元格中,而其自身的类是通用的或不存在的。锚定在标签文本上,用 following-sibling 横向移动,比计算随字段增减而变化的 :nth-of-type 位置更耐用。

CSS 胜出的情况

对于日常的大多数情况,CSS 是更好的默认选择。它更短、可读性更强,其语法是大多数开发者从前端工作中已经掌握的,因此团队成员无需学习第二门语言就能审查您的选择器。在类名和 ID 合理的结构良好的页面上,.product-card .price 比其 XPath 等效写法用更少的空间表达了您需要的一切。

CSS 也与浏览器自动化工具自然配合。当您驾驶无头浏览器并需要抓取动态内容时,您在 document.querySelector 中会写的那些 CSS 选择器可以直接沿用,这使您的选择器词汇在项目的静态解析和实时 DOM 部分保持一致。对于简单、快速、在整洁布局上重复提取的工作,CSS 是正确的工具,只有当 CSS 真正无法表达目标时才升级到 XPath。

Crawlbase Crawling API

当您自己解析原始 HTML 时,选择器是不可避免的,但并不总是您需要亲手编写的工作。Scraper API 自动解析常见页面类型(如产品页、搜索结果和评论)并输出结构化 JSON,因此对于支持的目标,您完全跳过 XPath 和 CSS,直接从响应中读取字段。需要自定义解析时,将其与渲染后的 HTML 配合,用上面的选择器进行解析。从免费层级开始。

编写不会崩溃的选择器

抓取的难点不在于选择语言,而在于编写能在网站下次部署后继续工作的选择器。同样的健壮性规则适用于 XPath 和 CSS。

  • 优先选择稳定属性而非视觉类。css-1x7a9qmt-4 这样的哈希值或工具类是生成的,经常变化。data-testididitemprop 或 ARIA role 更有可能在网站重新设计后存活。当它们存在时,优先锚定在这些上。
  • 避免长而深的链式选择器。body > div > div > section > div:nth-child(2) > ul > li 这样的选择器编码了整个布局,因此沿该路径任何地方添加包装元素都会破坏它。匹配最近的有意义容器加一个稳定的钩子,而不是追溯完整的祖先链。
  • 不要依赖脆弱的位置。:nth-of-type(4) 假设计数永远不变。当稳定的标签或属性可用时,锚定在那上面并相对导航(这正是 XPath 轴的优势所在),而不是硬编码索引。
  • 对多值类使用 contains一个带有 class="btn btn-primary active" 的元素不会精确匹配 [@class='btn-primary']。在 XPath 中用 contains(@class, 'btn-primary'),或在 CSS 中用普通的 .btn-primary 类选择器,后者已经能在多个类中匹配一个。
  • 让失败响亮,而非无声。包装提取逻辑,使缺失字段返回 None 而不是崩溃,然后记录哪个选择器返回了空值。这将网站变更从神秘的空白记录变成了哪个选择器需要维护的清晰信号。

将选择器视为需要维护的代码。标记会漂移,上个季度运行顺畅的爬虫最终会返回空字段。解决方法几乎总是在开发者工具中重新检查实时元素并收紧选择器,而不是重建爬虫。对于更完整的设置,关于如何用 Python 抓取网站的指南从获取到解析到存储进行了端到端的介绍,而这些选择器模式可以直接融入该流程。

完全跳过选择器

有时最好的选择器是没有选择器。如果您的目标是常见页面类型,Crawling API 会直接返回已解析的 JSON,因此无需任何选择。对于其他情况,您仍然需要自己获取和解析,而当页面是客户端渲染或有保护时,您解析的渲染后 HTML 可以来自 Crawling API。无论哪种方式,这里的选择器技能都是将原始标记转化为干净记录的关键,而掌握两种语言意味着您永远不会因为其中一种无法到达某个元素而陷入困境。

回顾

核心要点

  • 两者都针对相同的 DOM。CSS 选择器简洁且熟悉;XPath 冗长但更富表达力。大多数真实的爬虫会混合使用两者。
  • CSS 涵盖标签、类、ID、属性和位置,使用简短、可读的模式:.class#iddiv > span[attr=val]:nth-of-type(n)
  • XPath 能做 CSS 无法做到的事:text()contains() 按文本匹配,用 ancestor 向上遍历树,用 following-sibling 横向移动,以及用 and/or 组合条件。
  • 在 Python 中用 parsel 运行两者(在同一个对象上使用 .css().xpath());BeautifulSoup 仅支持 CSS,lxml 是 XPath 的首选。
  • 健壮性胜过聪明。优先选择稳定属性,避免深链式选择器和脆弱的索引,对多值类使用 contains,并在字段缺失时让失败响亮。
  • 您可以完全跳过选择器,在受支持的页面类型上使用 Crawling API 的自动解析,将手写的选择器保留用于自定义目标。

常见问题

对于初学者,XPath 和 CSS 选择器哪个更好?

大多数情况下是 CSS 选择器。语法与您从样式页面中已知的内容重叠,对于标签、类、ID 和属性目标来说读起来简洁,每个抓取库都支持它。接下来学习 XPath,专门针对 CSS 无法做到的事情:按文本内容匹配,以及向上或横向导航树。

所有抓取库都支持 XPath 和 CSS 选择器吗?

大多数至少支持一种,许多两种都支持。parsel 和 Scrapy 在同一个对象上处理 CSS 和 XPath,lxml 专为 XPath 而建,Selenium 和 Playwright 都接受两者。BeautifulSoup 是个明显的例外:它通过 .select() 支持 CSS,但没有原生 XPath 支持。在确定选择器风格之前请查阅您的库文档。

CSS 选择器能按文本匹配元素吗?

不能。CSS 无法通过元素包含的文本来选择它;它只能按标签、类、ID、属性和位置匹配。当您需要"写着 Add to Cart 的按钮"或"Founded 标签旁边的单元格"时,这正是 XPath 的 text()contains(text(), ...) 的用武之地,而这些没有 CSS 等效写法。

XPath 比 CSS 选择器快吗?

在大多数抓取工作中差异可以忽略不计,因为库通常在运行之前就将 CSS 内部编译为 XPath。应该根据表达力和可读性来选择,而不是原始速度。如果 CSS 选择器能清楚地表达您的需求,就用它;当需要文本匹配、向上导航或 CSS 无法表达的组合条件时,才使用 XPath。

如何编写在网站变更时不会崩溃的选择器?

锚定在稳定的钩子上,如 iddata-testiditemprop 或 ARIA 角色,而不是生成的视觉类。通过匹配最近的有意义容器而不是追溯完整祖先链来保持选择器简短;在稳定标签存在的地方避免硬编码位置索引;对多值类使用 contains。然后在字段缺失时让失败响亮,这样标记变更就会表现为清晰的信号,而不是无声的空白。

什么时候应该完全跳过选择器?

当您的目标是自动解析服务已经理解的常见页面类型时。Crawling API 为受支持的目标(如产品页和搜索结果)返回结构化 JSON,因此无需解析 HTML,也无需维护选择器。将手写的 XPath 和 CSS 保留用于自定义页面或自动解析器未覆盖的字段。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量