Python爬虫教程:XPath解析技术详解

爬取网页数据时,HTML 结构千变万化让人头疼?正则表达式太脆弱,标签缩进或顺序稍微一变就崩盘?别急,XPath 绝对是你正在寻找的那把「网页导航精准手术刀」。本篇就带你用 Python + lxml 快速掌握核心用法,让数据提取变得轻松高效!


1. 快速认识 XPath

XPath (XML Path Language) 是一门在文档树中定位节点的路径语言。它最初为 XML 设计,但对 HTML 的解析同样堪称完美。

为什么选 XPath?

  • ✅ 路径式语法,直观易懂,就像操作文件系统一样
  • ✅ 内置丰富的筛选函数,过滤节点随心所欲
  • ✅ 支持向上、向下、同级全方位导航,不放过任何角落
  • ✅ W3C 官方标准,各大主流语言均有成熟的解析库支持

2. 环境准备:Python + lxml

Python 生态中,lxml 是实现 XPath 最流行的库。它底层基于 C 语言,解析速度飞快,且容错能力极强——哪怕遇到不规范的 HTML,也能自动修复成可查询的树结构。

安装依赖

pip install lxml

验证安装成功

from lxml import etree

print(f"lxml XPath解析库版本:{etree.__version__}")

运行后若能正常输出版本号(如 lxml XPath解析库版本:5.3.0),说明环境已经准备就绪。


3. 第一步:把 HTML 变成可查询的「树」

XPath 基于文档树模型工作,所以我们得先把 HTML 字符串或本地文件转换成 lxml 的 ElementTree 对象。

解析 HTML 字符串

from lxml import etree

sample_html = """
<html>
    <body>
        <div class="container">
            <ul>
                <li class="item-0"><a href="link1.html">first item</a></li>
                <li class="item-1"><a href="link2.html">second item</a></li>
                <li class="item-inactive"><a href="link3.html">third item</a></li>
                <li class="item-1"><a href="link4.html">fourth item</a></li>
                <li class="item-0"><a href="link5.html">fifth item</a></li>
            </ul>
        </div>
    </body>
</html>
"""

# 初始化 HTML 解析器(自动修复不规范的 HTML)
html_parser = etree.HTMLParser()
tree = etree.fromstring(sample_html, html_parser)

# 打印修复并格式化后的 HTML,确认结构
print(etree.tostring(tree, pretty_print=True, method="html").decode("utf-8"))

从本地 HTML 文件加载

# 解析本地的 test.html 文件
tree = etree.parse("test.html", etree.HTMLParser())

两种方式得到的 tree 对象用法完全一致,接下来我们重点玩转它。


4. 核心节点选择:路径、属性、文本

拿到 tree 对象后,调用它的 .xpath() 方法并传入表达式,就能轻松查询。返回结果通常是匹配节点组成的列表,或者属性值、文本的列表。

基本路径符号速查

