Python快速入门专业版(五十九):re实战——用正则爬取豆瓣电影Top250(全流程解析)

目录

一、爬取目标

我们的目标非常明确:获取豆瓣电影Top250的以下核心字段------

字段 说明 示例
排名 1-250的序号 1
电影名 中文片名 肖申克的救赎
导演与主演 导演及主要演员 弗兰克·德拉邦特 / 蒂姆·罗宾斯...
评分 豆瓣评分(保留1位小数) 9.7
评价人数 参与评分的人数 2963220人评价
电影简介 一句话短评 希望让人自由。

这些数据将全部保存到CSV文件中,方便后续的数据分析与可视化。

豆瓣电影Top250页面结构规整、反爬门槛相对较低,是入门爬虫的经典练手项目。不过请注意:豆瓣并非完全没有反爬 ,直接裸请求会被拦截,因此我们必须做好请求头伪装。

二、URL规律分析

打开豆瓣Top250页面(https://movie.douban.com/top250),按F12打开开发者工具观察:

  • 第1页https://movie.douban.com/top250?start=0&filter=
  • 第2页https://movie.douban.com/top250?start=25&filter=
  • 第3页https://movie.douban.com/top250?start=50&filter=

规律非常清晰:每页显示25部电影,start参数每次增加25。一共10页(25 × 10 = 250),start从0递增到225。

用公式表达就是:

复制代码
第n页的URL: https://movie.douban.com/top250?start={(n-1)*25}&filter=

或者直接用循环:

python 复制代码
for page in range(10):
    url = f"https://movie.douban.com/top250?start={page * 25}&filter="

三、HTML结构解析

在动手写正则之前,必须先弄清楚目标数据在HTML中的位置。用Chrome打开豆瓣Top250,右键 →"检查",定位到任意一部电影。

每一部电影都被包裹在 <div class="item"> 标签内,这是我们正则匹配的核心容器。其内部结构大致如下:

html 复制代码
<div class="item">
    <div class="pic">
        <em class="">1</em>          <!-- 排名 -->
        <a href="...">
            <img src="..." />
        </a>
    </div>
    <div class="info">
        <div class="hd">
            <a href="...">
                <span class="title">肖申克的救赎</span>   <!-- 电影名 -->
                <span class="title"> / The Shawshank Redemption</span>  <!-- 英文名 -->
            </a>
        </div>
        <div class="bd">
            <p class="">
                导演: 弗兰克·德拉邦特&nbsp;主演: 蒂姆·罗宾斯...<br>
                1994&nbsp;/&nbsp;美国&nbsp;/&nbsp;犯罪 剧情
            </p>
            <div class="star">
                <span class="rating_num" property="v:average">9.7</span>  <!-- 评分 -->
                <span>2963220人评价</span>   <!-- 评价人数 -->
            </div>
            <p class="quote">
                <span class="inq">希望让人自由。</span>   <!-- 简介 -->
            </p>
        </div>
    </div>
</div>

各字段的定位规律如下:

字段 HTML特征
排名 <em class="">数字</em>
电影名 <span class="title">电影名</span>(第一个title标签)
导演/主演 <p class=""> 内部,导演: 开头
评分 <span class="rating_num" property="v:average">分数</span>
评价人数 <span>数字人评价</span>(star div下的最后一个span)
简介 <span class="inq">一句话简介</span>

四、完整代码实现

4.1 环境准备

bash 复制代码
# 安装requests库(re是Python内置模块,无需额外安装)
pip install requests

4.2 完整代码

python 复制代码
import requests
import re
import csv
import time
import random
from typing import List, Dict, Optional

# ---------- 1. 请求函数(含重试机制) ----------
def fetch_page(url: str, headers: dict, retries: int = 3) -> Optional[str]:
    for attempt in range(retries):
        try:
            resp = requests.get(url, headers=headers, timeout=10)
            if resp.status_code == 200:
                resp.encoding = 'utf-8'
                return resp.text
            print(f"状态码 {resp.status_code},重试 {attempt+1}/{retries}")
            time.sleep(random.uniform(2, 4))
        except Exception as e:
            print(f"请求异常: {e},重试 {attempt+1}/{retries}")
            time.sleep(2 ** attempt)  # 指数退避
    return None

# ---------- 2. 正则解析函数 ----------
def parse_movies(html: str) -> List[Dict[str, str]]:
    pattern = re.compile(
        r'<div class="item">.*?'
        r'<em class="">(?P<rank>\d+)</em>.*?'
        r'<span class="title">(?P<title>[^&]*?)</span>.*?'
        r'<p class="">(?P<director_actor>.*?)</p>.*?'
        r'<span class="rating_num" property="v:average">(?P<rating>\d\.\d)</span>.*?'
        r'<span>(?P<votes>\d+)人评价</span>.*?'
        r'<span class="inq">(?P<intro>.*?)</span>',
        re.S
    )
    movies = []
    for m in pattern.finditer(html):
        # 清洗导演/主演字段
        raw = m.group('director_actor').strip()
        cleaned = re.sub(r'\s+', ' ', raw.replace('\xa0', ' ')).strip()
        movies.append({
            'rank': m.group('rank'),
            'title': m.group('title').strip(),
            'director_actor': cleaned,
            'rating': m.group('rating'),
            'votes': m.group('votes'),
            'intro': m.group('intro').strip()
        })
    return movies

# ---------- 3. 保存CSV ----------
def save_to_csv(movies: List[Dict[str, str]], filename: str = 'douban_top250.csv'):
    if not movies:
        return
    with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
        writer = csv.DictWriter(f, fieldnames=['rank', 'title', 'director_actor', 'rating', 'votes', 'intro'])
        writer.writeheader()
        writer.writerows(movies)
    print(f"✅ 已保存 {len(movies)} 条数据到 {filename}")

# ---------- 4. 主流程 ----------
def main():
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    }
    all_movies = []
    print("=" * 50)
    print("🚀 开始爬取豆瓣电影Top250")
    print("=" * 50)

    for page in range(10):
        start = page * 25
        url = f"https://movie.douban.com/top250?start={start}&filter="
        print(f"📄 正在爬取第 {page+1}/10 页 (start={start})...")
        
        html = fetch_page(url, headers)
        if not html:
            continue
        
        movies = parse_movies(html)
        print(f"   ✓ 解析到 {len(movies)} 部电影")
        all_movies.extend(movies)
        
        time.sleep(random.uniform(1, 2.5))  # 礼貌间隔

    save_to_csv(all_movies)
    
    # 预览
    print("\n📊 数据预览(前5条):")
    for m in all_movies[:5]:
        print(f"  {m['rank']}. {m['title']} | 评分: {m['rating']} | {m['votes']}人评价")

