XPath 教程:爬虫如何用 XPath 定位网页元素
XPath 是爬虫里精准定位网页元素的核心技能,本文从节点选取、谓语、轴运算到 Python lxml 实战,全面讲解 XPath 语法与避坑指南。
为什么爬虫要学 XPath?
CSS 选择器你会了,但遇到这类情况就不够用了:
- 要选"父节点下第二个
li里的链接" - 要选"所有带
href属性且包含/detail/的a标签" - 要选"当前节点的爷爷节点里的
span文本"
这些用 CSS 选择器能做到吗?能,但会很绕,甚至做不到。
XPath 就是来解决这个问题的——它能精准定位 DOM 树里任意位置的元素,不依赖层级关系的 CSS 类名,而是用路径表达式一层层找过去。
CSS 选择器是"猜",XPath 是"指"。
学完这篇,你能做到:
- 用路径表达式选任意节点
- 用谓语精确找到第 N 个元素或特定属性
- 用轴(axis)向上找父节点、向下找子孙节点
- 在 Python 的 lxml 库里写 XPath 表达式
XPath 是什么?和 CSS 选择器有什么区别?
XPath 全称是 XML Path Language,最初是用来在 XML 文档里定位节点的语言。 后来被广泛引入了 HTML 解析领域,因为它本质上是一套"在树状结构里找节点"的语法。
简单对比:
| CSS 选择器 | XPath | |
|---|---|---|
| 起源 | 为样式设计 | 为 XML 解析设计 |
| 选父节点 | ul > li |
//li/parent::ul |
| 选第 N 个 | :nth-child(2) |
//li[2] |
| 选属性 | a[href] |
//a[@href] |
| 选文本 | 不直接支持 | //span/text() |
| 选祖先 | 不支持 | //span/ancestor::div |
| 上手难度 | 低 | 中 |
CSS 选择器适合简单快速的定位,XPath 适合复杂精准的定位。 两者不是替代关系,而是互补关系——爬虫老手通常两个都会,需要哪个用哪个。
XPath 基础语法:节点选取怎么写?
XPath 用"路径表达式"来描述节点位置,就像文件系统里的路径。
最基本的五种表达式
| 表达式 | 含义 | 示例 |
|---|---|---|
nodename |
选取该名字的所有子节点 | div → 所有 div |
/ |
从根节点选取(绝对路径) | /html/body |
// |
从匹配位置的当前节点往下选(相对路径) | //div//a |
. |
当前节点 | |
.. |
当前节点的父节点 | |
@ |
选取属性 | @href |
看一个 HTML 结构:
<html>
<body>
<div class="container">
<ul class="movie-list">
<li class="movie-item">
<a href="/movie/1">电影标题 A</a>
</li>
<li class="movie-item">
<a href="/movie/2">电影标题 B</a>
</li>
</ul>
</div>
</body>
</html>
对应 XPath:
//ul → 选取所有 ul
//body/div → 选取 body 下的 div
//div//a → 选取 div 内任意层级的所有 a
//a/@href → 选取所有 a 标签的 href 属性值
//li[1] → 选取第一个 li
谓语怎么用?如何精确找到第 N 个元素或特定属性?
谓语就是写在方括号里的条件 [条件],可以精确筛选节点。
按索引定位:第一个、第二个、最后一个
//li[1] → 第一个 li(XPath 从 1 开始,不是 0)
//li[last()] → 最后一个 li
//li[last()-1] → 倒数第二个 li
//li[position() < 3] → 前两个 li
按属性值定位:class、href、id
//div[@class="container"] → class 等于 container 的 div
//a[@href="/movie/1"] → href 等于指定值的 a
//div[@id] → 有 id 属性的所有 div
//div[not(@id)] → 没有 id 属性的所有 div
按文本内容定位
//span[text()="下一页"] → 文本等于"下一页"的 span
//span[contains(text(),"价格")] → 文本包含"价格"的 span
//a[contains(@href,"/detail")] → href 包含 "/detail" 的 a
组合条件:多个谓语叠加
//li[@class="movie-item"][1] → class 为 movie-item 的第一个 li
//div[@id="main"]//a[@href][2] → id 为 main 的 div 下,第二个带 href 的 a
XPath 轴怎么用?父子兄弟节点定位
轴(Axis)是 XPath 独有的能力——可以往当前节点的上方或下方任意方向查找,这是 CSS 选择器做不到的。
语法://轴::节点测试[谓语]
常用轴
| 轴 | 含义 | 示例 |
|---|---|---|
parent:: |
父节点 | //span/parent::div |
child:: |
子节点(默认,可省略) | //div/child::a |
descendant:: |
所有后代节点 | //div/descendant::span |
ancestor:: |
所有祖先节点 | //span/ancestor::div |
following-sibling:: |
后续兄弟节点 | //h2/following-sibling::p |
preceding-sibling:: |
前置兄弟节点 | //td/preceding-sibling::th |
self:: |
当前节点自身 | //span[self::span[@class="price"]] |
实战例子
找到当前节点的父节点:
//span[@class="price"]/parent::div
找到所有兄弟节点:
//li[@class="active"]/following-sibling::li
找到爷爷节点里的某个属性:
//span/ancestor::div[@id="article"]//h1
选中当前节点的文本再往父节点走:
//text()[.="登录"]/parent::button
Python 爬虫里怎么用 XPath?lxml + etree 示例
Python 里用 XPath,主要通过 lxml 库解析 HTML 并提取数据:
from lxml import etree
html = """
<html>
<body>
<div class="movie-list">
<li class="movie">
<span class="title">肖申克的救赎</span>
<span class="score">9.7</span>
</li>
<li class="movie">
<span class="title">霸王别姬</span>
<span class="score">9.6</span>
</li>
</div>
</body>
</html>
"""
selector = etree.HTML(html)
# 选所有电影标题
titles = selector.xpath('//span[@class="title"]/text()')
print(titles) # ['肖申克的救赎', '霸王别姬']
# 选第一部电影的分数
score = selector.xpath('//li[1]//span[@class="score"]/text()')
print(score) # ['9.7']
# 选包含"救赎"的标题
title = selector.xpath('//span[contains(text(),"救赎")]/text()')
print(title) # ['肖申克的救赎']
# 获取 href 属性
links = selector.xpath('//a/@href')
print(links) # ['/movie/1', '/movie/2']
# 选中所有 a 的文本和对应 href
for a in selector.xpath('//a'):
text = a.xpath('text()')[0] if a.xpath('text()') else ''
href = a.xpath('@href')[0] if a.xpath('@href') else ''
print(text, href)
常用 lxml + XPath 方法
| 方法 | 作用 |
|---|---|
selector.xpath('//xpath') |
返回列表,所有匹配结果 |
selector.xpath('//xpath')[0] |
取第一个匹配结果 |
selector.xpath('string(//xpath)') |
取拼接后的完整文本 |
selector.xpath('//xpath/@href') |
取属性值列表 |
selector.xpath('count(//li)') |
统计匹配数量 |
XPath 和 CSS 选择器怎么选?哪个更好用?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 页面结构简单,类名清晰 | CSS 选择器 | 写起来更短,更直观 |
| 需要定位第 N 个元素 | XPath | CSS :nth-child 语法繁琐 |
| 需要取文本内容 | XPath | CSS 无法直接取文本 |
| 需要向上找父节点/祖先 | XPath | CSS 不支持向上查找 |
| 需要定位带特定属性的元素 | 两者皆可 | CSS a[href] 和 XPath //a[@href] 等价 |
| 需要统计节点数量 | XPath | count() 函数比 CSS 方便 |
建议: 先用 CSS 选择器搞定简单的,遇到复杂定位再上 XPath。两种工具配合使用,爬虫老手都是这样干的。
XPath 常见报错与处理
1. 空列表:匹配不到元素
results = selector.xpath('//div[@class="movie"]//a/text()')
# results = [] ← 空列表
检查:
- 类名拼写是否正确(大小写敏感)
- 页面是否是动态加载(需要用 Selenium/Playwright 先生成 HTML)
- 用浏览器开发者工具复制实际 XPath 检查
2. 下标越界
title = selector.xpath('//li[1]//span/text()')[0]
# IndexError: list index out of range
XPath 从 1 开始,不是 0。如果页面结构不确定,先判断:
titles = selector.xpath('//li//span/text()')
if titles:
print(titles[0])
3. 属性名拼错
//img[@src] ✅ 正确
//img[@scr] ❌ 错误(scr 是常见拼写错误)
4. 文本含空格或换行导致匹配失败
<span> 价格 </span>
//span[text()="价格"] ❌ 失败(文本有空格)
//span[contains(text(),"价格")] ✅ 成功
XPath 优化技巧:写得更精准、更稳定
技巧 1:用 @class 代替没有语义的 div
//div/div/div/span ❌ 脆弱,层级一改就失效
//div[@class="movie-card"]//span ✅ 语义稳定,不依赖具体层级
技巧 2:用 contains() 处理动态生成的类名
有些页面类名是动态生成的,比如 class="item-83271":
//li[contains(@class,"item-")]//a → 只要 class 里含 "item-" 就能匹配
技巧 3:用 string() 取完整文本(避免空白节点干扰)
string(//div[@class="content"]) → 拼接所有子节点文本,自动去除空白
text() → 只取当前节点直接文本,可能遗漏子节点内容
技巧 4:用 normalize-space() 处理首尾空格
//span[normalize-space(text())="价格"] → 自动去除首尾空格再匹配
技巧 5:避免使用 @id 做定位(ID 常动态生成)
//div[@id="product-price"] ❌ ID 常动态变化,维护成本高
//span[contains(@class,"price")] ✅ 用类名或属性值组合更稳定
常见问题
XPath 和 CSS 选择器哪个更快?
在 lxml 里,CSS 选择器会被转成 XPath 执行,速度差异可以忽略。选你能写得更准的,而不是更快的。
页面是 JavaScript 动态生成的,能用 XPath 吗?
能,但要先拿到渲染后的 HTML。用 Selenium 或 Playwright 打开页面,等内容加载完成,再提取 HTML 给 lxml 解析:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
page.wait_for_selector(".movie-item") # 等内容加载完成
html = page.content()
browser.close()
from lxml import etree
selector = etree.HTML(html)
titles = selector.xpath('//span[@class="title"]/text()')
为什么 XPath 在浏览器开发者工具里能选中,但 lxml 解析不出来?
浏览器里的 XPath 基于渲染后的 DOM,有可能有隐藏的命名空间(namespace)导致解析失败。解决方法是使用 etree.HTML() 而不是 etree.XML(),或者用 lxml.html.fromstring() 解析:
from lxml import html
selector = html.fromstring(html_source)
XPath 能处理 JSON 数据吗?
XPath 只处理 XML/HTML,不能直接处理 JSON。如果目标数据在 API 返回的 JSON 里,requests + json() 更直接,不需要 XPath。
XPath 中的通配符怎么用?
//* → 选取所有元素
//div/* → 选取 div 下所有直接子元素
//li[@class="item-*"] → class 以 "item-" 开头的所有 li
XPath 支持逻辑运算吗?
支持,用 and、or、not():
//div[@class="main" and @id="content"] → 同时满足两个条件
//div[@class="main" or @class="sidebar"] → 满足任一条件
//div[not(@class="hidden")] → 不包含某类名
总结
XPath 的核心能力就三点:
- 路径表达式 — 精准定位树中任意节点
- 谓语 — 用条件过滤,找到第 N 个或满足特定属性的元素
- 轴 — 往上找父/祖先,往下找子孙,不用受 CSS 只能"向下找"的限制
CSS 选择器和 XPath 不是谁替代谁,而是各有优势。CSS 快而直观,XPath 强而精准。
学完这篇,你应该能应对爬虫里 80% 的元素定位场景。剩下 20% 的极端情况(比如跨 iframe、Shadow DOM),后面再单独研究。
相关知识
如果还没学 CSS 选择器,可以先看 CSS 选择器入门:从改样式到爬虫数据定位,XPath 和 CSS 选择器是互补关系,两者都需要掌握。
想深入理解网页结构,推荐阅读:
FAQ
Q:XPath 从 1 开始数还是从 0 开始?
A:XPath 从 1 开始数,和编程语言的数组下标从 0 开始不同。//li[1] 选的是第一个 li,不是第二个。
Q:CSS 选择器和 XPath 能同时用吗?
A:能。Python 的 lxml 库支持用 CSS 选择器写法,自动转成 XPath 执行。写复杂的节点定位时,XPath 更强大;写简单的样式类名定位时,CSS 选择器更直观。
Q:为什么 //div 能匹配所有 div,但 //div[@class="container"] 却匹配不到?
A:检查页面 HTML 源码里实际的 class 属性值。有些页面类名是动态的,比如 class="container-abc123",需要用 contains() 模糊匹配,而不是精确匹配。
Q:XPath 能跨 iframe 查找元素吗?
A:不能直接跨 iframe。用 XPath 只能查到当前文档树的节点。如果目标元素在 iframe 里,需要先切换到那个 iframe 的 document,再继续用 XPath 查找。
Q:XPath 的 // 和 / 有什么区别?
A:/ 是从根节点开始的绝对路径,// 是从当前节点开始往下任意层级的相对路径。爬虫里最常用的是 //,因为往往不知道根节点是什么,直接从文档任意位置往下找更实用。
Q:XPath 能统计某个元素出现了多少次吗?
A:能。用 count() 函数:selector.xpath('count(//div[@class="item"]'),返回匹配到的 div 数量(浮点数),转为整数的用法是 int(selector.xpath('count(//li)')。
Practice