语法符号作用示例含义
/从根节点开始,或选取直接子节点tree.xpath('/html/body/div/ul')选中文档唯一的 <ul>
//从文档任意位置,选取所有子孙节点tree.xpath('//li')选中全部 5 个 <li>
*通配符,匹配任意节点名tree.xpath('//li/*')选中所有 <li> 的直接子节点(即 <a>
.当前节点(常用于循环内的相对定位)见下文实战案例
..父节点tree.xpath('//a[@href="link4.html"]/../@class')返回 ['item-1']

属性筛选与获取

XPath 使用 [@属性名="值"] 这种谓语(放在方括号里的筛选条件)来精确匹配:

# 筛选 class 属性等于 "item-0" 的所有 li
filtered_li = tree.xpath('//li[@class="item-0"]')
print(len(filtered_li))   # 输出:2

# 多条件组合:使用 and / or 连接
multi_attr_li = tree.xpath('//li[contains(@class, "item") and position()<3]')
# contains():模糊匹配属性值,对多值 class(如 "item active")尤其好用
# position():返回节点在兄弟中的位置

# 直接获取属性值:在路径后追加 /@属性名
all_hrefs = tree.xpath('//li/a/@href')
print(all_hrefs)   # ['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']

提取文本内容

  • /text():仅获取直接子节点的纯文本
  • //text():递归获取所有子孙节点的文本(可能包含换行和多余空白)
# 获取所有 a 标签的直接文本
a_texts = tree.xpath('//li/a/text()')
print(a_texts)   # ['first item', 'second item', 'third item', 'fourth item', 'fifth item']

小贴士:使用 //text() 时要留意提取到的内容可能超出预期,建议先用 /text() 保证精确,必要时再用 Python 的 .strip() 清理。


5. 进阶技巧:按序选择与轴导航

按序选择节点

XPath 内置了几个非常实用的位置函数,需要注意的是位置编号从 1 开始,而非编程中常见的 0。

# 第一个 li
first_li = tree.xpath('//li[1]/a/text()')
print(first_li)   # ['first item']

# 最后一个 li
last_li = tree.xpath('//li[last()]/a/text()')
print(last_li)    # ['fifth item']

# 倒数第三个
last_3rd = tree.xpath('//li[last()-2]/a/text()')
print(last_3rd)   # ['third item']

轴选择:全方位导航

轴 (Axis) 定义了当前节点与目标节点之间的空间关系,让你能灵活穿梭于复杂的文档结构。以下是常用轴一览:

轴名作用
ancestor::选取当前节点的所有祖先节点(父、祖父...直到根)
attribute::选取当前节点的所有属性(通常简写为 @*
child::选取当前节点的直接子节点(通常简写为 /
descendant::选取当前节点的所有子孙节点(通常简写为 //
following-sibling::选取当前节点之后的所有同级节点
preceding-sibling::选取当前节点之前的所有同级节点

举个例子:想获取当前 <li> 后面紧跟着的那个兄弟 <li>,就可以用 following-sibling::li[1]。这在处理表格或列表数据时非常方便。


6. 实战小案例:电商商品列表提取

当需要循环处理多个同类节点时,务必使用相对路径(以 .// 开头)。否则每次都会从整个文档根节点重新搜索,不仅效率低下,还容易因为上下文错乱而取到错误数据。

from lxml import etree

product_html = """
<div class="product-list">
    <div class="product">
        <h3><a href="/product/1">复古蓝牙音箱</a></h3>
        <span class="price">¥199.00</span>
        <span class="sales">已售2300件</span>
    </div>
    <div class="product">
        <h3><a href="/product/2">机械键盘青轴</a></h3>
        <span class="price">¥349.00</span>
        <span class="sales">已售8700件</span>
    </div>
</div>
"""

tree = etree.fromstring(product_html)
products = []

for p_node in tree.xpath('//div[@class="product"]'):
    # 关键!使用相对路径 .// 从当前 p_node 开始查找
    name = p_node.xpath('.//h3/a/text()')[0]
    price = p_node.xpath('.//span[@class="price"]/text()')[0]
    sales = p_node.xpath('.//span[@class="sales"]/text()')[0]
    products.append({
        "name": name,
        "price": price,
        "sales": sales
    })

print(products)

输出结果:

[
    {'name': '复古蓝牙音箱', 'price': '¥199.00', 'sales': '已售2300件'},
    {'name': '机械键盘青轴', 'price': '¥349.00', 'sales': '已售8700件'}
]

7. 避坑指南与小工具

常见问题排查

  • 编码乱码:爬取网页后,先用 response.encoding = response.apparent_encoding 自动检测正确编码,再传递给 lxml 解析。
  • 动态内容抓不到:XPath 只能解析服务器返回的初始 HTML。如果数据是通过 JavaScript 动态生成的,需要配合 Selenium、Playwright 等工具,或者直接分析 Ajax 接口。
  • 表达式在代码中不生效:把表达式粘贴到浏览器的控制台,用 $x('//你的表达式') 实时测试,快速定位问题。

性能优化小建议

  • 尽量用具体标签 + 属性来定位,避免滥用 //* 全局通配。
  • 从有唯一标识的节点(如 id="header")向下一层层查找,减少全局扫描。
  • 循环体内务必使用.// 开头的相对路径

8. 扩展学习资源


这篇教程覆盖了 XPath 在 Python 爬虫中的核心场景和高频用法,赶紧找个小网站动手试试吧!如果在实践中遇到什么问题,欢迎在评论区留言交流~