XPath 教程:爬虫如何用 XPath 定位网页元素

XPath 是爬虫里精准定位网页元素的核心技能,本文从节点选取、谓语、轴运算到 Python lxml 实战,全面讲解 XPath 语法与避坑指南。

7 分钟阅读 xpathlxmlpython-crawlerdata-extraction

为什么爬虫要学 XPath?

CSS 选择器你会了,但遇到这类情况就不够用了:

  • 要选"父节点下第二个 li 里的链接"
  • 要选"所有带 href 属性且包含 /detail/a 标签"
  • 要选"当前节点的爷爷节点里的 span 文本"

这些用 CSS 选择器能做到吗?能,但会很绕,甚至做不到。

XPath 就是来解决这个问题的——它能精准定位 DOM 树里任意位置的元素,不依赖层级关系的 CSS 类名,而是用路径表达式一层层找过去。

CSS 选择器是"猜",XPath 是"指"。

学完这篇,你能做到:

  1. 用路径表达式选任意节点
  2. 用谓语精确找到第 N 个元素或特定属性
  3. 用轴(axis)向上找父节点、向下找子孙节点
  4. 在 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 = []  ← 空列表

检查:

  1. 类名拼写是否正确(大小写敏感)
  2. 页面是否是动态加载(需要用 Selenium/Playwright 先生成 HTML)
  3. 用浏览器开发者工具复制实际 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 支持逻辑运算吗?

支持,用 andornot()

//div[@class="main" and @id="content"]   → 同时满足两个条件
//div[@class="main" or @class="sidebar"] → 满足任一条件
//div[not(@class="hidden")]               → 不包含某类名

总结

XPath 的核心能力就三点:

  1. 路径表达式 — 精准定位树中任意节点
  2. 谓语 — 用条件过滤,找到第 N 个或满足特定属性的元素
  3. — 往上找父/祖先,往下找子孙,不用受 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

读完这一节,去靶场里验证一下。

去挑战广场练习