Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月7日

发布于:2026年9月2日

CSS 选择器速查表:带示例的指南

CSS 选择器是指定目标元素的最简洁方式。它们用于样式设置、JavaScript 查询,以及所有专业爬虫库中的数据提取。 这是一份以实际应用为导向的参考指南:首先介绍语法表,接着是真正名副其实的伪类,然后是特异性,最后是频繁出现的模式。 它还涵盖了 CSS 目前仍无法实现的功能,不过这份清单比以往要短了。

关于本文作者及其在此处为何无关紧要的一点说明:我们是 Geonode,我们向从事数据提取工作的人士出售代理服务。 选择器的选择与我们销售的产品毫无关联,唯一值得一提的是诊断方面的重叠——从外部来看,一个无法匹配任何内容的选择器与一个被拦截的请求看起来完全一样,但它们是完全无关的问题。 如果页面已加载但你的选择器未找到匹配项,那么本文正是你需要的参考资料,且无需更改页面获取方式的任何设置。

基本选择器

选择器匹配条件
*所有元素
div所有 div 元素
.card具有类名 card 的元素
#main具有 id main 的元素
.card.featured同时具有这两个类名的元素
div.card具有类名 card 的 div 元素
div, p同时具有类名 div 和 p 的元素

其中有两点值得重点说明。

.card.featured(无空格)表示一个元素同时具有这两个类名。 .card .featured(带空格)表示一个具有类名 featured 的元素,位于一个具有类名 card 的元素内部。仅一个字符的差异就完全改变了含义,这也是该领域中最常见的拼写错误。

div, p 是一个选择器列表,而非关系。它匹配任一组中的所有内容,且每个部分均独立评估——因此 div, p.note 匹配所有带有类名 note 的 div 以及段落,而非带有类名 note 的 div。

组合子

这些用于表达元素之间的关系。

选择器匹配条件
div p位于 div 内部任意位置的 p(后代)
div > p作为 div 的直接子元素的 p
h2 + p紧跟在 h2 之后的 p(相邻同级元素)
h2 ~ p紧跟在h2之后且具有相同父元素的p(一般同级元素)

后代组合符是一个空格,这使得它很容易被无意中引入,也容易在阅读时被忽略。

> 更精确且通常更稳健。div > p 不会匹配下个月有人用 <section> 包裹的段落——这听起来像是缺点,但往往恰恰相反,因为一个会悄无声息地匹配比预期更多元素的选择器,比一个停止匹配并告知你的选择器更糟糕。

+ 和 ~ 是兄弟组合符,它们只向前查找。 CSS 中没有“前一个同级元素”组合子;:has() 间接提供了该效果,下文将进行说明。

属性选择器

通常这是最稳定的选项,因为属性通常是经过深思熟虑后选择的。

选择器匹配条件
[href]具有 href 属性的元素
[href="/about"]精确值
[href^="/docs"]值 以...开头
[href$=".pdf"]值 以...结尾
[href*="download"]值 包含...
[class~="card"]值是 包含单词“card” 的空格分隔列表
[lang|="en"]值是 en 或以 en- 开头
[data-state="open" i]不区分大小写的匹配

其中有两个功能未被充分利用。

[class~="card"] 与 .card 完全等价 —— ~= 运算符执行以空格分隔的单词匹配,这正是 class 属性所需要的。了解它的存在主要有助于理解 .card 的实际作用,以及在变量中包含属性名时通过编程方式构建选择器。

i标志可使匹配不区分大小写,对于网站中大小写不一的属性值而言,这确实非常实用。

就提取而言,data-*属性比类(class)更值得优先选用。[data-testid="product-card"]是由开发者选定作为稳定的挂钩;而类名可能会在每次部署时被构建工具重新生成。

伪类:结构类

选择器匹配条件
:first-child作为父元素第一个子元素的元素
:last-child作为父元素最后一个子元素的元素
:only-child没有同级兄弟元素的元素
:nth-child(3)第三个子元素
:nth-child(2n)编号为偶数的子元素
:nth-child(2n+1)编号为奇数的子元素
:nth-child(-n+3)前三个子元素
:nth-last-child(2)倒数第二个子元素
:first-of-type同类型子元素中排在第一位的
:nth-of-type(2)同类型子元素中排在第二位的
:empty没有子元素(包括文本)的元素
:root文档根目录,通常为 html

