一、前言
在上一篇中,我们学习了API接口型爬虫 ------通过分析Ajax请求直接获取JSON数据。然而,互联网上仍有大量网站采用传统的服务端渲染(SSR)模式,数据直接嵌入在HTML源码中。这类网站无法通过抓包找到API接口,必须直面HTML文档的解析问题。
本文将以 百度热搜 为实战目标,深入讲解如何:
- 分析静态网页的HTML DOM树结构
- 使用
BeautifulSoup进行CSS选择器 与标签遍历解析 - 处理复杂的class命名(如Webpack打包后的哈希类名)
- 应对"置顶"条目与普通条目的结构差异
- 实现数据的结构化提取与文本文件持久化
目标站点特点: 百度热搜是典型的服务端渲染页面,所有数据在HTML返回时已经完整嵌入。页面使用了现代前端构建工具(Webpack/Vite)打包,导致class名包含哈希后缀(如
category-wrap_iQLoo),这对传统的固定class匹配提出了挑战。
二、网站分析与HTML结构探测
2.1 百度热搜页面概览
打开 百度热搜实时榜,可以看到熟悉的热搜榜单界面:
观察发现: 页面包含多条热搜条目,每条包含排名数字 、标题文字 、热度指数三要素。部分条目带有"置顶"标识,排名区域显示为图标而非数字。
2.2 开发者工具分析
按F12打开开发者工具,使用元素选择器 (Ctrl+Shift+C)悬停在热搜条目上,可以看到每条热搜被包裹在一个div容器中:
通过逐层展开DOM树,我们提取到以下关键HTML结构:
html
<!-- 普通热搜条目 -->
<div class="category-wrap_iQLoo horizontal_1eKyQ">
<div class="index_1Ew5p">1</div> <!-- 排名 -->
<div class="c-single-text-ellipsis">标题</div> <!-- 标题 -->
<div class="hot-index_1Bl1a">4952713</div> <!-- 热度 -->
</div>
<!-- 置顶热搜条目(注意:没有index_1Ew5p类,排名区域是图标) -->
<div class="category-wrap_iQLoo horizontal_1eKyQ">
<!-- 此处没有排名div,或排名div内容为空 -->
<div class="c-single-text-ellipsis">置顶标题</div>
<div class="hot-index_1Bl1a">9999999</div>
</div>
2.3 解析策略设计
核心挑战分析:
| 挑战点 | 说明 | 解决方案 |
|---|---|---|
| 哈希类名 | category-wrap_iQLoo中的iQLoo是Webpack添加的哈希 |
使用完整类名匹配,或改用contains模糊匹配 |
| 置顶条目 | 置顶内容没有排名数字,直接查找会返回None |
增加空值判断,赋予"置顶"默认值 |
| 多层嵌套 | 标题、热度等字段分布在不同层级div中 | 使用相对查找:div.find()而非soup.find() |
解析路径设计:
外层容器(category-wrap_iQLoo)
├── 排名div(index_1Ew5p) → 获取文本,空则设为"置顶"
├── 标题div(c-single-text-ellipsis) → 获取文本
└── 热度div(hot-index_1Bl1a) → 获取文本
这种**"先定位容器,再相对查找子元素"**的策略,比全局搜索更加精准可靠,避免了不同条目间的字段错位。
三、代码实现与深度解析
3.1 完整源码
python
import requests
from bs4 import BeautifulSoup
# ========================================
# 第一部分:HTML数据获取
# ========================================
def fetch_html(url):
"""
发送HTTP GET请求获取网页HTML内容
参数:
url: 目标网页地址
返回:
解码后的HTML字符串,失败返回None
深度解析:
1. response.encoding = 'utf-8' 是必要操作:
- requests库默认使用ISO-8859-1编码猜测,中文网站常误判
- 显式指定utf-8可避免中文乱码(如"热搜"显示为"çƒæœ")
2. 未设置headers模拟浏览器,因为百度热搜对UA要求较宽松
- 但在生产环境中仍建议添加,防止被反爬系统标记
"""
try:
response = requests.get(url, timeout=15)
# 显式设置编码:百度页面采用UTF-8编码,防止中文乱码
response.encoding = 'utf-8'
response.raise_for_status() # 检查HTTP状态码
return response.text
except requests.RequestException as e:
print(f"网络请求异常: {e}")
return None
# ========================================
# 第二部分:BeautifulSoup数据提取
# ========================================
def extract_data(html_content):
"""
使用BeautifulSoup解析HTML,提取热搜排名、标题、热度
参数:
html_content: 网页HTML字符串
返回:
列表,元素为(排名, 标题, 热度)元组
解析策略详解:
1. 选择'html.parser'解析器:
- Python内置,无需额外安装
- 容错性强,对不规范HTML有较好兼容性
- 速度适中,适合大多数场景
- 替代方案:'lxml'(速度快,需安装),'lxml-xml'(XML专用)
2. 外层循环:soup.find_all('div', {'class': 'category-wrap_iQLoo horizontal_1eKyQ'})
- 定位每一条热搜的"根容器"
- 使用字典形式传递class参数,匹配包含多个类名的元素
- 注意:BeautifulSoup的class匹配是"包含"而非"精确等于"
3. 内层查找:div.find('div', {'class': 'xxx'})
- 在已定位的容器内部进行相对查找
- 这是关键设计:确保提取的"排名-标题-热度"属于同一条热搜
- 避免全局搜索导致的字段错位(如第一条的标题配第二条的热度)
"""
# 创建BeautifulSoup解析对象
# 'html.parser'是Python标准库解析器,无需额外安装依赖
soup = BeautifulSoup(html_content, 'html.parser')
items = [] # 初始化结果容器
# 遍历所有热搜条目容器
# find_all返回的是ResultSet(可迭代对象),按文档中出现顺序排列
for div in soup.find_all('div', {'class': 'category-wrap_iQLoo horizontal_1eKyQ'}):
# ---------------- 排名提取 ----------------
# 查找排名div,class为index_1Ew5p
rank_div = div.find('div', {'class': 'index_1Ew5p'})
# 防御性编程:get_text(strip=True)提取文本并去除首尾空白
# 如果rank_div为None(如置顶条目),则赋予默认值'置顶'
rank = rank_div.get_text(strip=True) if rank_div else None
# 置顶条目处理:当排名为空字符串或None时,标记为"置顶"
# 这是百度热搜的特殊设计:前两条通常为置顶,无数字排名
if not rank:
rank = '置顶'
# ---------------- 标题提取 ----------------
# c-single-text-ellipsis是百度常用的文本溢出省略类
# 该类使用了CSS的text-overflow: ellipsis,保证标题单行显示
title_div = div.find('div', {'class': 'c-single-text-ellipsis'})
title = title_div.get_text(strip=True) if title_div else '未知标题'
# ---------------- 热度提取 ----------------
# hot-index_1Bl1a包含纯数字热度值,如"4952713"
# 部分条目可能没有热度值(如广告位),同样做None判断
hotness_div = div.find('div', {'class': 'hot-index_1Bl1a'})
hotness = hotness_div.get_text(strip=True) if hotness_div else '0'
# 将三元组加入结果列表
items.append((rank, title, hotness))
return items
# ========================================
# 第三部分:数据持久化
# ========================================
def save_to_file(items, filename='baidu_hot_top.txt'):
"""
将提取的热搜数据保存到文本文件
参数:
items: 包含(排名, 标题, 热度)元组的列表
filename: 输出文件名,默认'baidu_hot_top.txt'
设计考量:
1. 使用with语句管理文件上下文,确保资源自动释放
2. 采用'w'写入模式(覆盖),如需追加应改为'a'
3. 每条记录末尾添加换行符\n,实现一行一条记录
4. 同时向控制台打印,实现"边保存边观察"的调试友好设计
"""
with open(filename, 'w', encoding='utf-8') as file:
for item in items:
# 格式化输出字符串:f-string语法,Python 3.6+支持
line = f'排名:{item[0]}, 标题:{item[1]}, 热度:{item[2]}\n'
# 控制台打印:便于实时观察爬取进度与结果
print(line, end='') # end=''避免print自动添加额外换行
# 文件写入:持久化存储
file.write(line)
print(f"\n数据已成功保存至: {filename}")
# ========================================
# 第四部分:主程序入口
# ========================================
if __name__ == '__main__':
# 百度热搜实时榜URL
# tab=realtime表示实时热搜,其他可选值:novel(小说), movie(电影), teleplay(电视剧)等
target_url = 'https://top.baidu.com/board?tab=realtime'
print(f"开始爬取: {target_url}")
print("-" * 50)
# 步骤1:获取网页HTML
html_content = fetch_html(target_url)
if html_content:
# 步骤2:解析提取数据
hot_items = extract_data(html_content)
print(f"\n共提取到 {len(hot_items)} 条热搜数据")
print("=" * 50)
# 步骤3:保存到文件
save_to_file(hot_items)
else:
print("爬取失败,请检查网络连接或URL是否正确")
3.2 核心设计思想解析
(1)BeautifulSoup解析器选择
代码中使用了html.parser,这是Python标准库内置的解析器。但在不同场景下,解析器的选择直接影响性能与兼容性:
| 解析器 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
html.parser |
内置,无需安装 | 速度较慢,容错一般 | 学习、小型项目 |
lxml |
速度快,容错强 | 需安装C依赖 | 生产环境、大规模爬取 |
html5lib |
按浏览器方式解析 | 速度最慢 | 需要与浏览器完全一致 |
安装lxml只需:pip install lxml,然后将解析器参数改为'lxml'即可。
(2)相对查找 vs 全局查找
这是本代码最核心的设计思想:
python
# ❌ 错误做法:全局查找,容易错位
all_ranks = soup.find_all('div', {'class': 'index_1Ew5p'})
all_titles = soup.find_all('div', {'class': 'c-single-text-ellipsis'})
# 如果某个条目缺少排名,两个列表长度不一致,按索引配对会出错!
# ✅ 正确做法:先定位容器,再相对查找
for div in soup.find_all('div', {'class': 'category-wrap_iQLoo horizontal_1eKyQ'}):
rank = div.find('div', {'class': 'index_1Ew5p'}) # 在该容器内查找
title = div.find('div', {'class': 'c-single-text-ellipsis'})
# 保证三者属于同一条热搜,即使某些字段缺失也不会错位
这种**"容器隔离"**策略,是处理列表型网页数据的黄金法则。
(3)防御性编程模式
代码中大量使用了if xxx else yyy的三元表达式:
python
rank = rank_div.get_text(strip=True) if rank_div else None
title = title_div.get_text(strip=True) if title_div else '未知标题'
这体现了**"永远不信任网页结构"**的爬虫哲学:
- 网页随时可能改版,某个class可能突然消失
- 广告位、置顶位的结构可能与正常条目不同
- 优雅处理None,比让程序崩溃更有价值
四、运行效果展示
4.1 控制台输出
程序运行时的控制台输出如下,可以看到清晰的排名、标题、热度信息:
输出特征:
- 排名数字与"置顶"标识清晰区分
- 热度值完整显示,无科学计数法截断
- 中文显示正常,无编码乱码问题
4.2 生成的数据文件
爬取完成后,生成的baidu_hot_top.txt文件内容如下:

