
简介基于Python的豆瓣电影评论情感分析与关键词提取WordCloud设计源码面向对自然语言处理、文本挖掘或数据可视化感兴趣的Python学习者与研究者。项目覆盖评论数据预处理、情感分类模型训练、TF-IDF/TextRank关键词提取最终生成词云图与情感分布、趋势分析报告便于快速把握评论的整体情感倾向与热点话题。压缩包共39个文件含main.py主程序、19张PNG图片、6份xlsx分析报告、4个xml配置文件、2个otf字体文件以及停用词等辅助文本整体约44.31MB。已有182人学习下载。通过源码可完整了解豆瓣电影评论情感分析流程理解NLTK、pandas、scikit-learn等库的实际应用还可以直接替换目标电影评论进行复现适合作为课程设计或入门级NLP项目的参考实现。1. 豆瓣打分救不了选片用Python把评论区拆开看豆瓣8.2分的电影你花两小时看完却想骂人。评论区里“神作”和“烂片”两拨人吵得不可开交而评分是这两拨人的平均值。只看分数你永远看不到这种撕裂。基于Python的豆瓣电影评论情感分析与关键词提取wordcloud设计要解决的正是这个问题把短评抓下来用情感分析判断每条文本是正还是负再用关键词提取找出两拨人各自在吵什么最后用wordcloud把高频词画成一张图。整个过程用到的Python基础语法不多但把爬虫、文本清洗、分词、统计、可视化串成了一条完整链路。适合刚学完Python基础、想拿真实项目练手的人也适合做短文本舆情分析但没跑通过全流程的从业者。下面这套实现按“能直接复现”的标准写参数和坑都标在对应位置。2. 技术栈与整体链路为什么选SnowNLPTF-IDF而不是堆大模型2.1 一条短文本舆情链路到底要经过几个环节豆瓣短评分析看起来是个小工具但它天然就是一条完整的短文本舆情处理流水线完整链路是采集 → 清洗 → 分词 → 情感分析 → 关键词提取 → 词云可视化。六个环节顺序固定前一个的输出是后一个的输入少一个环节后面就出问题。很多新手一上来就写一个脚本爬完直接扔给SnowNLP结果分词错误、html标签混进关键词、词云里全是“https”这样的噪音。把环节拆开有三个好处排错时能定位到具体模块每个环节的中间结果可以单独拿出来检查后面想换掉某个算法不影响其他环节。现在很多资料都在讲从文本情感分析到多模态情感分析宣传视频弹幕、图像评论一起分析但豆瓣短评最合适的输入就是纯文本先把文本这条链路跑通比追概念重要得多。想搭短文本舆情情感倾向分析系统设计与可视化的小团队也可以先把这个最小闭环跑通再考虑存储和前端界面而不是一上来就上大数据平台。2.2 核心选型requestsBeautifulSoup、jieba、SnowNLP、wordcloud这五个组件的选型逻辑如下表环节选型理由替代方案数据获取requests BeautifulSoup豆瓣短评页是静态HTML这两个库依赖少、易调试Scrapy规则复杂或需要并发时替换分词jieba中文分词事实标准支持自定义词典哈工大LTP、pkuseg精度更高但配置重情感分析SnowNLP开箱即用几十行代码跑通无需标注数据微调BERT、大模型API成本高出几个量级关键词jieba.analyse自带TF-IDF和TextRank两种实现直接在分词结果上工作LDA主题模型适合发现隐性主题可视化wordcloud中文词云参数成熟掩膜、字体、配色都可控pyecharts、plotly交互更强但对本项目过重SnowNLP是一个典型黑匣子模型内部是贝叶斯分类器训练语料偏电商和微博。很多人知道它是黑匣子就放弃这不对。在入门阶段黑匣子反而是优势你不用先经历数据标注、特征工程的漫长过程几十行代码就能看到情感分析结果长什么样跑通之后再决定要不要换更重的模型。数据分析与可视化这个方向第一版方案的首要目标是让整条链路通起来而不是在单个环节追求SOTA。2.3 环境准备与项目文件怎么摆先说python安装时最常见的坑安装包解压完命令行敲python却提示找不到命令基本是安装时没勾选Add to PATH。这个问题能让新手卡一下午。装好之后检查版本python --version pip --version然后安装第三方库。这五个库是核心依赖pip install requests beautifulsoup4 jieba snownlp wordcloud matplotlib pandas在PyCharm里新建项目时右下角解释器要选到python 3.8及以上版本这是pycharm配置python环境最常见的踩坑点——解释器选错pip装好的库在这个项目里全部import不到。项目文件我建议按功能拆成下面这样别一个main.py塞几千行douban_analysis/ ├── main.py # 主流程调度下面几个模块 ├── spider.py # 采集与解析评论 ├── clean.py # 文本清洗 ├── analyze.py # 分词、情感分析、关键词提取 ├── wordcloud_gen.py # 词云生成 ├── stopwords.txt # 中文停用词表 └── output/ ├── comments.csv └── wordcloud.png文件按职责拆分任何一个环节出问题直接看对应文件不需要通读所有代码。后面几章我按这个结构逐个写实现你可以直接复制对应文件的内容。3. 豆瓣电影评论采集与清洗requests请求参数和一段能跑的解析代码3.1 豆瓣短评接口的请求边界URL参数、UA与频率控制豆瓣电影短评页的URL格式是https://movie.douban.com/subject/{movie_id}/commentsmovie_id就是豆瓣页面网址里subject/后面那串数字比如《流浪地球2》是34874432。这个页面接受四个查询参数参数取值示例作用start0, 20, 40分页偏移量每页20条limit20单页返回条数保持默认即可sortnew_score / hotnew_score按最新hot按热门statusP只看“看过”状态的短评请求头里最重要的是User-Agent和Cookie。豆瓣对裸UA的请求拦截概率很高带上浏览器UA还不够访问评论页面通常要求登录状态所以要把自己登录后的Cookie从浏览器开发者工具里复制出来填进去。这里有一个必须强调的边界采集频率控制在1秒1条左右单次采集页数不要多。豆瓣的反爬识别核心是频率特征不是UA有多像浏览器高频请求必然触发拦截。这个项目定位是学习用途拿到数据做文本分析不涉及商业使用控制好频率是对目标站点的基本尊重也会让你的脚本稳定得多。后面遇到403或418先检查是不是采集太快。3.2 用requestsBeautifulSoup拉取并解析短评列表的代码spider.py的核心实现如下import time import random import requests from bs4 import BeautifulSoup def fetch_douban_comments(movie_id: str, max_pages: int 5) - list: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Cookie: 把你的登录Cookie填到这里, Referer: fhttps://movie.douban.com/subject/{movie_id}/, } comments [] for page in range(max_pages): params { start: page * 20, limit: 20, status: P, sort: new_score, } url fhttps://movie.douban.com/subject/{movie_id}/comments resp requests.get(url, paramsparams, headersheaders, timeout10) if resp.status_code ! 200: print(f第{page 1}页请求失败状态码: {resp.status_code}) break resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) items soup.select(.comment-item .short) if not items: print(没有解析到评论页面结构可能发生了变化) break comments.extend([item.get_text(stripTrue) for item in items]) time.sleep(random.uniform(1.0, 2.0)) return comments这段代码的逻辑是用page变量控制分页每页请求后把解析出的短评文本追加到comments列表最后统一返回。分页的核心在startpage * 20豆瓣每页固定20条第二页从第20条开始第三页从第40条开始这个偏移量算错会导致重复抓取或漏抓。两个关键参数需要说明timeout10是必须的默认的无限等待会让脚本卡在某个失效请求上random.uniform(1.0, 2.0)是每页之间的随机睡眠比固定sleep(1)更接近人类浏览节奏也降低了频率特征被识别的概率。解析用的选择器.comment-item .short对应短评列表的HTML结构豆瓣改版结构变化时打开页面按F12看一眼实际class名改成对应值即可。3.3 文本清洗把“不看了/快进”变成可分析的干净语料爬下来的原始文本里混着URL、用户名、多余的空白符这些内容不是情感词如果不清洗分词后会产生http、www、某某这类噪声token后面TF-IDF计算时它们甚至会占据权重前列。清洗代码很简单但位置很关键必须在分词之前做import re def clean_text(text: str) - str: text re.sub(rhttps?://\S, , text) text re.sub(r\w, , text) text re.sub(r[^], , text) text re.sub(r\s, , text) return text.strip() cleaned_comments [clean_text(c) for c in comments]四个正则分别处理一类问题https?://\S匹配以http或https开头的链接\w匹配用户名[^]是防御性的防止某些内容里残留html标签\s把多个连续空白压缩成单个空格。最后strip()去掉首尾空白。清洗完建议打印前10条看一眼确认没有明显噪声再往下走这一步不值钱但能省后面大量排查时间。把清洗结果连同样本来源一并落盘方便后面分析时回溯import pandas as pd df pd.DataFrame({ raw: comments, cleaned: cleaned_comments, source: douban }) df.to_csv(output/comments.csv, indexFalse, encodingutf-8-sig)保存用utf-8-sig编码是为了让Excel打开csv时中文不乱码这是Windows环境下项目脚手架常见的坑提前规避。4. 情感分析与关键词提取从jieba分词到SnowNLP阈值的完整实现4.1 分词与停用词为什么单字要过滤自定义词典放哪jieba分词是整个文本分析流程的地基情感分析和关键词提取都建立在分词结果之上。分词这一步如果产生大量错误切分后面的权重计算全部失真。基础用法很简单import jieba import pandas as pd def load_stopwords(path: str stopwords.txt) - set: with open(path, r, encodingutf-8) as f: return {line.strip() for line in f if line.strip()} stopwords load_stopwords() df pd.read_csv(output/comments.csv) def tokenize(text: str): words jieba.lcut(text) return [w for w in words if w.strip() and w not in stopwords and len(w) 1] df[tokens] df[cleaned].apply(tokenize)len(w) 1过滤单字这是从实践里总结出来的经验中文里“烂”“好”“哭”这类单字情感词有一定信息量但在影评场景下会被“的”“了”“啊”这类无意义单字淹没直接过滤掉换来更干净的词频统计。停用词表用网上常见的通用中文停用词表即可网上搜“中文停用词表”能拿到哈工大等来源的版本下载后放到项目根目录。上面的判断条件里w.strip()在文本清洗已经被执行过一次这里再写是双保险防止有些分词结果本身就是空白字符。如果跑完分词发现“流浪地球”“满江红”这类专有名词被切成“流浪”和“地球”需要在分析前加载自定义词典jieba.load_userdict(userdict.txt)userdict.txt每行一个词格式是“词语 词频 词性”词频和词性可以省略流浪地球2 5 nt 满江红 5 nt 吴京 5 nr自定义词典是jieba提升短文本分词精度成本最低的手段比换分词模型划算得多。电影名、演员名、网络流行词“下饭”“yyds”这类都放到这里。4.2 SnowNLP情感评分与阈值设定0.5只是一个起点情感分析是整个项目里最容易被误用的环节。SnowNLP的sentiments属性返回0到1之间的浮点数越大越正面0.5被默认为中性分界。但实际跑豆瓣影评直接用0.5分正负会得到一个让你怀疑人生的结果大量“烂片”评论被分到正面。原因在于SnowNLP训练语料偏电商和微博影评里满是反讽、黑话和夸张表达模型整体评分偏乐观。我一般会先跑完整批评论看分布再定阈值from snownlp import SnowNLP def sentiment_score(text: str) - float: return SnowNLP(text).sentiments df[sentiment] df[cleaned].apply(sentiment_score) # 先看整体分布再决定阈值 print(df[sentiment].describe())describe()输出的分位数信息比均值有用。如果0.25分位数是0.55说明四分之一以上的评论都被打到了0.55以上0.5作为分界明显偏低。常见做法是取0.6和0.4分别作为正负阈值中间算中性pos df[df[sentiment] 0.6] neu df[(df[sentiment] 0.4) (df[sentiment] 0.6)] neg df[df[sentiment] 0.4] total len(df) print(f正面占比: {len(pos) / total:.1%}) print(f中性占比: {len(neu) / total:.1%}) print(f负面占比: {len(neg) / total:.1%})分桶统计比打印均值更能暴露两极分化而这个项目要的就是两极分化。阈值不是固定值每换一部电影、每换一批评论都应该重新看一眼分布再定。这个“先看分布再定阈值”的步骤比纠结选哪个情感分析模型重要得多。注意SnowNLP的输入是清洗后的整条文本不用分词。它内部有自己的分词逻辑外部分词只用于关键词提取别把词列表传进去。4.3 关键词提取与词云生成TF-IDF的topK怎么定关键词提取用jieba.analyse的TF-IDF实现这是jieba自带模块不需要额外安装。把清洗后的整批评论拼成一个长文本传入import jieba.analyse all_text .join(df[cleaned].tolist()) keywords jieba.analyse.extract_tags( all_text, topK30, withWeightTrue, ) for word, weight in keywords: print(f{word}: {weight:.4f})选择TF-IDF而不是TextRank是因为影评短文本里情感词和片名的区分度高TF-IDF对这类显式特征更直接。TextRank适合长文本的主题发现用在短评上反而会放大共现噪声。topK30是经验值词太少信息量不够太多词云会糊。withWeightTrue可以拿到权重既用于排序也可以映射到词云的字体大小。拿到关键词后生成基础版中文词云from wordcloud import WordCloud import matplotlib.pyplot as plt word_freq {word: weight for word, weight in keywords} wc WordCloud( font_pathC:/Windows/Fonts/msyh.ttc, background_colorwhite, width800, height600, max_words100, random_state42, ).generate_from_frequencies(word_freq) plt.figure(figsize(10, 8)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(output/wordcloud.png, dpi150, bbox_inchestight)font_path是中文词云雪崩的开始Windows系统用C:/Windows/Fonts/msyh.ttc微软雅黑或simhei.ttf黑体Linux服务器上需要自己上传一个中文字体文件并写绝对路径。generate_from_frequencies直接吃词频字典比先拼字符串再generate更可控不会因为字符串里的标点产生额外分词。random_state42固定随机种子保证每次生成的词云布局一致方便复现和对比实验。max_words100限制词数量防止低频噪音词进入画面。5. 避坑与常见问题跑通评论情感分析最容易翻车的5个位置5.1 Windows下读文件报UnicodeDecodeError代码还没跑就翻车现象运行第4章的load_stopwords或pd.read_csv时报UnicodeDecodeError: gbk codec cant decode byte代码逻辑看起来完全没问题。原因Windows默认编码是GBKPython的open()和pd.read_csv()不指定编码时大概率以GBK尝试读取而文件是utf-8写的。这属于平台编码差异跟代码逻辑无关。解决所有读写文件的入口显式指定编码。读文件用encodingutf-8写csv用encodingutf-8-sig。这个规则写在项目一开始就省掉大量血泪排查时间不要在代码里依赖系统默认编码。5.2 SnowNLP输出全部在0.5附近阈值选错比模型选错更致命现象跑完情感分析打印sentiment分布大半条评论的分数挤在0.4到0.6之间正负分界模糊词云里“好看”“难看”同时成为高频词。原因第一是短文本本身信息量少模型置信度天然偏低第二是训练语料和影评领域存在偏差模型在陌生文本上倾向输出模棱两可的分数。解决先跑分布再定阈值不要迷信0.5。打印describe()看分位数如果中位数已经接近0.6把正面阈值上调到0.65甚至0.7。另一种解决方向是扩充领域词典把“下饭”“二刷”“意难平”“烂尾”这类影评高频词通过load_userdict注入让模型在分词阶段就拿到更准确的边界。换个新模型属于最后手段阈值校准能解决大部分问题。5.3 词云中文全变方块wordcloud绕不开的字体参数现象词云生成成功但中文全部显示为一个个小方框英文正常图看起来像二维码。原因wordcloud默认渲染字体是DroidSansMono不包含中文字形。很多教程代码里没写font_path网上一抄就踩进去。解决在WordCloud()构造函数里必须指定font_path指向一个中文字体文件。Windows用C:/Windows/Fonts/msyh.ttcmacOS用/System/Library/Fonts/PingFang.ttcLinux把字体文件放到项目目录下写相对路径font_path./fonts/NotoSansCJKsc-Regular.otf。这个参数不写词云图就是废图。5.4 请求频繁返回403/418豆瓣反爬不是玄学是频率问题现象爬虫脚本前两页正常第三页开始连续403或者返回418。加再多UA伪装也没用。原因403表示请求被拒绝418是服务端明确识别到自动化请求后的返回码。核心触发因子是请求频率UA和Cookie只是必要条件。豆瓣对短时间内的分页快速请求有成熟的频率检测固定sleep(0.5)都不够稳。解决把每页之间的延时提高到1到2秒随机值用random.uniform代替固定sleep单次任务采集页数控制在5页以内触发403后放弃重试等几分钟再跑不要用退避循环硬刚。数据采集的边界意识比技术本身重要这个项目用于学习文本分析低频采集完全够用。5.5 关键词全是“觉得”“真的”停用词表没跟上现象关键词提取结果前10名是“觉得”“真的”“一个”“没有”“但是”真正的剧名和情绪词排到20名开外。原因通用停用词表覆盖了“的”“了”这类文言虚词但“觉得”“真的”“感觉”“有点”这类高频口语词没进表它们在影评里出现频率极高TF-IDF直接把它们顶上来了。解决把停用词表替换成更全的中文停用词表并针对本领域追加口语词。追加的内容可以直接写入stopwords.txt每行一个词。检查方法很简单打印前20个关键词凡是不能用一句话说明“它为什么重要”的词都应该进停用词表。6. 验证与进阶用30条人工标注给SnowNLP“校温度”6.1 漂移率自检先抽样30条再决定阈值动不动情感分析最怕的不是分数不对而是你根本不知道它不对。我的习惯是每换一部电影、每跑一批新评论都先做一个30条的小样本自检随机抽30条评论人工判断正负倾向再和SnowNLP结果对比算不一致比例。这一步就是给模型“校温度”看看它的输出和人类直觉偏了多少。import random sample_df df.sample(30, random_state1) for idx, row in sample_df.iterrows(): print(f[{row[sentiment]:.2f}] {row[cleaned][:50]}) # 对照每一条人工判断结果统计主观标签与模型标签的不一致数如果30条里有超过10条与人工判断明显相反说明阈值定错了按第4章的方法重新看分布调阈值。如果阈值调整后依然大面积不一致才考虑换模型。这个验证技巧成本极低却能拦住大部分自欺欺人的分析结论。6.2 词云升级成掩膜形状让输出更有“设计感”基础词云跑通后可以加一个参数让词云呈现电影海报或人物剪影形状。用PIL读取一张黑白掩膜图背景是黑色目标是白色传给WordCloud的mask参数from PIL import Image import numpy as np mask np.array(Image.open(mask.png)) wc WordCloud( font_pathC:/Windows/Fonts/msyh.ttc, maskmask, background_colorwhite, max_words100, random_state42, ).generate_from_frequencies(word_freq)掩膜图注意三点形状区域必须是白色背景是黑色词云尺寸会自动跟随掩膜尺寸但建议原图控制在1000像素以内边缘太锐会造成渲染异常background_color要设为白色或透明背景否则掩膜外的区域会被填充。这套方法论里爬虫、清洗、分词、情感分析、关键词提取、词云可视化的每一个环节都能单独替换成更专业的方案。我每次换新电影跑数据都按第6.1节的30条自检先校准一遍再决定后面的图怎么做。做完这些你会对“网友到底在吵什么”比评分数字有更直观的判断。希望帮到你。本文还有配套的精品资源点击获取