简介:基于Python爬虫的豆瓣影评分析爬取项目,是一套可直接用于课程设计或毕业设计的完整工程包,尤其适合计算机、人工智能、通信工程、自动化等专业学生。项目聚焦豆瓣影评的爬取、清洗、建模与可视化,覆盖从数据采集到情感分析的全流程,既能支撑毕设/课设的完整方案,也便于初学者按模块进阶学习。压缩包共69个文件,以46个Python脚本为主,涵盖爬虫工具、模型训练与评估、UI界面、数据预处理及可视化模块,另附5个Markdown说明文档、13张示例图片和1份docx设计报告,整体仅1.54MB,结构紧凑。已有42人下载学习,内容包含设计报告和环境配置说明,方便快速复现;模型端除基础爬虫外,还提供BERT、RNN、ERNIE等深度网络实现,可作为深度学习入门的实战范例。
1. 用Python爬虫做豆瓣影评分析:课程设计怎么选型才不算白做
“课程设计-基于Python爬虫的豆瓣影评分析爬取项目”是计算机、大数据、信息管理类专业里出现频率极高的题目。真正拉开差距的不是“能不能爬到”,而是能不能把“抓取—解析—入库—分析—可视化”讲成一条自洽的链路。很多同学卡在第一步就把requests写成了urllib,反爬一打就换库重来,最后交上去的代码连自己都跑不通第二遍。
这篇博文按一套可复现的方案来讲:用requests抓取、BeautifulSoup解析豆瓣短评页,pandas做清洗,jieba分词加snownlp做情感分析,再落到课程设计报告里的图表和答辩演示。豆瓣的移动端接口和网页端结构这些年一直在变,但短评列表页的HTML结构相对稳定,适合作为课程设计的抓取目标。适合人群是正在做课设、想抄一套能跑通的代码,或者想弄懂解析、反爬、分析环节各自的边界在哪的Python初学者。
2. 豆瓣影评爬虫的反爬边界与请求模型,先想清楚再写代码
2.1 豆瓣网页端的数据接口与短评分页结构
豆瓣影评数据在网页端主要有两个来源:电影主页的“短评”Tab,和独立的短评列表页https://movie.douban.com/subject/{电影ID}/comments?start={偏移量}&limit=20&status=P&sort=new_score。短评列表页每页20条,分页参数是start而不是page,这是初学者最容易写错的地方。limit固定为20,超出会被忽略或触发重定向到登录页。
网页端短评列表的HTML里,每条短评是div.comment-item节点,其中评论内容在span.short,评分在span.allstar50这类带allstar前缀的class里,title属性对应的就是星级数字。用户名在span.comment-info a,有用数在span.vote_count里。这套结构在过去几年基本没大改,课程设计阶段抓取这个页面够用,不推荐去碰移动端接口,那里的签名校验复杂得多。
需要注意,豆瓣未登录状态下访问短评列表页,翻到第10页左右就会出现“检测到有异常请求”的验证页,返回200但HTML里没有评论数据。课程设计一般抓一两个题材、每部电影抓前10页就足够分析,不要为了凑数据量硬翻到底。
2.2 请求头、Cookie与频率控制:课程设计够用的反爬应对
豆瓣的反爬主要靠请求频率、请求头特征和Cookie的综合判断。requests库没有默认UA,直接裸请求会拿到403。课程设计阶段只需要模拟一套完整的浏览器请求头,代码写在get_headers()函数里统一返回。
def get_headers(): return { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.5", "Referer": "https://movie.douban.com/", "Connection": "keep-alive", }请求头里最关键的是User-Agent和Referer。豆瓣对明显的脚本访问识别很直接,UA是爬虫库自带、Referer为空或指向首页,就会进入风控。Accept-Language带上zh-CN是为了避免拿到英文版页面,那样class名会变,解析逻辑全废。
频率控制是另一个硬指标。单线程time.sleep(2)基本安全,1秒以内连续请求很容易触发验证码。课程设计不需要并发,不要为了演示“效率高”而加线程池,被抓到验证码后处理成本远高于那几秒的提速收益。Cookie方面,首次访问会通过Set-Cookie下发bid和ll等字段,requests的Session对象会自动保存,用requests.Session()而不是直接requests.get()就能保持会话。
提示:不要为了绕过验证码去研究扫码登录、滑块模拟或验证码识别,课程设计的安全边界就是“低频、低量、模拟正常浏览器”。
2.3 robots.txt与合规边界:课程设计该怎么把握尺度
豆瓣的robots.txt明确禁止了部分路径的爬取。资料里要求“爬取豆瓣影评”,通常老师认可的做法是:公开页面、低频访问、数据仅用于课程分析,并且成果不公开发布、不用于商业用途。在报告里写清抓取范围、频率上限和数据用途,是合规表达的主要方式。
课程设计阶段不推荐维护IP代理池或动态切换UA,也不要用分布式爬虫的框架。一方面是课设体量用不上,另一方面是这类手段在校内答辩时容易被追问“合法性和必要性”。正确的做法是把精力放在请求间隔控制、失败重试、断点续抓这几个工程点上,既能体现工程意识,又能避免触碰反爬的边界。
3. 用requests+BeautifulSoup实现豆瓣影评爬取的最小可运行程序
3.1 抓取单部电影的影评条目:短评解析与字段抽取
先实现单页抓取和解析。目标URL里,subject/××××是豆瓣电影ID,start=0是首页。下面的代码抓取《肖申克的救赎》的短评列表页并解析出评论内容、评分、点赞数和用户名。
import time import requests from bs4 import BeautifulSoup SESSION = requests.Session() def fetch_comments_page(movie_id, start=0): url = f"https://movie.douban.com/subject/{movie_id}/comments" params = { "start": start, "limit": 20, "status": "P", "sort": "new_score", } resp = SESSION.get(url, params=params, headers=get_headers(), timeout=10) resp.raise_for_status() return resp.text def parse_comment_items(html_text): soup = BeautifulSoup(html_text, "html.parser") items = [] for item in soup.select("div.comment-item"): star_elem = item.select_one("span.allstar50, span.allstar40, span.allstar30, span.allstar20, span.allstar10") comment_elem = item.select_one("span.short") user_elem = item.select_one("span.comment-info a") vote_elem = item.select_one("span.vote_count") if comment_elem is None: continue items.append({ "user": user_elem.get_text(strip=True) if user_elem else "", "rating": star_elem["title"] if star_elem else "", "comment": comment_elem.get_text(strip=True), "votes": int(vote_elem.get_text(strip=True)) if vote_elem else 0, }) return items代码逻辑是:先用select按CSS选择器定位所有div.comment-item节点,再在每个节点内部二次查找星级、评论、用户名和有用数。span.allstar50需要写全5种星级的选择器,因为没评分时当前台用户选择“看过”后,有的短评不显示星星。取不到span.short时直接跳过,避免后续pandas数据处理时混入空值。get_text(strip=True)的作用是去掉文本两端空白,豆瓣短评里经常有换行和空格,不处理清洗时还要再做一步。
vote_count为空时是“没用”按钮里的数字为0,所以解析不到时直接填0而不是报错。这一处的容错逻辑在答辩时会被问到,属于典型的“真实页面与教科书页面不一样”的坑。
3.2 按ID列表批量爬取:多页翻页与错误重试
单页解析没问题后,补上翻页逻辑和异常重试。下面的main()函数遍历一部电影的前若干页,遇到请求异常时退避重试,成功后把解析结果累加到列表里。
import random from time import sleep def crawl_movie_comments(movie_id, max_pages=10): all_comments = [] for page in range(max_pages): start = page * 20 for attempt in range(3): try: html_text = fetch_comments_page(movie_id, start) items = parse_comment_items(html_text) if not items: print(f"第 {page + 1} 页无数据,可能触发验证码,停止翻页") return all_comments all_comments.extend(items) break except requests.RequestException as e: print(f"第 {page + 1} 页请求失败,重试第 {attempt + 1} 次:{e}") sleep(2 ** attempt) sleep(random.uniform(1.5, 3.0)) return all_comments if __name__ == "__main__": result = crawl_movie_comments("1292052", max_pages=10) print(f"共抓取 {len(result)} 条评论")翻页逻辑上,start按页数×20递增,第0页从start=0取第1~20条,第1页从start=20取第21~40条。返回空列表时主动停止而不是继续翻,因为豆瓣验证码页面也是200状态码,请求本身不报错,只能靠“没有解析出评论”来判断风控。
重试采用指数退避:第一次失败等1秒,第二次等2秒,第三次直接放弃这一页。random.uniform(1.5, 3.0)让每页抓取后的等待时间在1.5到3秒之间浮动,避免固定间隔被识别为脚本行为。这个随机等待在答辩时可以解释为“模拟人类阅读节奏”,属于学术意义上的技术合理性说明。
3.3 数据落盘:CSV和JSON两种持久化格式的选择
抓到的数据先落盘再分析,不要边爬边分析。CSV适合课程设计报告里的表格展示,JSON适合保留字段嵌套关系。
import json import csv def save_csv(comments, movie_id): with open(f"douban_comments_{movie_id}.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["user", "rating", "comment", "votes"]) writer.writeheader() writer.writerows(comments) def save_json(comments, movie_id): with open(f"douban_comments_{movie_id}.json", "w", encoding="utf-8") as f: json.dump(comments, f, ensure_ascii=False, indent=2)CSV必须用utf-8-sig编码,Excel直接打开UTF-8无BOM的CSV时中文会乱码。这是交付代码时最容易被忽视的细节。JSON用ensure_ascii=False保证中文可读,indent=2让报告里贴数据片段时不会挤成一团。
CSV和JSON选择逻辑:报告里要贴表格就用CSV,后续如果计划扩展抓取用户主页或短评回复,就用JSON保留扩展字段。课程设计推荐两者都落盘,多一个文件输出在答辩时显得数据流程完整。
4. 用pandas和jieba对豆瓣影评做清洗、分词与情感分析
4.1 影评数据清洗:去除空白、去重与繁体统一
爬下来的评论并不能直接进分析流程。豆瓣短评里常见的问题有四类:短时间内大量重复的“刷评”文本、标签符号残留、繁体中文与简体中文混用、评分字段为空。清洗逻辑写在clean_comments()里,用pandas的向量化操作一次处理完。
import pandas as pd def clean_comments(df): df = df.dropna(subset=["comment"]) df["comment"] = df["comment"].str.replace(r"<[^>]+>", "", regex=True) df["comment"] = df["comment"].str.replace(r"\s+", " ", regex=True) df["comment"] = df["comment"].str.strip() df = df.drop_duplicates(subset=["comment"], keep="first") return df清洗顺序有讲究:先去HTML标签(评论里可能出现二次转义后的<和>)、再压空白、最后去重。先dropna是因为后续还会按评论内容判断情感,空值直接剔除比填充更合理。drop_duplicates(subset=["comment"])保留第一条,去掉完全相同的重复评论,这类重复往往来自同一用户多次提交或复制粘贴刷屏。
繁体转简体不用自己写映射表,opencc库是通用解:
from opencc import OpenCC cc = OpenCC("t2s") def to_simplified(text): return cc.convert(text) df["comment"] = df["comment"].apply(to_simplified)t2s表示繁体转简体。为什么需要这一步?因为豆瓣用户群体里港台用户不少,热门电影的短评里繁体占比不低,jieba的分词词典对繁体的切分准确率不如简体,直接分词会导致高频词统计出现简繁两套词汇。
4.2 电影评分分布与评论数与评分的相关性
清洗后先做描述性统计。短评自带的rating是1到5的整数星,先看分布再决定要不要做评分预测类分析。
def rating_report(df): rating_counts = df["rating"].value_counts().sort_index() print(rating_counts) rating_ratio = df["rating"].value_counts(normalize=True).sort_index() print("各评分占比:") print(rating_ratio.round(4))value_counts(normalize=True)输出占比而不是绝对数量。课程设计报告里的常规结论是“评分集中在4星和5星”,但真正的信息量在于对比不同电影的分布形态:一部争议片的评分是U型分布,一部经典片是L型偏态分布。这个对比放到报告里比单张柱状图更有分析深度。
评论数维度的分析要另外统计:对每部电影统计总评论数、平均评分、评论长度均值和中位数。中位数比均值更抗长评干扰,短评里偶尔出现几百字的长评会拉高均值。
def comment_stats(df): df["length"] = df["comment"].str.len() stats = { "total_comments": len(df), "avg_rating": df["rating"].astype(float).mean(), "avg_length": df["length"].mean(), "median_length": df["length"].median(), } return pd.Series(stats)评分字段转成float再算均值,因为原始值是字符串。str.len()直接统计字符数,中文按字符算,一个汉字长度为1。这个字段后续还能用来分析“短评长度和评分高低有没有关系”,用df.groupby("rating")["length"].mean()一行就能出结果。
4.3 高频词提取与一句话情感分析:课程设计报告的核心图表
分词和情感分析是影评分析项目里最容易“看着高级”的部分。用jieba做精确模式分词,去掉停用词,统计词频前20位画柱状图;用snownlp对每条评论的情感值打分,按阈值分成正向、负向、中性。
import jieba STOP_WORDS = {"的", "了", "在", "是", "我", "有", "和", "就", "不", "人", "都", "一", "一个", "上", "也", "很"} def tokenize_comment(text): words = jieba.lcut(text) return [w for w in words if w.strip() and w not in STOP_WORDS and len(w) > 1] df["words"] = df["comment"].apply(tokenize_comment) all_words = [w for words in df["words"] for w in words] word_freq = pd.Series(all_words).value_counts().head(20)lcut返回List类型,len(w) > 1过滤掉单字,单字里大部分是语气词且没有分析价值。停用词表在课程设计里写一个30~50词的静态集合就够,不要引入几百KB的停用词库,答辩时解释不清。
snownlp的情感分析基于朴素贝叶斯训练,直接对短句打分:
from snownlp import SnowNLP def sentiment_score(text): try: return round(SnowNLP(text).sentiments, 2) except Exception: return None df["sentiment"] = df["comment"].head(100).apply(sentiment_score)只抽取前100条做情感分析,因为snownlp对每句话的推断需要几毫秒到几十毫秒,全量跑几千条数据在答辩演示时耗时会很明显。评分和情感值的相关系数可以用df["rating"].astype(float).corr(df["sentiment"])计算,一般情况下两者呈正相关,但不会太高。这里的分析结论在报告里写成“评论者的文本情绪与星级评分存在中等强度正相关”是有数据支撑的。
可视化推荐用matplotlib画词频柱状图,用pyecharts画情感分布饼图。pyecharts生成的是HTML文件,插入报告时可以直接截图。
5. 课程设计收尾:爬虫运行验证、报告要点与答辩演示
5.1 运行验证与重试策略的工程化检查
代码写完先跑通验证,再写报告。验证按下面的顺序执行,每步都有明确通过标准:
| 验证步骤 | 操作方式 | 通过标准 |
|---|---|---|
| 环境验证 | python -c "import requests, bs4, pandas, jieba, snownlp" | 无报错输出 |
| 单页抓取 | python crawl.py --movie 1292052 --pages 1 | 输出“共抓取 20 条评论” |
| 翻页抓取 | python crawl.py --movie 1292052 --pages 10 | 评论数在150~200之间,无验证码阻断 |
| 数据落盘 | 检查CSV首行和末行 | 首行为字段名,末行数据完整 |
| 清洗去重 | 对比清洗前后len(df) | 清洗后行数为原始行的85%以上 |
重试策略的工程化检查很简单:断网或把timeout设成0.1秒,看程序是否抛异常后按2秒间隔重试,三次后跳过这一页而不是崩溃退出。这一条在答辩时是被追问最多的工程细节,代码和输出日志都会成为加分项。
5.2 报告撰写与答辩演示的3个重点
课程设计报告不用把代码全贴进去,重点写三块:数据流程图、反爬应对表和结果分析图。数据流程图用Visio或draw.io画请求—解析—清洗—分析—可视化五层结构,每层标注使用的库名。反爬应对表列出User-Agent伪装、请求随机延时、失败重试三个策略,各配一句说明。结果分析图放评分分布柱状图、高频词Top20柱状图、情感饼图三张就够,每张图配三段以内的解读文字,写明“我发现什么—为什么这样—对分析意味着什么”。
答辩演示的节奏控制在5分钟以内:先跑crawl_movie_comments抓一部电影的前3页,现场展示数据量;再运行清洗和词频统计;最后打开CSV文件确认中文无乱码。演示前删掉代码里的调试用print,只保留进度输出。
答辩时提醒自己一句:课程设计的评分上限不取决于爬虫抓得多猛,而取决于对每一步“为什么这样做”的表达清晰度。把请求头参数、重试退避、停用词过滤这三点讲清楚,比多抓几部电影更有实际收益。
本文还有配套的精品资源,点击获取