Python爬虫实战(二):百度热搜榜单爬取

一、前言

在上一篇中,我们学习了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()

六、总结

通过本次实战,我们完整掌握了静态网页爬虫的核心技术栈:

  1. HTML结构分析能力:学会使用F12开发者工具定位DOM元素,理解class、id等属性的作用
  2. BeautifulSoup解析技巧 :掌握find/find_all的灵活运用,理解相对查找与全局查找的区别
  3. 防御性编程思维:面对不稳定的网页结构,通过空值判断、默认值设置保证代码健壮性
  4. 数据清洗与持久化:将半结构化的HTML转换为结构化的文本数据

API爬虫 vs 静态爬虫对比:

维度 API接口型(上一篇) 静态网页型(本篇)
数据来源 Ajax返回的JSON HTML中嵌入的文本
解析工具 response.json() BeautifulSoup
结构稳定性 高(接口契约) 低(前端随时改版)
反爬强度 通常更强 通常较弱
适用场景 现代SPA应用 传统服务端渲染

学习建议: 建议读者尝试扩展------比如增加"热搜简介"字段的提取(class为hot-desc_1m_jR),或实现定时爬取对比同一话题的热度变化,甚至使用WordCloud生成热搜词云图。


如果本文对你有帮助,欢迎点赞、收藏、关注!有任何问题欢迎在评论区留言讨论。

相关推荐
TechWayfarer1 小时前
网络安全视角:利用IP定位API接口识别机房与基站流量(合规风控篇)
开发语言·网络·数据库·python·安全·网络安全
码界筑梦坊1 小时前
118-基于Python的游戏账号数据可视化分析系统
python·游戏·信息可视化·毕业设计·pandas·fastapi
dinglu1030DL1 小时前
Less如何构建CSS样式库_通过继承机制优化组件化开发
jvm·数据库·python
前端若水1 小时前
智能体开发与传统软件开发的核心区别
网络·人工智能·python·开源·log4j
duke8692672141 小时前
C#怎么获取U盘的插拔事件_C#如何重写WndProc捕获消息【进阶】
jvm·数据库·python
m0_690825821 小时前
如何高效实现多用户通知系统而不造成数据库冗余
jvm·数据库·python
2301_812539671 小时前
golang如何使用Fiber高性能框架_golang Fiber框架入门教程
jvm·数据库·python
2401_880071401 小时前
html标签如何提升可访问性_aria-label与title区别【指南】
jvm·数据库·python
2401_850491651 小时前
如何管理多个监听器_listener.ora中非默认端口配置实战
jvm·数据库·python