Python爬虫实战⑦|XPath精准定位,复杂网页数据提取利器
·
author: 专注Python实战,分享爬虫与数据分析干货
title: Python爬虫实战⑦|XPath精准定位,复杂网页数据提取利器
update: 2026-04-26
tags: Python,爬虫,XPath,lxml,数据提取,精准定位
作者:专注Python实战,分享爬虫与数据分析干货
更新时间:2026年4月
适合人群:已掌握BS4、想用XPath提升提取效率的爬虫开发者
前言:CSS选择器不够用?试试XPath
前面我们用BeautifulSoup的find/find_all/select提取数据,很好用。但遇到这些情况就吃力了——
- 提取"某个div下面第二个p标签的文本"
- 选择"包含特定文字的标签"
- 获取"父标签的属性"
- 按位置、按条件组合筛选
XPath = XML Path Language,比CSS选择器强大10倍的数据定位语言。 在lxml库的加持下,速度还比BS4快5-10倍。
一、lxml安装与基础
1.1 安装lxml
pip install lxml -i https://pypi.tuna.tsinghua.edu.cn/simple
1.2 创建XPath解析对象
from lxml import etree
# 方式1:从字符串创建
html = """
<div class="container">
<h1 id="title">文章标题</h1>
<ul class="list">
<li class="item">项目1</li>
<li class="item active">项目2</li>
<li class="item">项目3</li>
</ul>
<p class="desc">描述文本 <a href="/link">链接</a></p>
</div>
"""
# etree.HTML() 自动修复不规范的HTML
tree = etree.HTML(html)
# 方式2:从文件创建
# tree = etree.parse("page.html")
# 方式3:从requests响应创建
import requests
response = requests.get("https://example.com")
tree = etree.HTML(response.text)
二、XPath语法详解
2.1 基础路径表达式
| 表达式 | 说明 | 示例 |
|---|---|---|
| / | 从根节点选取 | /html/body/div |
| // | 从任意位置选取 | //div |
| . | 当前节点 | ./p |
| … | 父节点 | …/div |
| @ | 选取属性 | //a/@href |
| * | 任意元素 | //div/* |
| @* | 任意属性 | //a/@* |
2.2 路径选择实战
from lxml import etree
html = """
<html>
<body>
<div id="main">
<h1>标题</h1>
<ul>
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
<a href="/page1" class="link">链接1</a>
<a href="/page2">链接2</a>
</div>
</body>
</html>
"""
tree = etree.HTML(html)
# 绝对路径
h1 = tree.xpath("/html/body/div/h1/text()")
print("绝对路径:", h1) # ['标题']
# 相对路径(从任意位置找)
lis = tree.xpath("//li/text()")
print("所有li:", lis) # ['项目1', '项目2', '项目3']
# 获取属性
hrefs = tree.xpath("//a/@href")
print("所有链接:", hrefs) # ['/page1', '/page2']
# 获取class属性
classes = tree.xpath("//a/@class")
print("a的class:", classes) # ['link']
2.3 谓语(条件筛选)
谓语用方括号 [] 表示,用于添加筛选条件:
from lxml import etree
html = """
<div>
<ul id="nav">
<li class="item">首页</li>
<li class="item active">产品</li>
<li class="item">关于</li>
<li class="item last">联系</li>
</ul>
<div class="content">
<p>段落1</p>
<p>段落2</p>
<p>段落3</p>
</div>
<a href="/link1">链接1</a>
<a href="/link2">链接2</a>
<a href="/link3" target="_blank">链接3</a>
</div>
"""
tree = etree.HTML(html)
# 按索引选择(XPath索引从1开始!)
first_li = tree.xpath("//li[1]/text()")
print("第1个li:", first_li) # ['首页']
last_li = tree.xpath("//li[last()]/text()")
print("最后1个li:", last_li) # ['联系']
second_li = tree.xpath("//li[2]/text()")
print("第2个li:", second_li) # ['产品']
# 倒数第2个
second_last = tree.xpath("//li[last()-1]/text()")
print("倒数第2个li:", second_last) # ['关于']
# 前2个
first_two = tree.xpath("//li[position()<=2]/text()")
print("前2个li:", first_two) # ['首页', '产品']
# 按属性选择
active = tree.xpath("//li[@class='item active']/text()")
print("active项:", active) # ['产品']
# 有target属性的a标签
blank_links = tree.xpath("//a[@target]/@href")
print("新窗口链接:", blank_links) # ['/link3']
# 属性包含某个值(contains)
items_with_item = tree.xpath("//li[contains(@class, 'item')]/text()")
print("含item的li:", items_with_item) # ['首页', '产品', '关于', '联系']
2.4 文本匹配
# 文本包含特定内容
results = tree.xpath("//li[contains(text(), '产')]/text()")
print("包含'产'的li:", results) # ['产品']
# 文本以特定内容开头
results = tree.xpath("//li[starts-with(text(), '首')]/text()")
print("以'首'开头的li:", results) # ['首页']
# 完全匹配文本
results = tree.xpath("//li[text()='关于']/text()")
print("文本是'关于'的li:", results) # ['关于']
三、XPath高级技巧
3.1 逻辑运算
# and — 同时满足多个条件
result = tree.xpath("//li[contains(@class, 'item') and contains(@class, 'active')]/text()")
print("item且active:", result)
# or — 满足任一条件
result = tree.xpath("//li[contains(@class, 'active') or contains(@class, 'last')]/text()")
print("active或last:", result)
# not — 排除
result = tree.xpath("//li[not(contains(@class, 'active'))]/text()")
print("非active的li:", result)
3.2 轴(Axes)——沿关系导航
from lxml import etree
html = """
<div class="container">
<div class="header">
<h1>标题</h1>
<p class="subtitle">副标题</p>
</div>
<div class="body">
<p>段落1</p>
<p>段落2</p>
<p>段落3</p>
</div>
<div class="footer">
<a href="/about">关于</a>
<a href="/contact">联系</a>
</div>
</div>
"""
tree = etree.HTML(html)
# child — 子节点(默认)
children = tree.xpath("//div[@class='header']/child::*")
print(f"header的子标签: {len(children)}个")
# parent — 父节点
parent = tree.xpath("//h1/parent::div/@class")
print(f"h1的父div的class: {parent}")
# following-sibling — 后面的兄弟节点
siblings = tree.xpath("//div[@class='header']/following-sibling::div/@class")
print(f"header后面的兄弟div: {siblings}")
# preceding-sibling — 前面的兄弟节点
siblings = tree.xpath("//div[@class='footer']/preceding-sibling::div/@class")
print(f"footer前面的兄弟div: {siblings}")
# ancestor — 所有祖先
ancestors = tree.xpath("//h1/ancestor::div/@class")
print(f"h1的所有祖先div: {ancestors}")
# descendant — 所有后代
desc = tree.xpath("//div[@class='container']/descendant::p/text()")
print(f"container下所有p: {desc}")
3.3 获取标签和属性的完整信息
# 获取标签名
tag_name = tree.xpath("//div[@class='header']/child::*[1]/name()")
# 注意:lxml不支持name()函数,用替代方案
# 获取元素对象(不加/text()或/@attr就返回元素)
elements = tree.xpath("//li[contains(@class, 'item')]")
for elem in elements:
print(f"标签: {elem.tag}")
print(f"文本: {elem.text}")
print(f"属性: {elem.attrib}")
print(f"内部HTML: {etree.tostring(elem, encoding='unicode')}")
print()
四、XPath vs CSS选择器对比
| 功能 | CSS选择器 | XPath |
|---|---|---|
| 按标签 | div | //div |
| 按class | .item | //*[@class=‘item’] |
| 按id | #main | //*[@id=‘main’] |
| 后代 | div p | //div//p |
| 直接子 | div > p | //div/p |
| 按属性 | a[href] | //a[@href] |
| 文本包含 | ❌ 不支持 | //*[contains(text(),‘关键词’)] |
| 按位置 | :nth-child(2) | [2] |
| 父节点 | ❌ 不支持 | /… |
| 逻辑组合 | 有限 | and/or/not |
| 模糊匹配 | ^= $= *= | contains/starts-with |
结论:XPath更强大,CSS选择器更简洁。日常用XPath,简单场景用CSS。
五、实战案例:用XPath抓取豆瓣电影
import requests
from lxml import etree
import time
import random
import csv
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Referer": "https://movie.douban.com/",
}
all_movies = []
for page in range(2): # 演示抓2页
start = page * 25
url = f"https://movie.douban.com/top250?start={start}"
print(f"抓取第{page+1}页...")
response = requests.get(url, headers=headers, timeout=15)
response.encoding = "utf-8"
tree = etree.HTML(response.text)
# 用XPath提取数据
items = tree.xpath("//ol[@class='grid_view']/li")
for item in items:
# 排名
rank = item.xpath(".//em/text()")[0] if item.xpath(".//em/text()") else ""
# 电影名(可能有多个title span,取第一个)
title_list = item.xpath(".//span[@class='title']/text()")
title = title_list[0] if title_list else ""
# 其他名称
other_names = title_list[1:] if len(title_list) > 1 else []
# 评分
rating = item.xpath(".//span[@class='rating_num']/text()")[0].strip() if item.xpath(".//span[@class='rating_num']/text()") else ""
# 评价人数
people = item.xpath(".//div[@class='star']/span[4]/text()")[0] if item.xpath(".//div[@class='star']/span[4]/text()") else ""
# 短评
quote = item.xpath(".//span[@class='inq']/text()")[0] if item.xpath(".//span[@class='inq']/text()") else ""
# 详情链接
link = item.xpath(".//div[@class='hd']/a/@href")[0] if item.xpath(".//div[@class='hd']/a/@href")") else ""
all_movies.append([rank, title, rating, people, quote, link])
print(f" 获取 {len(items)} 部")
time.sleep(random.uniform(1, 3))
# 保存CSV
with open("豆瓣Top250_XPath版.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["排名", "电影", "评分", "评价人数", "短评", "链接"])
writer.writerows(all_movies)
print(f"\n共 {len(all_movies)} 部电影,已保存到CSV")
for m in all_movies[:5]:
print(f" {m[0]:>3}. {m[1]:<15} {m[2]} {m[4]}")
六、XPath调试技巧
6.1 在浏览器中测试XPath
Chrome开发者工具 → Console → 输入:
// 测试XPath
$x("//div[@class='item']")
// 测试并获取文本
$x("//div[@class='item']/text()")
// 测试并获取属性
$x("//a/@href")
6.2 XPath Helper插件
安装Chrome插件 “XPath Helper”:
- Ctrl+Shift+X 打开
- 输入XPath表达式
- 实时高亮匹配结果
6.3 常见XPath错误排查
# 问题1:返回空列表
result = tree.xpath("//div[@class='item']/text()")
# 排查:class可能有多个值,用contains
result = tree.xpath("//div[contains(@class, 'item')]/text()")
# 问题2:编码问题
result = tree.xpath("//p[contains(text(), '中文')]/text()")
# 排查:确保HTML字符串是正确的编码
# 问题3:命名空间问题
# 有些XML有命名空间,需要处理
html = '<root xmlns:ns="http://example.com"><ns:item>数据</ns:item></root>'
tree = etree.HTML(html)
# 方法1:忽略命名空间
result = tree.xpath("//*[local-name()='item']/text()")
# 方法2:注册命名空间
# 较复杂,实际爬虫中HTML很少有命名空间
七、知识卡
| XPath表达式 | 说明 |
|---|---|
| //div | 所有div标签 |
| //div/p | div下直接子p |
| //div//p | div下所有后代p |
| //div[@class=‘item’] | class等于item的div |
| //div[contains(@class,‘item’)] | class包含item的div |
| //div[@id=‘main’]/p[2] | id=main的div下第2个p |
| //div/p[last()] | 最后一个p |
| //div/p[position()❤️] | 前两个p |
| //a/@href | a标签的href属性 |
| //p/text() | p标签的文本 |
| //*[contains(text(),‘关键词’)] | 文本包含关键词的任意标签 |
| //li/… | li的父节点 |
| and/or/not | 逻辑运算 |
八、课后作业
必做题:
- 用lxml+XPath重写第1篇的豆瓣电影爬虫
- 练习5种不同的XPath谓语条件
- 在浏览器Console中用$x()测试XPath表达式
选做题:
- 对比同一页面用BS4和XPath提取数据的速度差异
- 用XPath轴(axes)提取指定元素的兄弟节点数据
完成作业的同学,把运行截图发到评论区!
XPath = 爬虫数据提取的终极武器。 比CSS选择器更强大,比BS4更快速,是专业爬虫工程师的标配技能。
本篇要点:
- XPath语法(路径、谓语、函数、轴)
- lxml库的安装和使用
- XPath vs CSS选择器对比
- 实战:XPath版豆瓣爬虫
- 浏览器调试技巧
下一篇学习Selenium——模拟真实浏览器,JS渲染页面一把抓。
收藏 + 关注,专栏更新不迷路!
有问题欢迎评论区留言,大家一起讨论!
标签:Python | XPath | lxml | 数据提取 | 精准定位 | 爬虫进阶
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)