1. 项目概述:爬取豆瓣电影评论的实用价值
爬取豆瓣电影评论是Python学习者最常接触的实战项目之一。作为国内最具影响力的影视评分平台,豆瓣积累了海量真实用户生成的影评数据。这些数据对电影市场分析、观众情感倾向研究、影视推荐算法优化都具有重要价值。
我最初接触这个项目是在2016年,当时需要为某影视公司做市场调研。手动收集数据效率太低,于是开始研究用Python自动化获取评论。经过多次迭代,现在这套方法已经相当成熟,可以帮助你快速获取结构化评论数据。
这个项目适合:
- Python初学者想通过实战巩固requests、BeautifulSoup等库的使用
- 数据分析师需要获取原始评论数据进行情感分析
- 影视从业者希望了解观众对某部电影的真实反馈
- 任何对网络数据采集感兴趣的学习者
2. 技术选型与工具准备
2.1 为什么选择Python做爬虫?
Python在爬虫领域有不可替代的优势:
- 丰富的库生态:requests、BeautifulSoup、Scrapy等成熟工具链
- 简洁的语法:相比Java等语言,代码更易写易读
- 强大的数据处理能力:获取的数据可直接用pandas等库分析
我尝试过用Node.js和Go写爬虫,但最终都回归Python,因为调试和修改效率更高。特别是处理反爬机制时,Python可以快速调整策略。
2.2 核心工具安装
pip install requests beautifulsoup4 pandas- requests:比urllib更人性化的HTTP库
- BeautifulSoup4:HTML解析神器
- pandas:数据清洗和存储的瑞士军刀
注意:建议使用Python 3.7+版本,避免某些库的兼容性问题。我曾在3.5环境遇到bs4的解析异常,升级后解决。
2.3 开发环境配置
推荐使用VS Code + Python插件:
- 安装Python扩展
- 创建虚拟环境:python -m venv venv
- 激活环境后安装上述依赖
我习惯用Jupyter Notebook做初步测试,确认爬取逻辑无误后再移植到.py文件。这样比直接写脚本效率更高。
3. 豆瓣网页结构分析
3.1 评论页面URL规律
以《流浪地球2》为例:
https://movie.douban.com/subject/35267208/comments?start=0&limit=20&status=P&sort=new_score关键参数:
- start:分页起始位置
- limit:每页显示条数(最大100)
- sort:排序方式(new_score最新热门)
通过修改start值即可翻页:
- 第1页:start=0
- 第2页:start=20
- ...
3.2 HTML标签结构
使用Chrome开发者工具(F12)检查元素:
- 每条评论包裹在
<div class="comment-item">中 - 用户名在
<span class="comment-info">下的a标签 - 评分藏在
<span class="rating"的title属性 - 评论内容在
<span class="short">
这种结构相对规整,比某些动态渲染的网站容易抓取。但要注意豆瓣偶尔会调整页面结构,需要定期检查选择器是否仍然有效。
4. 爬虫核心代码实现
4.1 基础爬取函数
import requests from bs4 import BeautifulSoup import time import random def fetch_comments(movie_id, page=1): url = f"https://movie.douban.com/subject/{movie_id}/comments" params = { "start": (page-1)*20, "limit": 20, "sort": "new_score" } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..." } try: response = requests.get(url, params=params, headers=headers) response.raise_for_status() return response.text except Exception as e: print(f"请求失败: {e}") return None关键点:
- 必须设置User-Agent模拟浏览器
- 使用try-except捕获网络异常
- 通过params参数构造查询字符串
4.2 评论数据解析
def parse_comments(html): soup = BeautifulSoup(html, 'html.parser') comments = [] for item in soup.select('div.comment-item'): try: user = item.select_one('span.comment-info > a').text.strip() rating = item.select_one('span.rating')['title'] if item.select_one('span.rating') else '无评分' content = item.select_one('span.short').text.strip() date = item.select_one('span.comment-time')['title'] comments.append({ "user": user, "rating": rating, "content": content, "date": date }) except Exception as e: print(f"解析异常: {e}") continue return comments解析技巧:
- 使用CSS选择器比find_all更简洁
- 每个字段都做strip()处理去除空白字符
- 对可能不存在的元素(如评分)做判空处理
4.3 分页爬取与数据存储
def crawl_douban_comments(movie_id, max_page=5): all_comments = [] for page in range(1, max_page+1): print(f"正在爬取第{page}页...") html = fetch_comments(movie_id, page) if html: comments = parse_comments(html) all_comments.extend(comments) # 随机延迟防止被封 time.sleep(random.uniform(1, 3)) # 保存为CSV df = pd.DataFrame(all_comments) df.to_csv(f'douban_comments_{movie_id}.csv', index=False) return df注意事项:
- 设置合理的爬取间隔(1-3秒)
- 使用random增加请求间隔的随机性
- 及时保存数据防止意外中断丢失
5. 反爬机制应对策略
5.1 常见反爬手段
豆瓣会检测以下异常行为:
- 高频请求(每分钟超过30次)
- 无User-Agent或使用明显爬虫UA
- 请求参数异常
- 来自同一IP的持续访问
我曾在测试时连续爬取50页,结果IP被暂时封禁2小时。后来调整策略后基本没再遇到问题。
5.2 实战应对方案
- 请求头完善:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...", "Referer": "https://movie.douban.com/", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive" }- 代理IP池方案(需自行搭建或购买服务):
proxies = { "http": "http://user:pass@ip:port", "https": "http://user:pass@ip:port" } response = requests.get(url, proxies=proxies)- 自动化Cookies管理:
session = requests.Session() # 先访问一次首页获取cookies session.get("https://www.douban.com") # 后续请求自动携带cookies response = session.get(comment_url)6. 数据清洗与分析示例
6.1 常见数据问题
原始数据可能存在:
- HTML实体编码(如 &)
- 特殊表情符号/颜文字
- 无意义的换行和空格
- 评分为文字描述("力荐"、"推荐"等)
6.2 清洗代码示例
import pandas as pd import html def clean_data(df): # 转换HTML实体 df['content'] = df['content'].apply(html.unescape) # 评分文字转数字 rating_map = { '力荐': 5, '推荐': 4, '还行': 3, '较差': 2, '很差': 1, '无评分': None } df['rating_num'] = df['rating'].map(rating_map) # 去除首尾空白 df['user'] = df['user'].str.strip() return df6.3 简单分析示例
# 评分分布 rating_dist = df['rating_num'].value_counts().sort_index() # 词云生成 from wordcloud import WordCloud text = ' '.join(df['content'].tolist()) wordcloud = WordCloud(font_path='msyh.ttc').generate(text)7. 项目优化方向
7.1 异步爬取加速
使用aiohttp替代requests:
import aiohttp import asyncio async def fetch_page(session, url): async with session.get(url) as response: return await response.text() async def main(urls): async with aiohttp.ClientSession() as session: tasks = [fetch_page(session, url) for url in urls] return await asyncio.gather(*tasks)实测异步方式能提升3-5倍效率,但要注意控制并发量避免被封。
7.2 数据持久化方案
除了CSV,还可以考虑:
- MongoDB:适合非结构化数据
- MySQL:便于复杂查询
- Elasticsearch:支持全文检索
我通常根据后续使用场景选择:
- 简单分析用CSV
- 长期存储用MySQL
- 文本搜索用Elasticsearch
7.3 定时增量爬取
使用APScheduler实现定时任务:
from apscheduler.schedulers.blocking import BlockingScheduler def job(): # 获取上次最后一条评论的时间 last_date = get_last_comment_date() # 只爬取新评论 crawl_new_comments(since=last_date) scheduler = BlockingScheduler() scheduler.add_job(job, 'interval', hours=6) scheduler.start()8. 法律与道德注意事项
遵守robots.txt协议:
- 豆瓣的robots.txt对部分目录有限制
- 建议设置合理的爬取间隔
数据使用限制:
- 禁止商业化使用(除非获得授权)
- 学术研究需注明数据来源
个人隐私保护:
- 不要公开用户个人信息
- 对敏感内容做脱敏处理
我在实际项目中会严格控制爬取量,通常单次不超过1000条评论,且数据仅用于分析统计,从不存储用户个人信息。
9. 常见问题与解决方案
9.1 返回403错误
可能原因:
- 请求头不完整
- Cookies验证失败
- IP被暂时封禁
解决方案:
- 检查User-Agent是否设置
- 添加Referer等头部信息
- 更换IP或等待1-2小时
9.2 数据解析为空
可能原因:
- 页面结构已更新
- 触发反爬返回验证页
- 网络问题导致HTML不完整
调试步骤:
- 保存原始HTML检查结构
- 测试CSS选择器是否有效
- 检查是否出现验证码
9.3 编码问题
常见错误:
- 'gbk' codec can't encode character...
- 中文显示为乱码
解决方法:
# 设置正确编码 response.encoding = 'utf-8' # 或者手动处理 html = response.content.decode('utf-8')10. 个人实战经验分享
关于爬取频率:
- 工作日白天可以稍快(2-3秒/次)
- 晚上和周末建议放慢(5-10秒/次)
- 遇到验证码立即暂停1小时
数据存储技巧:
- 每爬取50条就写入文件一次
- 使用pickle保存中间状态
- 文件名包含爬取时间戳
调试建议:
- 先用单页测试解析逻辑
- 保存异常页面供后期分析
- 使用logging记录运行日志
我维护的这个爬虫已经稳定运行3年,核心经验就是:慢就是快。与其追求速度被ban,不如稳定获取数据。最近一次升级是添加了自动识别验证码的功能,发现验证码就自动切换代理并降低频率。