4、元素定位进阶:XPath 语法详解、CSS Selector 实战、相对路径与绝对路径选择策略

说实话,很多新手爬虫工程师最容易栽跟头的地方,就是元素定位。

你写了个看似完美的脚本,跑起来却总是报错「找不到元素」。为什么?说白了,就是定位策略没选对。今天这一章,我把压箱底的经验都掏出来,咱们把 XPath、CSS Selector 和路径选择策略彻底讲透。

4.1 XPath 语法详解:从入门到精通

XPath 的全称是 XML Path Language。别看名字带 XML,它在 HTML 文档里同样好用。我个人习惯,遇到复杂页面结构时,首选 XPath。

4.1.1 基础节点选择

先看几个最常用的表达式:

//div          —— 选择所有 div 元素
/html/body/div —— 从根节点开始的绝对路径
//div[@id='content'] —— 选择 id 为 content 的 div
//a[@class='link']   —— 选择 class 为 link 的链接

我在项目中遇到过一个问题:用 //div[@class='item'] 死活选不到元素。后来发现,那个 class 后面还跟了个空格和另一个类名。嗯,这里要注意,XPath 的 @class 匹配的是完整属性值。

避坑指南: 我曾经因为 class 属性里有多个类名,用 @class='item' 匹配失败,浪费了整整一下午。后来改用 contains(@class, 'item') 才搞定。

4.1.2 常用函数与操作符

XPath 的强大之处在于它的函数库。你想想看,如果页面结构稍微变一下,你的定位器就失效了,那多尴尬。用函数就能解决这个问题。

函数/操作符 示例 说明
contains() //div[contains(@class, 'btn')] 属性值包含指定字符串
text() //span[text()='确认'] 匹配文本内容
starts-with() //input[starts-with(@id, 'user_')] 属性值以指定字符串开头
and / or //div[@class='a' and @id='b'] 多条件组合
position() //ul/li[position()=3] 选择第几个子元素
小技巧: 我建议你在 Chrome 开发者工具的 Console 里用 $x('//表达式') 快速测试 XPath,比反复运行脚本快多了。

4.1.3 轴(Axes)的使用

轴是 XPath 的高级特性。说白了,就是让你在 DOM 树里「上下左右」随便走。

//div[@id='main']/following-sibling::div  —— 后面的兄弟节点
//div[@id='main']/preceding-sibling::div  —— 前面的兄弟节点
//div[@id='main']/ancestor::div           —— 所有祖先节点
//div[@id='main']/descendant::span        —— 所有后代 span

我记得有一次,目标网站把商品价格放在了一个很深的嵌套结构里,用普通路径根本定位不到。后来我用 //span[contains(text(), '¥')]/ancestor::div[@class='item'],一下子就拿到了整个商品容器。这就是轴的威力。

4.2 CSS Selector 实战:又快又稳

CSS Selector 的语法比 XPath 简洁,执行速度也更快。我个人在写 Selenium 脚本时,如果页面结构不复杂,会优先用 CSS Selector。

4.2.1 基础选择器

div.item          —— 类选择器,等价于 XPath 的 //div[@class='item']
#content          —— ID 选择器,等价于 //*[@id='content']
div > p           —— 子元素选择器
div p             —— 后代元素选择器
input[type='text'] —— 属性选择器

4.2.2 高级用法

CSS Selector 也有一些很实用的「黑科技」:

ul li:nth-child(3)       —— 第三个 li
ul li:first-child        —— 第一个 li
ul li:last-child         —— 最后一个 li
a[href^='https']         —— href 以 https 开头
a[href$='.pdf']          —— href 以 .pdf 结尾
div:not(.hidden)         —— 不包含 hidden 类的 div
实战经验: 我在爬取某个电商网站时,发现商品列表里混入了广告位。用 div.product:not(.ad) 就完美过滤掉了。你想想看,如果不用 :not(),你得写多复杂的 XPath?

4.3 相对路径与绝对路径选择策略

这是很多新手最容易纠结的地方。我直接说结论:能用相对路径,就别用绝对路径

4.3.1 绝对路径的坑

绝对路径长这样:/html/body/div[2]/div[3]/div[1]/span

为什么我不推荐?因为只要页面结构稍微调整一下——比如加了个广告位、改了个布局——你的路径就全废了。我曾经接手过一个项目,里面全是绝对路径,每次网站改版都要重写一半的定位器,那叫一个痛苦。

4.3.2 相对路径的优势

相对路径的核心思想是:找到一个稳定的锚点,然后基于它去定位。

// 好的做法:基于 ID 或唯一属性
//div[@id='product-list']//span[@class='price']

// 更好的做法:结合文本内容
//div[contains(@class, 'item')]//button[text()='加入购物车']

4.3.3 我的选择策略

我一般按这个优先级来选:

  1. 优先用 ID —— 如果元素有唯一的 id,直接用 #id//*[@id='id']
  2. 其次用唯一属性 —— 比如 data-* 属性、name 属性
  3. 再次用文本内容 —— 用 text()contains(text())
  4. 最后用层级关系 —— 结合稳定的父元素,用相对路径往下找
避坑指南: 我曾经用 //div[1] 这种索引定位,结果网站上线 A/B 测试,两个版本的页面结构不一样,索引全乱了。从那以后,我再也不依赖索引了。

4.4 实战对比:XPath vs CSS Selector

到底用哪个?我直接给你一个对比表:

场景 推荐方案 原因
根据文本内容定位 XPath CSS Selector 不支持文本匹配
根据属性值部分匹配 XPath contains() 比 CSS 的 *= 更灵活
简单层级定位 CSS Selector 语法简洁,执行速度快
复杂 DOM 遍历 XPath 轴(Axes)功能无可替代
动态 ID 或 class 两者皆可 contains*= 匹配部分值
我的建议: 别纠结「哪个更好」。两个都学,哪个顺手用哪个。我自己的习惯是:简单场景用 CSS,复杂场景用 XPath。遇到特别难搞的页面,两个一起上,互相验证。

好了,这一章的内容就到这。记住一句话:定位器的稳定性,决定了爬虫的寿命。下一章咱们聊聊「动态加载数据的捕获与处理」,到时候会用到今天讲的这些定位技巧,别掉队。