if __name__ == '__main__':
    main()

4.3 正则表达式逐行解析

我们来拆解这个核心正则表达式:

python 复制代码
pattern = re.compile(
    r'<div class="item">.*?'                    # 1. 定位到电影容器
    r'<em class="">(?P<rank>\d+)</em>.*?'       # 2. 提取排名(命名分组 rank)
    r'<span class="title">(?P<title>[^&]*?)</span>.*?'  # 3. 提取电影名
    r'<p class="">(?P<director_actor>.*?)</p>.*?'       # 4. 提取导演/主演
    r'<span class="rating_num" property="v:average">(?P<rating>\d\.\d)</span>.*?'  # 5. 提取评分
    r'<span>(?P<votes>\d+)人评价</span>.*?'              # 6. 提取评价人数
    r'<span class="inq">(?P<intro>.*?)</span>',          # 7. 提取简介
    re.S                                                # 让 . 匹配换行符
)

关键点说明

  • (?P<name>...)命名分组 ,可以通过 match.group('name') 按名称取值,比数字索引更清晰。
  • .*?非贪婪匹配,尽可能少地匹配字符,避免跨过多个标签造成误匹配。
  • [^&]*? 用于匹配电影名,[^&] 表示匹配除 & 以外的任意字符------因为豆瓣电影名中可能包含 &nbsp; 等HTML实体,用这个可以更精准地截断。
  • re.S 标志让 . 能够匹配换行符,因为HTML源码中标签之间往往有换行和缩进。

五、数据清洗

从HTML中提取的数据往往带有杂质,需要进行清洗:

5.1 导演/主演字段清洗

原始数据类似:

复制代码
导演: 弗兰克·德拉邦特&nbsp;主演: 蒂姆·罗宾斯...

清洗步骤:

  1. .strip() 去除首尾空白
  2. .replace('\xa0', ' ')&nbsp; 替换为普通空格(\xa0&nbsp; 的Unicode表示)
  3. re.sub(r'\s+', ' ', text) 将多个连续空白压缩为单个空格
python 复制代码
director_actor_raw = match.group('director_actor').strip()
director_actor = re.sub(r'\s+', ' ', director_actor_raw.replace('\xa0', ' ')).strip()

5.2 评价人数清洗

原始数据是 "2963220人评价",我们只保留数字部分。正则中已经用 (\d+) 只捕获了数字,所以无需额外清洗。

5.3 电影名清洗

有些电影名包含HTML实体(如 &quot;),但豆瓣的 .title 中通常不包含,直接 .strip() 即可。

六、进阶优化

6.1 随机User-Agent池

使用单一的User-Agent容易被识别为爬虫。可以引入 fake_useragent 库动态生成:

python 复制代码
from fake_useragent import UserAgent

ua = UserAgent()
headers = {
    'User-Agent': ua.random,  # 每次请求随机一个UA
    # ... 其他headers
}

安装:

bash 复制代码
pip install fake-useragent

