目录
- 一、爬取目标
- 二、URL规律分析
- 三、HTML结构解析
- 四、完整代码实现
-
- [4.1 环境准备](#4.1 环境准备)
- [4.2 完整代码](#4.2 完整代码)
- [4.3 正则表达式逐行解析](#4.3 正则表达式逐行解析)
- 五、数据清洗
-
- [5.1 导演/主演字段清洗](#5.1 导演/主演字段清洗)
- [5.2 评价人数清洗](#5.2 评价人数清洗)
- [5.3 电影名清洗](#5.3 电影名清洗)
- 六、进阶优化
-
- [6.1 随机User-Agent池](#6.1 随机User-Agent池)
- [6.2 请求间隔控制](#6.2 请求间隔控制)
- [6.3 异常重试机制](#6.3 异常重试机制)
- [6.4 处理验证码](#6.4 处理验证码)
- 七、结果展示与验证
- 八、常见问题与解决方案
- 九、总结
一、爬取目标
我们的目标非常明确:获取豆瓣电影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="">
导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯...<br>
1994 / 美国 / 犯罪 剧情
</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')按名称取值,比数字索引更清晰。.*?是非贪婪匹配,尽可能少地匹配字符,避免跨过多个标签造成误匹配。[^&]*?用于匹配电影名,[^&]表示匹配除&以外的任意字符------因为豆瓣电影名中可能包含 等HTML实体,用这个可以更精准地截断。re.S标志让.能够匹配换行符,因为HTML源码中标签之间往往有换行和缩进。
五、数据清洗
从HTML中提取的数据往往带有杂质,需要进行清洗:
5.1 导演/主演字段清洗
原始数据类似:
导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯...
清洗步骤:
- 用
.strip()去除首尾空白 - 用
.replace('\xa0', ' ')将 替换为普通空格(\xa0是 的Unicode表示) - 用
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实体(如 "),但豆瓣的 .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 | 人生就像一盒巧克力。 |
验证要点:
- 数量验证:最终是否正好250条?如果有缺失,检查对应页面的正则是否匹配。
- 字段完整性:每条记录是否都有完整的6个字段?简介字段可能为空(部分电影无短评),需要容错处理。
- 数据准确性:随机抽查几条,与网页实际数据对比。
八、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 返回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的全流程:
- URL规律分析 :
start参数每页递增25,共10页 - 请求发送:携带User-Agent伪装浏览器,处理异常和重试
- 数据提取:用正则表达式的命名分组精确定位各字段
- 数据清洗:去除HTML杂质,格式化数据
- 数据存储 :用CSV模块保存,使用
utf-8-sig编码避免中文乱码 - 进阶优化:随机UA、请求间隔、异常重试
正则表达式虽然看起来复杂,但一旦掌握,就能精准地从任何文本中提取结构化数据,是爬虫工程师的必备技能。而豆瓣Top250作为经典的练手项目,能帮助你快速入门并建立信心。
接下来,你可以在这些数据的基础上做更多有趣的事情------比如用 matplotlib 绘制评分分布图,或者用 pandas 分析不同年代电影的评分趋势。爬虫只是第一步,数据分析才是更大的舞台!