-of-type 的不同变体比人们意识到的更为重要。 p:first-child 仅当段落是其父元素的第一个子元素时才匹配——因此,如果标题排在前面,它将无法匹配任何内容。p:first-of-type 则无论前面有什么内容,都会匹配第一个段落。在数据提取中,后者几乎总是你真正想要的结果。

:nth-child(-n+3) 表示“前三个”,这是An+B语法中值得记住的惯用表达,此外还有 :nth-child(n+4) 表示“第四个及之后”。将它们组合起来即可形成范围::nth-child(n+2):nth-child(-n+5) 选择第2到第5个子元素。

伪类:逻辑类

这是近年来变化最大的类组。

选择器匹配条件
:not(.hidden)不带类名的元素 hidden
:is(h1, h2, h3)所列任一选择器
:where(h1, h2, h3)与 :is() 相同,但特异性为零
:has(> img)包含匹配后代或同级元素的元素

:is() 和 :where() 可简化重复的列表。:is(article, section) > h2 替代了 article > h2, section > h2,且随着列表变长,这种简化效果会迅速累积。 它们之间的唯一区别在于特异性::is() 采用其最特异的参数的特异性,而 :where() 始终贡献零特异性。

:not() 在现代浏览器中接受选择器列表,因此 :not(.a, .b) 有效,且表示“两者皆非”。较早版本的单参数行为需要通过 :not(.a):not(.b) 进行链式调用。

:has() 才是关键。 MDN 文档 将其描述为“当作为参数传递的任何相对选择器以该元素为锚点时,至少匹配一个元素,则该元素即为目标”,从而提供“一种相对于参考元素选择父元素或前序兄弟元素的方法”。 其状态为“基线广泛可用”,自 2023 年 12 月起在各浏览器中均受支持。

div.card:has(span.sold-out)     /* a card containing a sold-out marker */
h1:has(+ p)                     /* an h1 immediately followed by a p */
li:has(~ li.active)             /* an li with a later active sibling */
label:has(input:checked)        /* a label wrapping a checked input */

有三项已记录的限制:它“不能嵌套”在另一个 :has() 内部;伪元素不能作为其内部内容或锚点;其特异性遵循与 :is() 和 :not() 相同的规则——即以特异性最高的参数为准。

伪类:状态与表单

选择器匹配条件
:hover, :focus, :active交互状态
:focus-visible应显示可见环的焦点
:focus-within包含获得焦点的后代元素的元素
:checked已选中的复选框、单选按钮或选项
:disabled, :enabled表单控件状态
:required, :optional表单验证属性
:valid, :invalid约束验证状态
:placeholder-shown显示占位符的输入框
:target与 URL 片段匹配的元素
:visited, :link链接状态

其中大部分属于样式问题,而非数据提取问题,但有两个例外。:checked 在测试自动化中读取表单状态时确实非常有用,而 :disabled 则能告诉你控件是否具有交互性——这往往能解释为什么点击后没有任何反应。

请注意,在数据抓取场景中,无法通过 querySelectorAll 有效查询 :hover 和 :focus,因为在您仅解析过的文档中,这两种状态都不存在。

特异性简述

在选择器竞争样式时相关,而在查询时无关——值得了解,因为人们常将这两种情境混淆。

特异性由三个数值决定:

组成部分来源
ID#main
类、属性、伪类.card, [href], :hover
元素、伪元素div, ::before

从左到右比较:无论有多少个类,其特异性都永远无法超过单个 ID。内联样式胜过所有这些,而 !important 又胜过内联样式,这就是为什么二者都应作为最后手段而非常用工具。