6.2 请求间隔控制

在循环中设置随机间隔,模拟人类浏览行为:

python 复制代码
import random
import time

# 每次请求后随机等待1-3秒
time.sleep(random.uniform(1, 3))

6.3 异常重试机制

网络请求可能因各种原因失败(超时、连接重置等),加入重试机制提高鲁棒性:

python 复制代码
def fetch_page(url, headers, retries=3):
    for attempt in range(retries):
        try:
            response = requests.get(url, headers=headers, timeout=10)
            if response.status_code == 200:
                return response.text
        except Exception as e:
            print(f"第{attempt+1}次请求失败: {e}")
            time.sleep(2 ** attempt)  # 指数退避
    return None

6.4 处理验证码

豆瓣在检测到异常流量时可能返回验证码页面。如果发现响应中包含验证码相关关键词(如 "验证""captcha"),可以暂停程序并提示用户手动处理:

python 复制代码
if '验证' in html or 'captcha' in html.lower():
    print("检测到验证码,请手动在浏览器中完成验证后按Enter继续...")
    input()
    # 更新Cookie后继续

七、结果展示与验证

运行上述代码后,会生成 douban_top250.csv 文件,内容如下:

rank title director_actor rating votes intro
1 肖申克的救赎 导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯... 9.7 2963220 希望让人自由。
2 霸王别姬 导演: 陈凯歌 主演: 张国荣... 9.6 2234567 风华绝代。
3 阿甘正传 导演: 罗伯特·泽米吉斯 主演: 汤姆·汉克斯... 9.5 1987654 人生就像一盒巧克力。

验证要点

  1. 数量验证:最终是否正好250条?如果有缺失,检查对应页面的正则是否匹配。
  2. 字段完整性:每条记录是否都有完整的6个字段?简介字段可能为空(部分电影无短评),需要容错处理。
  3. 数据准确性:随机抽查几条,与网页实际数据对比。

八、常见问题与解决方案

问题 原因 解决方案
返回418状态码 未携带User-Agent或User-Agent被识别为爬虫 添加完整的请求头,使用真实的浏览器UA
返回乱码 编码问题或Accept-Encoding包含br压缩 设置response.encoding='utf-8',或从Accept-Encoding中移除br
正则匹配不到数据 HTML结构变化或正则写错 print(html[:2000])检查源码,调整正则表达式
爬到一半被ban 请求频率过高 增加请求间隔,使用随机UA和代理IP
部分电影简介为空 部分电影没有<span class="inq">标签 在正则中将简介部分设为可选 (?:<span class="inq">(?P<intro>.*?)</span>)?

九、总结

本文完整演示了如何使用 requests + re 爬取豆瓣电影Top250的全流程:

  1. URL规律分析start 参数每页递增25,共10页
  2. 请求发送:携带User-Agent伪装浏览器,处理异常和重试
  3. 数据提取:用正则表达式的命名分组精确定位各字段
  4. 数据清洗:去除HTML杂质,格式化数据
  5. 数据存储 :用CSV模块保存,使用 utf-8-sig 编码避免中文乱码
  6. 进阶优化:随机UA、请求间隔、异常重试

正则表达式虽然看起来复杂,但一旦掌握,就能精准地从任何文本中提取结构化数据,是爬虫工程师的必备技能。而豆瓣Top250作为经典的练手项目,能帮助你快速入门并建立信心。

接下来,你可以在这些数据的基础上做更多有趣的事情------比如用 matplotlib 绘制评分分布图,或者用 pandas 分析不同年代电影的评分趋势。爬虫只是第一步,数据分析才是更大的舞台!

相关推荐
旖旎夜光1 小时前
【LangChain实战】LangChain 学习笔记(二):结构化输出、流式传输与消息管理
人工智能·笔记·python·学习·ai·langchain
the局外人1 小时前
学习 FastAPI 的 Day 1:看懂接口与请求流程
后端·python·fastapi
whitelbwwww2 小时前
c++ 多线程
开发语言·c++·算法
额鹅恶饿呃2 小时前
随着CentOS官方停服的时间越来越久,大量仍在使用CentOS7的企业和运维从业者
java·python·算法·c#·ruby
Csvn2 小时前
🐍 Day 11: 调试与诊断 — 从 print 到 pdb 的进阶之路
后端·python
乌药ice2 小时前
c#中一个多线程安全的HashSet
开发语言·c#
秋名RG2 小时前
Java 异常处理全攻略:从入门到实战(JDK 21 版)
java·开发语言
努力努力再努力wz2 小时前
【Docker入门系列】:从架构演进到容器化:一文建立 Docker、虚拟化与 Namespace 的底层心智模型
运维·开发语言·数据结构·c++·docker·容器·架构
wuyk5552 小时前
13.堆排序:基于完全二叉树的高效排序算法一、什么是堆排序?
开发语言·算法·排序算法