一、为什么需要数据解析
在前两篇中,我们已经学会了如何发送请求并获取网页内容(HTML)。
但是获取到的 HTML 通常是结构复杂的标签内容,不能直接使用。
例如:
<div>
<h1>标题</h1>
<p>价格:100</p>
</div>
我们真正需要的是:
- 标题
- 价格
HTML 本质上是一个树结构(DOM),浏览器会根据这些标签进行渲染,但爬虫拿到的只是原始文本。
因此,我们需要“解析”这些 HTML,找到我们关心的节点,并提取出数据。
总结:
- 请求阶段:获取数据
- 解析阶段:提取数据(本篇重点)
补充:真实网页中的复杂情况
在真实网站中,HTML 结构通常比示例复杂很多,例如:
- 标签嵌套层级很深
- 文本可能被多个标签拆分
- 存在隐藏标签或无效节点
例如:
<div>
<h1><span>标题</span></h1>
<p>价格:<span>100</span></p>
</div>
此时如果直接使用 text(),可能会得到多个结果。
因此在实际开发中,我们需要:
- 熟悉 DOM 结构
- 学会逐层定位
- 多使用调试工具(浏览器 / scrapy shell)
二、HTML 解析的两种主流方式
在 Python 爬虫中,常见的 HTML 解析方式主要有两种:
1. XPath(路径解析)
- 通过路径定位节点
- 类似文件系统路径
- 适合结构清晰的页面
2. BeautifulSoup(标签解析)
- 通过标签查找内容
- 更接近“查找元素”的思维
- 使用简单、灵活
总结:
- XPath:更强大、更高效
- BeautifulSoup:更简单、更适合入门
在实际开发中,两者都很常用。
如何选择解析方式(实战建议)
- 页面结构规则、稳定 → 优先 XPath
- 页面结构混乱、不规则 → BeautifulSoup 更方便
- 需要高性能(大量数据) → XPath 更优
实际项目中,经常是两者结合使用。
三、XPath 入门
1. 安装 lxml
pip install lxml
2. 创建解析对象
# 导入 etree 模块
from lxml import etree
# 示例 HTML 字符串
html = """
<html>
<body>
<div id="main">
<h1>标题</h1>
</div>
</body>
</html>
"""
# 将 HTML 字符串解析为树结构
html_tree = etree.HTML(html)
# 解析本地 HTML 文件(可选)
# html_tree = etree.parse('test.html')
3. XPath 基本语法
路径
/ 直接子节点
// 所有子孙节点
示例
# 查找所有 div 标签
result = html_tree.xpath('//div')
print(result)
4. 属性选择
# 查找 id 为 main 的 div
result = html_tree.xpath('//div[@id="main"]')
print(result)
5. 谓词查询
谓词可以理解为“筛选条件”,常用于进一步限定节点。
# 查找所有带有 id 属性的 div
result = html_tree.xpath('//div[@id]')
print(result)
# 查找 id 为 main 的 div
result = html_tree.xpath('//div[@id="main"]')
print(result)
6. 文本提取
# 提取 div 中的文本内容
result = html_tree.xpath('//div/text()')
print(result)
注意:
html_tree.xpath('//div/text()')
只会获取当前节点的直接文本,不包括子标签中的内容。
如果需要获取所有文本,可以使用:
html_tree.xpath('//div//text()')
7. 属性提取
除了获取节点本身,也可以直接获取节点属性。
# 提取所有 class 属性
result = html_tree.xpath('//div/@class')
print(result)
8. 模糊匹配
# 查找 class 包含 item 的元素
result = html_tree.xpath('//div[contains(@class, "item")]')
print(result)
# 查找 id 以 he 开头的元素
result = html_tree.xpath('//div[starts-with(@id, "he")]')
print(result)
9. 返回值说明(非常重要)
result = html_tree.xpath('//div')
在 XPath 中,不同写法返回的内容类型可能不同。例如,选择节点时返回元素对象,选择 text() 或 @属性 时通常返回字符串。
# 返回节点对象列表
nodes = html_tree.xpath('//div')
print(nodes)
# 返回文本列表
texts = html_tree.xpath('//h1/text()')
print(texts)
# 返回属性值列表
attrs = html_tree.xpath('//div/@id')
print(attrs)
返回值通常是:
- list(列表)
- 元素对象(Element)或字符串
因此,通常需要:
# 获取第一个元素
first = result[0]
# 获取文本
text = html_tree.xpath('//h1/text()')[0]
推荐写法:
# 安全获取第一个元素
result = html_tree.xpath('//h1/text()')
text = result[0] if result else None
可以避免 IndexError 异常。
XPath 调试技巧(非常重要)
可以在浏览器中直接测试 XPath:
- 打开开发者工具(F12)
- 使用 Elements 面板
- 按 Ctrl + F
- 输入 XPath 表达式
例如:
//div[@class="quote"]
如果能匹配到元素,说明 XPath 正确。
四、BeautifulSoup 入门
1. 安装
pip install bs4
2. 创建对象
# 导入 BeautifulSoup
from bs4 import BeautifulSoup
html = """
<html>
<body>
<a class="name">Python</a>
</body>
</html>
"""
# 创建解析对象
soup = BeautifulSoup(html, 'lxml')
除了直接解析字符串,也可以解析本地 HTML 文件:
# 导入 BeautifulSoup
from bs4 import BeautifulSoup
# 读取本地 HTML 文件时,建议显式指定编码
with open('test.html', 'r', encoding='utf-8') as f:
soup = BeautifulSoup(f, 'lxml')
3. 基本查找方式
标签查找
# 查找第一个 a 标签
tag = soup.find('a')
print(tag)
# 查找所有 a 标签
tags = soup.find_all('a')
print(tags)
属性查找
# 查找 class 为 name 的标签
tag = soup.find('a', class_='name')
print(tag)
直接通过标签访问
# 直接获取第一个 a 标签
print(soup.a)
# 获取标签名称
print(soup.a.name)
# 获取标签属性字典
print(soup.a.attrs)
4. select(推荐)
# 类选择器
result = soup.select('.name')
# id 选择器
result = soup.select('#main')
# 层级选择
result = soup.select('div > a')
print(result)
select() 本质上使用的是 CSS 选择器,语法非常灵活。
# 属性选择器:查找带有 class 属性的 li
result = soup.select('li[class]')
print(result)
# 属性值选择器:查找 class 为 item 的 li
result = soup.select('li[class="item"]')
print(result)
# 多元素选择器:同时查找 a 和 span
result = soup.select('a, span')
print(result)
select_one(推荐)
# 获取第一个匹配元素
result = soup.select_one('.name')
print(result)
相比 find(),select_one() 更统一(与 select 配套)。
5. 获取内容
# 获取文本内容
text = tag.get_text()
print(text)
# 获取属性
attrs = tag.attrs
print(attrs)
获取属性值时,常见写法有三种:
# 方式一:通过 attrs.get 获取
value = tag.attrs.get('class')
print(value)
# 方式二:通过 get 获取
value = tag.get('class')
print(value)
# 方式三:通过字典方式获取
value = tag['class']
print(value)
注意区别:
print(tag.text)
print(tag.get_text())
两者基本一致,但 get_text() 更推荐,因为支持参数扩展。
五、XPath 与 BeautifulSoup 对比
| 方式 | 优点 | 缺点 |
|---|---|---|
| XPath | 功能强、速度快 | 语法较复杂 |
| BeautifulSoup | 简单易用 | 性能较低 |
建议:
- 初学者先使用 BeautifulSoup
- 熟练后建议掌握 XPath
六、实战案例:提取网页标题
# 模拟 HTML 数据
html = """
<html>
<body>
<h1>Python 爬虫</h1>
</body>
</html>
"""
# 导入解析库
from lxml import etree
# 解析 HTML
html_tree = etree.HTML(html)
# 使用 XPath 提取标题
result = html_tree.xpath('//h1/text()')
# 输出结果
print(result)
输出:
['Python 爬虫']
BeautifulSoup 实现同样功能
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
# 提取标题
result = soup.select_one('h1').get_text()
print(result)
七、常见问题
- xpath 返回空结果
- 路径写错
- HTML 结构和预期不一致
- 返回结果是列表
- xpath 默认返回 list,需要取索引
- text() 获取不到内容
- 标签中可能嵌套子标签
- BeautifulSoup 查不到元素
- 解析器问题(建议使用 lxml)
- HTML 结构变化
- XPath 提取结果不完整
- 可能使用了
/text()而不是//text()
- 页面结构变化
- 网站更新后 XPath 失效
- 建议使用更稳定的属性(id / class)
八、XPath 进阶补充
1. 位置选择
# 获取第一个 div
result = html_tree.xpath('(//div)[1]')
print(result)
# 获取最后一个 div
result = html_tree.xpath('(//div)[last()]')
print(result)
2. 逻辑运算
# 同时满足多个条件
result = html_tree.xpath('//div[@id="head" and @class="item"]')
print(result)
3. 并集选择
# 同时获取 title 和 price
result = html_tree.xpath('//title | //price')
print(result)
4. 轴(了解即可)
# 获取父节点
result = html_tree.xpath('//h1/..')
print(result)
# 获取所有祖先节点
result = html_tree.xpath('//h1/ancestor::*')
print(result)
5. 浏览器 XPath 插件(可选)
用于辅助定位节点
- 打开 Chrome 浏览器
- 进入扩展程序页面
- 安装 XPath Helper 插件
- 使用快捷键 Ctrl + Shift + X 打开
通过在页面中输入 XPath,可以快速验证路径是否正确。
九、BeautifulSoup 补充用法
1. find_all 常用参数
# 限制返回数量
result = soup.find_all('a', limit=2)
print(result)
# 同时查找多个标签
result = soup.find_all(['a', 'span'])
print(result)
2. string 与 get_text 区别
# string:仅当标签中只有一个字符串时可用
print(tag.string)
# get_text:推荐,能获取所有文本内容
print(tag.get_text())
3. 层级查找补充
# 查找 div 下所有 p
result = soup.select('div p')
print(result)
# 只查找直接子节点
result = soup.select('div > p')
print(result)
4. recursive 参数
# 只查找当前层级(不递归)
result = soup.find_all('p', recursive=False)
print(result)
十、JSON 数据解析补充
除了 HTML 页面,爬虫中还经常遇到 JSON 数据。很多 Ajax 接口返回的不是 HTML,而是结构化 JSON。
1. 使用 json 模块
如果接口返回内容如下:
{
"code": 0,
"data": {
"list": [
{"title": "第一条数据", "price": 100},
{"title": "第二条数据", "price": 200}
]
}
}
可以使用 json.loads() 转成 Python 字典。
import json
text = '''
{
"code": 0,
"data": {
"list": [
{"title": "第一条数据", "price": 100},
{"title": "第二条数据", "price": 200}
]
}
}
'''
data = json.loads(text)
for item in data['data']['list']:
print(item['title'], item['price'])
2. 使用 JSONPath
当 JSON 层级比较深时,也可以使用 JSONPath 来提取数据。
pip install jsonpath
示例:
import json
import jsonpath
data = json.loads(text)
titles = jsonpath.jsonpath(data, '$.data.list[*].title')
prices = jsonpath.jsonpath(data, '$.data.list[*].price')
print(titles)
print(prices)
JSONPath 的写法和 XPath 有点像:
| 写法 | 含义 |
|---|---|
$ |
根节点 |
. |
子节点 |
[*] |
匹配列表中的所有元素 |
$..title |
递归查找所有 title 字段 |
3. 图片下载的基本流程
如果页面中有图片地址,通常可以先用 XPath 或 BeautifulSoup 提取图片 URL,再逐个下载。
from urllib import request
from lxml import etree
html = '''
<html>
<body>
<img src="https://example.com/a.jpg">
<img src="https://example.com/b.jpg">
</body>
</html>
'''
tree = etree.HTML(html)
img_urls = tree.xpath('//img/@src')
for index, img_url in enumerate(img_urls):
request.urlretrieve(img_url, f'img_{index}.jpg')
真实网站中还需要注意相对路径问题。有些图片地址可能是:
/images/a.jpg
这时需要和网站域名拼接后再下载。
十一、总结
本篇我们重点学习了 HTML 解析的两种核心方式:
- XPath(路径解析)
- BeautifulSoup(标签解析)
同时掌握了:
- 如何定位节点
- 如何提取文本与属性
- 如何处理返回结果
- 常见问题的排查方法
- JSON 数据解析和图片下载的基本流程
这一篇的内容是整个爬虫体系中的核心,因为:
爬虫的本质 = 请求 + 解析
下一篇通常会进入:
- 动态页面(Selenium)
- 或接口抓取(JSON 数据)