两点现代注意事项。:where() 不提供任何特异性,这使其成为库默认值的理想选择——用户应能轻松覆盖这些默认值。此外,:is()、:not() 和 :has() 均采用其最特异参数的特异性,因此 :is(#main, div) 与 #main 的特异性相同。

对于 querySelectorAll 和 scraping,特异性完全不适用。 选择器要么匹配某个元素,要么不匹配;不存在竞争关系。

数据提取模式

在爬虫中真正发挥作用的选择器。

网格中的产品卡片:

div.product-grid > article.product-card

PDF 链接:

a[href$=".pdf"]

外部链接:

a[href^="http"]:not([href*="example.com"])

稳定的测试钩子(而非类):

[data-testid="price"]

包含内容的容器:

tr:has(td.error)
article:has(img)
form:has(input:invalid)

文章的第一段,无论其前文如何:

article p:first-of-type

跳过一行标题:

tbody tr:not(:first-child)

提取第 2 行至第 11 行:

tbody tr:nth-child(n+2):nth-child(-n+11)

具有特定类标识符的元素(通过编程构建):

[class~="card"]

需要排除的空单元格:

td:not(:empty)

编写不会失效的选择器

参考文档会告诉你有哪些可用选项。在这些选项中做出选择,正是确保选择器持久性的关键;而一个能在重新设计后依然有效、另一个却在下周就失效的选择器之间的区别,完全取决于它们依赖于什么。

根据锚点的选择是否经过深思熟虑来对其进行排序。 data-testid 之所以存在,是因为开发者为了实现自动化而将其放置在那里,因此它是页面上最稳定的元素。ID 紧随其后——通常是刻意设置的,偶尔也会被生成。语义元素或 ARIA 角色之所以稳定,是因为它们承载着意义。 手动编写的类名稳定性中等。像 css-1x9dj2k 这样的生成类名在每次构建时都会改变,因此毫无价值。位置选择器是所有选择器中稳定性最低的,因为它们编码了页面在某个特定时刻的确切布局。

选择器的具体程度应仅满足语义需求,切勿过度。 body > div > div > div.content > p 虽然精确但非常脆弱:该链条中任何位置添加的包裹元素都会导致其失效。而 .content p 既表达了实际需求,又能经受住页面结构调整的考验。出于安全考虑而增加选择器特异性的本能,反而会产生与安全背道而驰的结果。

优先选择会“高调报错”的选择器。 匹配到零个元素的选择器会引发显而易见的问题。而匹配到过多元素的选择器则会悄无声息地返回第一个元素,这看似合理,但可能导致错误。若需抉择,请选择会直接报错的版本,而非会产生偏差的版本——当关系确实是直接关联时,应选择 div > p 而非 div p。

将选择器集中存放。 散落在代码库中的四十个选择器字符串,就是四十个独立的风险源。若将它们收集到一个命名清晰、描述性强的模块中,它们便成为你所依赖内容的映射图;当网站发生变更时,更新工作只需一小时,而非耗费一整天进行“考古”式排查。

明确提取内容的结构。 这是能及时发现隐性漂移的习惯。如果价格应符合某种货币格式,就进行验证;如果列表中项目数量从未少于二十个,则将少于二十个视为错误。若缺乏这一验证,一旦选择器开始匹配错误的元素,就会无限期地生成格式正确的错误数据,而整个过程中没有任何环节会抛出异常。

**保存已解析的 HTML。**当选择器失效时,将旧标记与新标记进行对比,几秒钟内就能准确查明发生了哪些变化。为了排查问题而重新抓取页面不仅速度更慢、消耗带宽,还可能返回的页面与实际出错的页面不同。

CSS 目前仍无法实现的功能

以下是一份简短清单,其中的每一项都是选择 XPath 的正当理由。

根据文本内容进行匹配。 目前尚无标准的 :contains() 属性。该属性曾被提议但最终被放弃,且没有任何浏览器实现它。这是最大的功能缺口,也是“根据标签查找值”这种模式属于 XPath 领域的原因——//dt[normalize-space()='Price']/following-sibling::dd[1] 在 CSS 中没有等效实现。

导航到任意祖先元素。 :has() 提供了条件父元素选择功能——“包含此 span 的 div”——但缺乏通用的祖先轴。若要向上遍历数层以定位特定容器,则必须使用 XPath。

字符串函数。 没有截取、没有子字符串、没有大小写转换、没有字符串拼接。无论匹配到什么,都需要在宿主语言中进行后续处理。

查询带命名空间的 XML。 CSS 几乎不支持命名空间。对于网站地图、RSS 或 SOAP,请使用 XPath。

直接选择前一个同级元素。 :has() 实现了该效果——h2:has(+ p) 可选中标题——但没有 - 组合子来对应 +。

以上便是实际应用中的完整清单。人们以往依赖 XPath 完成的其他所有操作,如今 CSS 均可实现,且通常更易于阅读。

值得检查的环境差异

最常见的选择器意外情况是,某项功能存在于你当前运行环境之外的其他地方。

浏览器支持此处列出的所有内容,包括 :has()、:is()、:where() 以及多参数的 :not()。

服务器端的解析器差异很大。 Python 的 cssselect(lxml 和 Scrapy 均使用该库)会将 CSS 转换为 XPath——对于经典选择器效果良好,但对较新的选择器效果较差。特别是对 :has() 的支持并不普遍,一个在浏览器控制台中能正常工作的选择器,在解析器中可能会引发错误或无声地返回空结果。

BeautifulSoup 在其 select() 方法中使用 soupsieve,该方法的覆盖范围虽好但并非完全一致,且完全不支持 XPath。

Playwright 和 Selenium 直接使用浏览器的引擎,因此其支持情况与浏览器一致——此外,Playwright 还提供了文本定位器和角色定位器,这些定位器覆盖了 CSS 无法表达的大部分内容。

实际操作建议:在实际运行的环境中测试选择器。虽然浏览器控制台使用起来很方便,但它测试的是基于 JavaScript 处理后的 DOM 的不同实现,这可能从两个方面误导你。

大家还问

.card.featured 和 .card .featured 有什么区别?

前者(无空格)匹配同时具有这两个类的元素;后者(带空格)则匹配位于具有类名 card 的元素内部、且具有类名 featured 的元素。一个空格就完全改变了匹配的含义,这也是 CSS 选择器中最常见的错误。

如何在 CSS 中选择父元素?

使用 :has() — div:has(> span.price) 会选择 div 而不是 span。该选择器自 2023 年 12 月起已在各大浏览器中广泛支持。由于没有通用的祖先选择器,因此要定位几层之上的特定容器,仍需使用 XPath。

CSS 选择器能否匹配文本?

不能。没有标准的 :contains() 伪类;该伪类曾被提议但从未被采纳。要根据文本内容进行匹配,需要使用 XPath,或者使用特定库的定位器,例如 Playwright 的 getByText()。

:nth-child 和 :nth-of-type 有什么区别?

:nth-child 会统计父元素的所有子元素;:nth-of-type 则仅统计同类型的元素。因此,如果标题出现在最前面,p:first-child 将无法匹配任何内容,而 p:first-of-type 则无论如何都会匹配第一个段落。

:is() 起什么作用?它与 :where() 有什么区别?

两者都接受一个选择器列表,并匹配其中任意一个,从而简化了重复的选择器。唯一的区别在于特异性::is() 采用其最特异的参数的特异性,而 :where() 始终贡献零特异性——这使得 :where() 非常适合那些应易于覆盖的默认样式。

CSS 特异性是如何工作的?

就各个组件而言,ID 的权重高于类、属性和伪类,而类、属性和伪类的权重又高于元素和伪元素选择器。内联样式优于选择器,而 !important 又优于内联样式。但当你使用选择器进行查询而非样式设置时,上述规则均不适用。

用于数据抓取时,CSS 选择器是否优于 XPath?

对于类、ID、属性及结构化选择,答案是肯定的——CSS 选择器更简洁、更清晰。但若需匹配文本内容或向上遍历至任意祖先元素,则必须使用 XPath。大多数爬虫库同时支持这两种方式,根据具体情况灵活选用是常规做法。

为什么我的选择器在浏览器中有效,但在爬虫中却失效?

通常有两个原因。浏览器中的 DOM 是经过 JavaScript 处理后的结果,而你的解析器处理的是原始 HTML,因此该元素可能并不存在于你获取的数据中。此外,服务器端的 CSS 实现并不支持所有现代选择器——特别是 :has(),在浏览器之外的支持情况并不一致。

总结

CSS 选择器的核心内容其实很简单:用于标识的 type、class 和 id,用于关系的四个组合符,用于有针对性地选择元素的属性选择器,以及用于位置和状态的少数几个伪类。

与掌握额外的语法相比,以下三个习惯对优化实际选择器更为有效。 当关系确实直接时,请优先使用 > 而不是单纯的空格,因为匹配范围较窄的选择器比匹配范围过广的选择器更有用。请优先使用 data-* 属性而非类名,因为类名由构建工具自动生成,而属性则由人手动选择。当你指的是“第二个段落”而非“第二个子元素”时,请优先使用 :nth-of-type 而不是 :nth-child。

:has() 这一补充值得刻意采用。纯CSS中的父元素选择和同级条件已经消除了此前迫使人们使用XPath的大部分场景,而且该功能在浏览器中已广泛可用足够长的时间,足以让人信赖。在浏览器之外,请先检查你的解析器——那里才是剩余意外情况的来源。