【Python爬虫】豆瓣TOP250电影数据抓取实战指南(附完整Python代码)
·
本文将手把手教你如何用Python抓取豆瓣TOP250电影数据,涵盖反爬策略、数据清洗及可视化分析全流程,提供可直接运行的完整代码。
一、项目背景与价值
豆瓣TOP250是影视行业的黄金标准数据集,包含:
-
电影评分/评价人数(市场热度)
-
导演/演员信息(人才价值分析)
-
影片类型/地区/年份(市场趋势)
-
经典台词(NLP情感分析素材)
应用场景:
二、技术栈与环境配置
# 所需库安装 pip install requests beautifulsoup4 pandas numpy matplotlib seaborn
| 库名称 | 用途 | 版本要求 |
|---|---|---|
| requests | 网络请求 | >=2.25 |
| BeautifulSoup | HTML解析 | >=4.9 |
| pandas | 数据处理 | >=1.2 |
| fake_useragent | 伪装浏览器头 | >=1.1 |
| time | 请求延时 | 内置 |
三、网页结构深度解析
打开豆瓣TOP250页面(https://movie.douban.com/top250)按F12观察:
-
数据容器:
html
<div class="item"> <!-- 单部电影区块 --> <div class="info"> <span class="title">肖申克的救赎</span> <p class="">导演: 弗兰克·德拉邦特</p> <div class="star"> <!-- 评分区域 --> <span class="rating_num">9.7</span> </div> </div> </div> -
翻页机制:
text
第1页: start=0 第2页: start=25 第3页: start=50 ... 第10页: start=225
四、反爬策略突破方案
豆瓣的反爬机制包括:
-
UA检测 - 使用随机User-Agent:
from fake_useragent import UserAgent headers = {'User-Agent': UserAgent().random} -
请求频率限制 - 添加随机延迟:
import random time.sleep(random.uniform(1.5, 3.5))
-
IP封锁 - 使用代理IP池(示例):
proxies = { 'http': 'http://12.34.56.78:8888', 'https': 'https://12.34.56.78:8888' } response = requests.get(url, headers=headers, proxies=proxies)
五、完整爬虫代码实现
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import random
import re
from fake_useragent import UserAgent
def scrape_douban_top250():
"""
修复版豆瓣TOP250电影数据抓取函数
"""
base_url = "https://movie.douban.com/top250?start={}"
movies = []
ua = UserAgent()
for page in range(0, 250, 25):
url = base_url.format(page)
headers = {
'User-Agent': ua.random,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Connection': 'keep-alive',
'Referer': 'https://movie.douban.com/',
'Cookie': 'bid=123456789;' # 添加简单cookie
}
try:
print(f"正在抓取第{page//25 + 1}页数据...")
response = requests.get(url, headers=headers, timeout=15)
response.encoding = 'utf-8'
# 检查响应状态
if response.status_code != 200:
print(f"请求失败,状态码: {response.status_code}")
continue
soup = BeautifulSoup(response.text, 'html.parser')
# 使用更健壮的选择器定位电影项目
grid_view = soup.find('ol', class_='grid_view')
if not grid_view:
print("未找到电影列表,可能页面结构变化")
continue
items = grid_view.find_all('li')
for item in items:
# 排名
rank = item.find('em').text if item.find('em') else "N/A"
# 标题
title_elem = item.find('span', class_='title')
title = title_elem.text.strip() if title_elem else "未知标题"
# 其他信息
other_title = item.find('span', class_='other').text.strip() if item.find('span', class_='other') else ""
# 信息部分
bd = item.find('div', class_='bd')
if not bd:
continue
info_text = bd.find('p').get_text(strip=True).replace('\xa0', ' ') if bd.find('p') else ""
# 导演信息
director = ""
if "导演:" in info_text:
director = info_text.split("导演:")[1].split("主演")[0].strip().split('/')[0]
# 年份
year = ""
if re.search(r'\d{4}', info_text):
year = re.search(r'\d{4}', info_text).group(0)
# 地区
region = ""
if "/" in info_text:
parts = info_text.split("/")
if len(parts) >= 2:
region = parts[-2].strip()
# 类型
genre = ""
if "/" in info_text:
parts = info_text.split("/")
if len(parts) >= 1:
genre = parts[-1].strip()
# 评分
rating_elem = item.find('span', class_='rating_num')
rating = rating_elem.text if rating_elem else "0.0"
# 评价人数
votes = "0"
star_div = item.find('div', class_='star')
if star_div:
spans = star_div.find_all('span')
if len(spans) >= 4:
votes = spans[3].text.replace('人评价', '')
# 经典台词
quote_elem = item.find('span', class_='inq')
quote = quote_elem.text if quote_elem else ""
# 添加到电影列表
movies.append({
'排名': rank,
'标题': title,
'其他标题': other_title,
'导演': director,
'年份': year,
'地区': region,
'类型': genre,
'评分': float(rating) if rating.replace('.', '', 1).isdigit() else 0.0,
'评价人数': int(votes.replace(',', '')) if votes.isdigit() else 0,
'经典台词': quote
})
print(f"成功抓取第{page//25 + 1}页数据,共{len(movies)}条记录")
# 随机延迟防止被封
delay = random.uniform(3, 7)
time.sleep(delay)
except Exception as e:
print(f"第{page//25 + 1}页抓取失败: {str(e)}")
return pd.DataFrame(movies)
# 执行抓取
df = scrape_douban_top250()
# 数据清洗(仅在DataFrame不为空时执行)
if not df.empty:
# 尝试转换年份为整数
df['年份'] = pd.to_numeric(df['年份'], errors='coerce').fillna(0).astype(int)
# 保存数据
df.to_csv('douban_top250.csv', index=False, encoding='utf-8-sig')
print(f"数据抓取完成,共{len(df)}条记录,已保存到 douban_top250.csv")
# 显示前5条数据
print("\n抓取的前5部电影:")
print(df.head())
else:
print("未能抓取到任何数据,请检查网络连接或反爬机制")
代码关键点:
-
使用
random.uniform()实现随机延时 -
try-except块捕获请求异常 -
多层CSS选择器精准定位数据
-
数据清洗与类型转换(评分转float,票数转int)
六、数据清洗技巧
原始数据需进行:
# 1. 处理年份异常值
df['year'] = df['year'].apply(lambda x: re.search(r'\d{4}', x).group() if re.search(r'\d{4}', x) else None)
# 2. 导演信息拆分
df['director'] = df['director'].str.split('/').str[0]
# 3. 处理缺失值
df['quote'] = df['quote'].fillna('无')
七、数据可视化分析
使用Matplotlib+Seaborn进行深度分析:
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(15,10))
# 1. 评分分布直方图
plt.subplot(2,2,1)
sns.histplot(df['rating'], bins=20, kde=True)
plt.title('豆瓣TOP250评分分布')
# 2. 年代趋势分析
plt.subplot(2,2,2)
sns.countplot(x='year', data=df, order=df['year'].value_counts().index[:10])
plt.xticks(rotation=45)
plt.title('电影年代分布TOP10')
# 3. 导演作品数量TOP10
plt.subplot(2,2,3)
top_directors = df['director'].value_counts().head(10)
sns.barplot(x=top_directors.values, y=top_directors.index)
plt.title('导演上榜作品数量TOP10')
# 4. 评分与投票数关系
plt.subplot(2,2,4)
sns.scatterplot(x='votes', y='rating', data=df, hue='rating', palette='viridis')
plt.xscale('log')
plt.title('评分与投票量关系(对数坐标)')
plt.tight_layout()
plt.savefig('douban_analysis.png', dpi=300)
可视化结果解读:
-
评分呈现左偏分布(均值8.9,最低8.3)
-
1994-2004是电影黄金年代(《肖申克》《阿甘正传》等)
-
宫崎骏以5部作品成最大赢家
-
投票数>150万的影片评分均>9.0
八、常见问题解决方案
| 问题现象 | 解决方案 |
|---|---|
| 返回403 Forbidden | 1. 升级UserAgent库 2. 增加代理IP |
| 数据部分缺失 | 添加备用CSS选择路径 |
| 抓取速度过慢 | 调整延时为1-2秒 |
| pandas保存中文乱码 | 使用encoding='utf-8-sig' |
| 连接超时 | 增加重试机制+超时设置 |
九、完整数据样例(前5条)
| 电影名称 | 年份 | 导演 | 评分 | 投票数(万) | 经典台词 |
|---|---|---|---|---|---|
| 肖申克的救赎 | 1994 | 弗兰克·德拉邦特 | 9.7 | 283 | 希望是美好的,也许是人间至善 |
| 霸王别姬 | 1993 | 陈凯歌 | 9.6 | 201 | 说的是一辈子!差一年... |
| 阿甘正传 | 1994 | 罗伯特·泽米吉斯 | 9.5 | 192 | 生活就像一盒巧克力... |
| 泰坦尼克号 | 1997 | 詹姆斯·卡梅隆 | 9.4 | 188 | 你跳,我也跳 |
| 这个杀手不太冷 | 1994 | 吕克·贝松 | 9.4 | 210 | 人生总是这么苦吗? |
十、项目扩展方向
-
构建推荐系统:
from sklearn.feature_extraction.text import TfidfVectorizer tfidf = TfidfVectorizer(stop_words='english') tfidf_matrix = tfidf.fit_transform(df['quote'])
-
自动化更新机制:
-
设置定时任务(crontab/Airflow)
-
增量抓取新上榜电影
-
结语
本文实现了从数据抓取到可视化分析的全流程,关键点在于:
-
动态UA+IP代理突破反爬
-
健壮的异常处理机制
-
数据深度清洗技巧
-
多维度的数据洞察
更多推荐
所有评论(0)