排名:置顶, 标题:中国方案推动全球绿色低碳转型, 热度:4952713
排名:1, 标题:多地公布公务员省考时间, 热度:4730000
排名:2, 标题:传说9人男子被判1年2个月, 热度:4260000
...
文件特点:
- UTF-8编码,中文无乱码
- 结构化格式,便于后续正则提取或导入Excel
- 包含完整的排名、标题、热度三要素
五、进阶思考与优化方向
5.1 多榜单扩展:URL参数化
百度热搜包含多个子榜单,通过URL参数tab切换:
python
TABS = {
'realtime': '实时热搜',
'novel': '小说榜',
'movie': '电影榜',
'teleplay': '电视剧榜',
'car': '汽车榜',
'game': '游戏榜'
}
def crawl_all_tabs():
for tab, name in TABS.items():
url = f'https://top.baidu.com/board?tab={tab}'
html = fetch_html(url)
items = extract_data(html)
save_to_file(items, f'baidu_{tab}.txt')
print(f"{name}爬取完成,共{len(items)}条")
这种参数化设计让代码从"爬一个页面"升级为"爬一类页面"。
5.2 反爬对抗升级
虽然百度热搜反爬较弱,但当爬取频率提高时,仍需注意:
python
import random
import time
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
'Referer': 'https://www.baidu.com/',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
# 随机延时:模拟人类操作间隔,避免触发频率限制
time.sleep(random.uniform(1, 3))
5.3 数据存储升级:CSV与数据库
当需要频繁查询、排序、筛选时,文本文件效率低下:
python
import csv
# CSV格式:结构化更强,Excel直接打开
with open('baidu_hot.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['排名', '标题', '热度', '爬取时间'])
for item in items:
writer.writerow([item[0], item[1], item[2], datetime.now()])
或存入MongoDB实现时序数据追踪------记录同一话题的热度变化趋势。
5.4 可视化分析
结合Matplotlib/Seaborn对热搜数据进行可视化:
python
import matplotlib.pyplot as plt
# 热度分布直方图
hotness_values = [int(item[2]) for item in items if item[2].isdigit()]
plt.hist(hotness_values, bins=20, color='coral')
plt.xlabel('热度值')
plt.ylabel('词条数量')
plt.title('百度热搜热度分布')
plt.show()
六、总结
通过本次实战,我们完整掌握了静态网页爬虫的核心技术栈:
- HTML结构分析能力:学会使用F12开发者工具定位DOM元素,理解class、id等属性的作用
- BeautifulSoup解析技巧 :掌握
find/find_all的灵活运用,理解相对查找与全局查找的区别 - 防御性编程思维:面对不稳定的网页结构,通过空值判断、默认值设置保证代码健壮性
- 数据清洗与持久化:将半结构化的HTML转换为结构化的文本数据
API爬虫 vs 静态爬虫对比:
| 维度 | API接口型(上一篇) | 静态网页型(本篇) |
|---|---|---|
| 数据来源 | Ajax返回的JSON | HTML中嵌入的文本 |
| 解析工具 | response.json() |
BeautifulSoup |
| 结构稳定性 | 高(接口契约) | 低(前端随时改版) |
| 反爬强度 | 通常更强 | 通常较弱 |
| 适用场景 | 现代SPA应用 | 传统服务端渲染 |
学习建议: 建议读者尝试扩展------比如增加"热搜简介"字段的提取(class为
hot-desc_1m_jR),或实现定时爬取对比同一话题的热度变化,甚至使用WordCloud生成热搜词云图。
如果本文对你有帮助,欢迎点赞、收藏、关注!有任何问题欢迎在评论区留言讨论。