目录
- 一、爬取目标
- 二、URL规律分析
- 三、HTML结构解析
- 四、完整代码实现
-
- 4.1 环境准备
- 4.2 完整代码
- 4.3 正则表达式逐行解析
- 五、数据清洗
-
- 5.1 导演/主演字段清洗
- 5.2 评价人数清洗
- 5.3 电影名清洗
- 六、进阶优化
-
- 6.1 随机User-Agent池
- 6.2 请求间隔控制
- 6.3 异常重试机制
- 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=
或者直接用循环:
for page in range(10):
url = f"https://movie.douban.com/top250?start={page * 25}&filter="

三、HTML结构解析
在动手写正则之前,必须先弄清楚目标数据在HTML中的位置。用Chrome打开豆瓣Top250,右键 →“检查”,定位到任意一部电影。
每一部电影都被包裹在 <div class="item"> 标签内,这是我们正则匹配的核心容器。其内部结构大致如下:
<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>
各字段的定位规律如下:
| 排名 | <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 环境准备
# 安装requests库(re是Python内置模块,无需额外安装)
pip install requests
4.2 完整代码
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 正则表达式逐行解析
我们来拆解这个核心正则表达式:
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 导演/主演字段清洗
原始数据类似:
导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯…
清洗步骤:
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 库动态生成:
from fake_useragent import UserAgent
ua = UserAgent()
headers = {
'User-Agent': ua.random, # 每次请求随机一个UA
# … 其他headers
}
安装:
pip install fake-useragent
6.2 请求间隔控制
在循环中设置随机间隔,模拟人类浏览行为:
import random
import time
# 每次请求后随机等待1-3秒
time.sleep(random.uniform(1, 3))
6.3 异常重试机制
网络请求可能因各种原因失败(超时、连接重置等),加入重试机制提高鲁棒性:
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"),可以暂停程序并提示用户手动处理:
if '验证' in html or 'captcha' in html.lower():
print("检测到验证码,请手动在浏览器中完成验证后按Enter继续…")
input()
# 更新Cookie后继续
七、结果展示与验证
运行上述代码后,会生成 douban_top250.csv 文件,内容如下:
| 1 | 肖申克的救赎 | 导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯… | 9.7 | 2963220 | 希望让人自由。 |
| 2 | 霸王别姬 | 导演: 陈凯歌 主演: 张国荣… | 9.6 | 2234567 | 风华绝代。 |
| 3 | 阿甘正传 | 导演: 罗伯特·泽米吉斯 主演: 汤姆·汉克斯… | 9.5 | 1987654 | 人生就像一盒巧克力。 |
验证要点:
八、常见问题与解决方案
| 返回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的全流程:
正则表达式虽然看起来复杂,但一旦掌握,就能精准地从任何文本中提取结构化数据,是爬虫工程师的必备技能。而豆瓣Top250作为经典的练手项目,能帮助你快速入门并建立信心。
接下来,你可以在这些数据的基础上做更多有趣的事情——比如用 matplotlib 绘制评分分布图,或者用 pandas 分析不同年代电影的评分趋势。爬虫只是第一步,数据分析才是更大的舞台!


