恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
豆瓣短评情感分析与词云可视化:Python NLP完整实践
首页
资讯中心
/
豆瓣短评情感分析与词云可视化:Python NLP完整实践
豆瓣短评情感分析与词云可视化:Python NLP完整实践
发布时间:2026/9/13 10:17:00
简介基于Python的豆瓣电影评论情感分析与关键词提取词云设计源码面向对自然语言处理、文本挖掘和数据可视化感兴趣的开发者与学生。项目以豆瓣热门电影评论为样本覆盖数据清洗、去停用词、情感分类模型训练、TF-IDF/TextRank关键词抽取以及WordCloud词云生成等完整流程适合用于课程设计或入门项目复现。压缩包共39个文件总大小约44.31MB主要包含主程序main.py、停用词表与过滤文本、6份xlsx分析报告如情感分布、趋势图数据、19张PNG可视化图片、中文字体otf以及xml配置与gitignore等辅助文件目录结构清晰便于按模块查阅。已有182人学习下载。通过该项目可直观了解评论情感倾向与热点话题并获得可直接运行的完整源码、可视化图表及分析报告便于在此基础上扩展更多电影或评论场景。1. 一行代码出词云但“电影评论情感分析”远不止画图这么简单豆瓣短评是中文 NLP 里最典型的“短文本强情感”样本单条评论通常不超过 140 字口语化严重夹杂网络新词、反讽和表情符号却天然带有评分标签1 星到 5 星供你做监督信号。很多人第一次接触情感分析就是从豆瓣短评开始的因为数据唾手可得结果肉眼可见。但真正把这个标题里的每块拼图串起来——Python 爬虫取数、情感得分计算、关键词提取、wordcloud 可视化——你会发现难点根本不在“会不会调用库”而在于短评里的否定句式怎么处理、领域停用词怎么积累、词云的字体和掩膜为什么总是报错。这篇文章我按自己的实现路径来讲先用 requests 拿到豆瓣短评并做清洗再用 SnowNLP 做情感概率输出接着用 jieba 的 TF-IDF 和 TextRank 分别提取关键词最后把得分最高的词和情感倾向一起投射到 wordcloud 上。所有代码都以可复现的最小片段给出涉及参数的地方我会专门解释调整依据。适合已经跑通 Python 基础环境、想完整走一遍 NLP 小项目的读者如果你连 Python 还没装先解决环境问题但每一步我都会标注依赖库的安装方式不绕路。2. 数据准备豆瓣短评的请求构造与文本清洗策略2.1 请求头与分页参数的最小可用方案豆瓣短评页面的 URL 结构相对稳定以某部电影的 short_comments 页面为例核心请求参数是start偏移量和limit每页数量。爬虫逻辑本身不复杂复杂的是怎么让你的请求看起来像真实用户。我一般会构造一组完整的请求头重点是User-Agent和Referer其中Referer必须指向电影详情页否则服务端可能直接返回 418。import requests import time from random import uniform def fetch_comments(movie_id, page_start0, limit20): url https://movie.douban.com/subject/{}/comments.format(movie_id) params { start: page_start, limit: limit, status: P, sort: new_score } headers { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36, Referer: https://movie.douban.com/subject/{}/.format(movie_id), Accept-Language: zh-CN,zh;q0.9, } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.text这里sortnew_score表示按热门评论排序相比按时间排序它的评论文本更长、情感表达更完整对后续情感分析和关键词提取都更友好。resp.raise_for_status()会在状态码非 200 时主动抛异常便于你在批量抓取时快速定位被封或参数错误。拿到 HTML 后推荐用lxml配合 XPath 提取评论内容而不是正则。豆瓣短评的 HTML 结构相对固定每条评论在div.comment-item下评论文本在span.short节点中。XPath 写起来直观解析速度也比 BeautifulSoup 快不少。from lxml import etree def parse_comments(html): tree etree.HTML(html) items tree.xpath(//div[contains(class, comment-item)]) comments [] for item in items: text_node item.xpath(.//span[contains(class, short)]/text()) if text_node: comments.append(text_node[0].strip()) return comments2.1.1 反爬阈值设置与失败重试豆瓣的限流策略不是通过 IP 封禁硬触发而是对单个 IP 的请求频率做统计。实际操作中我会把两次请求的间隔设定在 3 到 7 秒随机浮动单次会话最多抓 10 页200 条评论。每请求一页后检查返回 HTML 中是否包含「有异常请求」或验证码关键词出现则立即停止并休息 60 秒。如果你需要抓多部电影建议把每部电影的评论写入独立 CSV 文件文件名包含电影 ID避免单线程中断后全部重跑。批量场景下可以考虑用fake_useragent库随机生成 UA但注意豆瓣对个别 UA 段有历史封禁保守起见用真实浏览器复制来的 UA 反而更稳。2.2 短评清洗的三道工序抓下来的原始文本不能直接送进情感分析和词云。豆瓣短评的噪音集中在三个地方空格与换行符、重复评论同一用户刷屏或复制热评、以及繁体字。其中重复评论对词云的伤害最大因为 TF-IDF 会把重复词的 idf 拉低但词频统计不看 idf重复词会以巨大字号霸占词云中心。import re from collections import Counter def clean_comments(comments): cleaned [] seen set() for text in comments: text re.sub(r\s, , text) text re.sub(r[【】\[\]()], , text) if text and text not in seen: seen.add(text) cleaned.append(text) return cleaned def detect_duplicates(comments): normalized [re.sub(r[^\u4e00-\u9fa5], , c) for c in comments] counter Counter(normalized) return [c for c, n in counter.items() if n 1]detect_duplicates的用途是找出完全重复的评论但实际中很多“伪重复”只是相似比如“太好看了”和“太好看了”仅在标点上有区别这时需要去掉标点后再比较。至于繁体字推荐用opencc-python-reimplemented比 OpenCC 原库安装体积小转换效果一致。pip install opencc-python-reimplementedfrom opencc import OpenCC cc OpenCC(t2s) cleaned [cc.convert(text) for text in cleaned]清洗完成后你会得到一条可用的短评列表。此时先做一次描述性统计计算评论长度分布、出现频率最高的 30 个词不带停用词的原始切分这能帮你判断数据质量是否足够支撑后续分析。如果高频词里全是「电影」「一部」「真的」这类词说明停用词表必须扩充。3. 情感分析实现SnowNLP 在短评语料上的阈值校准3.1 为什么选择 SnowNLP 而不是复杂模型中文情感分析的库不少SnowNLP、cntext、transformers微调 BERT各有适用边界。对于豆瓣短评这种长度短、依赖上下文少、且你需要快速跑通全流程的场景SnowNLP 是最务实的选择安装简单、无需下载预训练模型、单条短评的情感计算耗时在毫秒级。BERT 类模型精度虽高但你要先解决训练语料标注问题这对一个词云项目来说成本过高。SnowNLP 的情感分析本质是一个朴素贝叶斯分类器输出的是0到1之间的情感概率越接近 1 表示正向倾向越强。它自带的训练语料来自电商购物评论所以直接套用到电影评论上会出现一个典型偏差对“剧情平淡但演技在线”这类评论模型更容易预测为负向因为“平淡”在电商语料里是差评信号。from snownlp import SnowNLP def sentiment_score(text): s SnowNLP(text) return s.sentiments pos_comments [c for c in cleaned if sentiment_score(c) 0.6] neg_comments [c for c in cleaned if sentiment_score(c) 0.4]这里把阈值设为0.6和0.4中间0.4到0.6的区间视为中性。这个阈值不是拍脑袋定的我测试过豆瓣短评数据0.5 作为分界线会把大量五星和三星评论混在一起因为三星评论的情感表达往往比较收敛。调整阈值后正负样本的比例会更接近电影评分分布。3.1.1 对短评里的否定句式做二次校验SnowNLP 在“不太好看”这类句式上经常翻车它的贝叶斯模型会把“不”和“太”分开计算导致整体偏正向。我在实践中会先做一轮简单的否定词检测如果句中包含「不」「没」「难」「差」且紧跟程度副词如“太”“很”“特别”则将 SnowNLP 输出结果做反转处理。negative_patterns [不太, 不好, 没意思, 难看, 失望, 尴尬] def adjusted_sentiment(text): score sentiment_score(text) for pattern in negative_patterns: if pattern in text: return 1 - score return score注意反转不是说把 0.7 变成 0.3 就一定正确比如“没有想象中难看”这个句子本身是否定之否定你不能单纯反转。这里提供一个折中反转后的分数如果落在0.35~0.65区间统一判为中性。这个规则牺牲了部分准确率但换来了情感极性判定的稳定性对后续词云分组有实际帮助。3.2 用豆瓣评分做情感标签的弱监督校验如果你懒得人工标注测试集有一个讨巧的验证思路把每条短评对应的星标也抓下来在 HTML 的span.allstar50等 class 中将五星和一星评论各自取出 100 条分别计算 SnowNLP 得分的均值。理想状态下五星评论的均值应该在 0.8 以上一星评论在 0.2 以下。如果两者差距不足 0.4说明模型对当前语料的区分度不够需要重新清洗数据或调整预处理逻辑。我这里给出一个简单的分组统计代码输出对比结果后你可以决定是否需要用评分数据做有监督微调。就本项目而言弱监督校验已经足够支撑词云的可信度。import pandas as pd df pd.DataFrame({comment: cleaned}) df[score] df[comment].apply(adjusted_sentiment) five_star df[df[comment].str.contains(allstar50, regexFalse)] one_star df[df[comment].str.contains(allstar10, regexFalse)] print(五星评论平均情感分:, five_star[score].mean()) print(一星评论平均情感分:, one_star[score].mean())阈值区间情感极性词云分组0 ~ 0.3强负面负面词云0.3 ~ 0.4弱负面负面词云0.4 ~ 0.6中性不入词云0.6 ~ 0.7弱正向正面词云0.7 ~ 1.0强正向正面词云这个分组方案和阈值设定直接对应词云的输入数据。你会发现中性评论被剔除后词云的文本总量会减少 10% 到 20%但剩余的词在情感表达上更纯粹视觉上的“噪音词”大幅减少。4. 关键词提取jieba 的 TF-IDF 与 TextRank 双路对比4.1 停用词表和词性过滤是关键词质量的分水岭jieba 的analyse.extract_tags默认使用 TF-IDF 算法但默认停用词表非常简陋连「我们」「你们」「这个」都挡不住。如果你直接对短评列表做关键词提取前 10 个词里至少有一半是代词和语气词。我的做法是把三份停用词合并jieba 自带表、哈工大停用词表、以及针对电影评论手工添加的领域词如「电影」「一部」「真的」「感觉」「觉得」。import jieba.analyse def load_stopwords(custom_wordsNone): stopwords set() for line in open(stopwords.txt, encodingutf-8): stopwords.add(line.strip()) if custom_words: stopwords.update(custom_words) return stopwords stopwords load_stopwords(custom_words[电影, 一部, 真的, 感觉, 觉得, 还是]) def extract_keywords_tfidf(texts, top_k20): text .join(texts) jieba.analyse.set_stop_words(stopwords.txt) tags jieba.analyse.extract_tags(text, topKtop_k, withWeightTrue) return tagsset_stop_words接收的是文件路径文件中每行一个停用词。这里有个容易踩的坑如果你在加载停用词后又调用了load_userdict添加自定义词典停用词的优先级依然高于用户词典也就是说即使你词频再高只要命中停用词就不会被提取。这是 jieba 的硬逻辑不按添加顺序覆盖。4.1.1 词性过滤参数的实际效果extract_tags不直接支持词性过滤但你可以配合jieba.posseg先做词性标注再保留n、v、a名词、动词、形容词三种词性。对于豆瓣短评形容词其实是最有价值的情感载体比如「惊艳」「失望」「无聊」直接决定情感极性。名词更多反映讨论主题比如「剧情」「演技」「特效」。import jieba.posseg as pseg def extract_keywords_by_pos(texts, top_k20): text .join(texts) words_with_flag pseg.cut(text) filtered [w.word for w in words_with_flag if w.flag in (n, v, a)] return jieba.analyse.extract_tags( .join(filtered), topKtop_k, withWeightTrue)这里有一个隐含问题经过posseg过滤后短句的语序被打乱但 TF-IDF 算法只依赖词频和逆文档频率不受语序影响所以结果仍然有效。如果你用 TextRank情况就不同了因为 TextRank 依赖词与词之间的共现关系打乱语序会破坏图结构。所以这两种算法在文本预处理阶段的要求不一样不能复用同一套过滤后的文本。4.2 TextRank 的参数调节与适用场景TextRank 在 jieba 中的调用方式是jieba.analyse.textrank它不需要初始语料库做 IDF 统计完全基于当前文本内部的词共现图。这对豆瓣短评有一个天然优势短评之间没有强上下文关联每一条都是独立文本TF-IDF 的 IDF 计算会被大量低频词干扰而 TextRank 只在当前文档集合内做 PageRank 迭代对低频词的惩罚更温和。tags_tr jieba.analyse.textrank(text, topK20, withWeightTrue, allowPOS(n, v, a), withFlagFalse)allowPOS直接指定参与 PageRank 迭代的词性推荐只放n、v、a。这里不建议加vn动名词因为它会把「表演」「导演」这类词重复计入不同词性导致权重被高估。对比两组结果时你会发现 TextRank 提取出的关键词更偏主题性如「导演」「镜头」TF-IDF 更偏评论特色如「好看」「烂」。如果做词云我会选择 TF-IDF 结果因为视觉冲击力更强如果做主题分析TextRank 更合适。对比维度TF-IDFTextRank原理词频 x 逆文档频率词共现图的 PageRank对短文本集IDF 易受噪声干扰共现关系更稳定关键词风格情感词高权重主题词高权重主要参数topK,withWeighttopK,withWeight,allowPOS,spanTextRank 还有一个span参数默认值为 5表示共现窗口大小。窗口越小词之间的关联越局部窗口越大越容易把语义无关的词连接起来。对豆瓣短评这种平均长度 50 字左右的文本建议把span设为 3避免把同一句话里相隔过远的词错误关联。5. wordcloud 设计中文字体、掩膜图片与情感色调映射5.1 字体路径是中文词云的第一道坎wordcloud库本身不处理字体渲染它是通过 Pillow 把词绘制到图片上。如果直接运行WordCloud()生成中文词云的图片你会得到一堆方块乱码。解决方案是显式指定一个支持中文的字体文件路径在 macOS 上用/System/Library/Fonts/PingFang.ttcWindows 上用C:/Windows/Fonts/msyh.ttc。from wordcloud import WordCloud, ImageColorGenerator import matplotlib.pyplot as plt import numpy as np from PIL import Image mask np.array(Image.open(douban_mask.png)) def generate_wordcloud(keywords_with_weight, mask_image, font_path, output_path): wc WordCloud( font_pathfont_path, maskmask_image, background_colorwhite, max_words200, max_font_size120, width1200, height800, random_state42, colormapRdBu ) word_freq dict(keywords_with_weight) wc.generate_from_frequencies(word_freq) wc.to_file(output_path)generate_from_frequencies接收的是{词: 权重}字典权重直接来自extract_tags返回的withWeightTrue不需要再取整。max_font_size控制最大字号但实际最终字号还受到relative_scaling参数影响默认auto模式下权重差越大字号梯度越明显。豆瓣短评的 TF-IDF 权重通常头部词是尾部词的几十倍所以如果你觉得词云大字太小、小字看不清可以手动把relative_scaling设为0.5。5.1.1 掩膜图片的透明通道与轮廓问题设计好的掩膜图不是随便找张 jpg 就行mask参数要求图片的轮廓区域是白色其他区域是黑色。常见的坑是透明背景 PNGnp.array读入后是四通道wordcloud 只会取前三个通道透明区域会被当作黑色处理导致词云只在透明区域外生成正好和预期相反。mask np.array(Image.open(mask.png).convert(RGB)) white_pixels np.all(mask 255, axis-1) mask[white_pixels] 0这段代码把纯白像素全部设为 0黑色背景白色区域成为词云的绘制区域黑色区域被遮挡。如果你要做一个圆形的词云更好的做法是直接用mask参数传入PIL生成的原图但确保图片本身是大尺寸至少 1000x1000否则词云边缘会锯齿严重。5.2 按情感分组生成双词云并用颜色映射情绪极性这是整条流水线的点睛之笔。前面情感分析已经把所有评论分成了正、负两组关键词提取也应该分别做。把正负两组的关键词分别生成词云你会得到两张直观对立的图正向词云中心是「经典」「感动」「演技」负向词云里是「尴尬」「拖沓」「失望」。def split_by_sentiment(comments): pos_text .join([c for c in comments if adjusted_sentiment(c) 0.6]) neg_text .join([c for c in comments if adjusted_sentiment(c) 0.4]) return pos_text, neg_text pos_text, neg_text split_by_sentiment(cleaned) pos_tags extract_keywords_tfidf([pos_text], top_k50) neg_tags extract_keywords_tfidf([neg_text], top_k50) generate_wordcloud(pos_tags, mask, font_path, pos_wordcloud.png) generate_wordcloud(neg_tags, mask, font_path, neg_wordcloud.png)颜色映射上负面词云选择colormapReds正面词云选择colormapBlues通过视觉上的冷暖对比强化情感对立。注意ImageColorGenerator可以用原图颜色给词云上色但那会让情感分组失去意义你很难看出哪些词代表正向、哪些代表负向。情感组词云颜色预期关键词示例正向 (score0.6)Blues经典、演技、感动、值得负向 (score0.4)Reds尴尬、拖沓、失望、浪费时间6. 把词云结果和评论原文做交叉验证提升分析可信度词云生成后不要急着收工。一个高频词在词云里占据大字号不代表它一定表达了你期望的情感倾向。举例来说「演技」在正向词云里可能代表“演技在线”但如果这部电影的负面评论里也有大量「演技」讨论你需要确认负面语境是「演技尴尬」还是「演技浮夸」。我会写一段交叉验证代码把包含特定关键词的正负评论分别打印出来这样你可以直接从自然语言层面确认词云结论是否符合直觉。def verify_keyword_in_context(keyword, pos_comments, neg_comments, top_n5): pos_hits [c for c in pos_comments if keyword in c][:top_n] neg_hits [c for c in neg_comments if keyword in c][:top_n] print(【正面语境】) for c in pos_hits: print( , c) print(【负面语境】) for c in neg_hits: print( , c) verify_keyword_in_context(演技, pos_comments, neg_comments)这段代码不是分析流程的附属品而是检验情感分析准确性的有效手段。当你在词云中看到「特效」这个关键词时把它在两份评论中的语境调出来很快就能判断这个词是正面的“特效震撼”还是负面的“特效五毛”。这比任何数据指标都直观。最后再提一个容易被忽略的参数wordcloud的repeat参数默认False。如果某部电影评论数量不够多关键词去重后可能不足 100 个词此时词云会显得稀疏。把repeatTrue可以让低频词循环出现填充画面但视觉上会显得不自然。我更推荐的做法是回到extract_tags把topK从 20 提高到 80让更多低频但有语义价值的词进入词云而不是用重复词填充。如果topK80仍然稀疏就说明你的评论样本量不足豆瓣短评至少抓 300 条以上才有稳定的关键词分布。本文还有配套的精品资源点击获取