bigywater.tech
爬虫工程 · 47

XPath 与 CSS 选择器:表达能力与耗时

写爬虫时用 XPath 还是 CSS 选择器常常是习惯之争。这篇用同一个页面量一量:各自能表达什么、哪些写法有陷阱(尤其 class 匹配和 //a[1])、cssselect 把 CSS 翻译成什么,以及耗时到底差多少。

13 / 16
16 个选取任务中 CSS 选择器能写出来的个数(XPath 全部能写)
1 / 4 / 3
XPath 用 @class="item"、contains(@class,"item")、带空格的 contains,与 CSS li.item 的 3 个对比选出的条数
2.8–3.1 ms
5000 个 li 的页面上,XPath 与 CSS(字符串或预编译)每次执行的耗时

表达能力

实测 · 本机执行输出
  任务                          CSS                     XPath   结果条数 CSS / XPath
  所有商品 li                 li.item                 ✓       3 / 3
  同时有 item 和 sale 两个 classli.item.sale            ✓       2 / 2
  data-id 为 2 的 li        li[data-id="2"]         ✓       1 / 1
  href 以 /p/ 开头的链接        a[href^="/p/"]          ✓       3 / 3
  href 含 other.test 的链接   a[href*="other.test"]   ✓       1 / 1
  li 的直接子元素 a             li > a                  ✓       6 / 6
  a 的下一个兄弟 span           a + span                ✓       4 / 4
  每个 ul 的第 2 个 li         ul > li:nth-child(2)    ✓       2 / 2
  不含 sale 的商品             li.item:not(.sale)      ✓       1 / 1
  价格文字(span.price 的文本)    span.price              ✓       4 / 4
  链接的 href 属性值            a[href]                 ✓       6 / 6
  文字是 "Two" 的链接           (CSS 表达不了)              ✓       — / 1
  包含 a 的 li 的父元素 ul       (CSS 表达不了)              ✓       — / 2
  有 sale 商品的 div(向上找祖先)   (CSS 表达不了)              ✓       — / 1
  最后一个 li                 li:last-child           ✓       2 / 2
  两种元素的并集 h1 和 p.note     h1, p.note              ✓       3 / 3
  CSS 能表达的 13 个任务,CSS 表达不了的 3 个(按文字内容选、选父元素、选祖先)

CSS 能写 16 个任务中的 13 个;写不了的是按文字内容选、选父元素和选祖先——这些要用 XPath(//a[text()="Two"]、/parent::ul、/ancestor::div[1])。反过来,XPath 里匹配 class 很啰嗦,CSS 的 li.item 更短也更不容易写错。

class 的陷阱

实测 · 本机执行输出
  CSS li.item                                      → 3 个(按空白分隔的"词"匹配,itemized 不算)
  XPath //li[@class="item"]                         → 1 个(整个属性值必须恰好等于 item,漏掉 "item sale")
  XPath //li[contains(@class, "item")]              → 4 个(子串匹配,把 "itemized" 也选进来了)
  XPath //li[contains(concat(" ", normalize-space(@class), " "), " item ")] → 3 个(和 CSS 一致,但写起来很长)

CSS 的 .item 是“按空白分隔的词”匹配。XPath 的 @class="item" 要求整个属性值恰好等于 item(漏掉 item sale),contains(@class, "item") 是子串匹配(多选 itemized),正确写法是 contains(concat(" ", normalize-space(@class), " "), " item ")。

位置

实测 · 本机执行输出
  //a[1]    → 6 个(每个父元素下的第一个 a)
  (//a)[1]  → 1 个(整个文档的第一个 a)
  CSS a:nth-of-type(1) → 6 个(和 //a[1] 一样,每个父元素下的第一个 a)
  XPath 的下标从 1 开始://li[1] 选出 ['One¥10'],//li[0] → 0 个

//a[1] 是“每个父元素下的第一个 a”,整个文档的第一个要写成 (//a)[1];XPath 下标从 1 开始。

CSS 其实是被翻译成 XPath 的

实测 · 本机执行输出
  li.item               → descendant-or-self::li[@class and contains(@class, 'item') and contains(concat(' ', normalize-space(@class), ' '), ' item ')]
  li.item.sale          → descendant-or-self::li[(@class and contains(@class, 'item') and contains(concat(' ', normalize-space(@class), ' '), ' item ')) and (@class and contains(@class, 'sale') and contains(concat(' ', normalize-space(@class), ' '), ' sale '))]
  a[href^="/p/"]        → descendant-or-self::a[@href and starts-with(@href, '/p/')]
  ul > li:nth-child(2)  → descendant-or-self::ul/li[count(preceding-sibling::*) = 1]
  li.item:not(.sale)    → descendant-or-self::li[(@class and contains(@class, 'item') and contains(concat(' ', normalize-space(@class), ' '), ' item ')) and (not(@class and contains(@class, 'sale') and contains(concat(' ', normalize-space(@class), ' '), ' sale ')))]
  a + span              → descendant-or-self::a/following-sibling::*[(self::span) and (position() = 1)]
  p.note                → descendant-or-self::p[@class and contains(@class, 'note') and contains(concat(' ', normalize-space(@class), ' '), ' note ')]

lxml 的 cssselect 把 CSS 翻译成 XPath 再执行,li.item 展开成带两层 contains 的长表达式,a + span 展开成 following-sibling 加 position() = 1。结果等价,多一层翻译。

耗时

实测 · 本机执行输出
  方式                                            每次耗时
  XPath 字符串 tree.xpath(…)(每次调用)           3.06 ms
  XPath 预编译对象 etree.XPath(…)              2.93 ms
  CSS 字符串 tree.cssselect(…)(每次调用)         2.77 ms
  CSS 预编译对象 CSSSelector(…)                2.84 ms
  XPath 精确匹配一个属性(预编译)                     1.29 ms
  CSS 精确匹配一个属性(预编译)                       1.10 ms
  编译一个 CSSSelector 对象的耗时:17 微秒;编译一个 etree.XPath 对象:3 微秒

5000 个 li 的页面上 XPath 和 CSS 的每次执行耗时都在 2.7–3.1 ms,预编译与否差别不到 10%,编译一个选择器只要几到十几微秒。所以选哪一种应由正确性和可读性决定,不是速度。

分步演示

XPath 与 CSS
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。lxml 6.1.1 的 lxml.html 与 cssselect 1.6.0(pip 装进实验专用目录,实验后删除);页面是手写的小页面(任务对比)和 5000 个 li 的生成页面(耗时);耗时每项重复 100 次取平均,编译耗时重复 2000 次;条数一致的判断是直接比较两种写法选出的元素个数
    • 没有验证:更复杂页面上的性能(深层嵌套、大量属性选择器);parsel 的 css/xpath 封装(没有测);XPath 2.0/3.1 与 lxml 不支持的函数;CSS 伪类 :has()、:is() 在 cssselect 里的支持;不同解析器(html5lib、Lexbor)下选择器的行为(selectolax 自带的 CSS 引擎没有测速)
    京ICP备2026067502号-1京公网安备11010502063487号