简介:面向Python数据挖掘初学者的实战资源包,聚焦豆瓣《肖申克的救赎》评论数据,利用SnowNLP库开展情感分析与词云可视化,帮助读者理解从文本清洗、分词到情感极性判断、高频词展示的完整流程。压缩包为rar格式,体积仅37KB,共10个文件,包括8个Python脚本、1个CSV评论数据集和1个TXT文本文件;脚本按功能递进组织,涵盖数据读取、结巴分词、SnowNLP情感打分及结果统计等典型环节,适合逐一对齐学习。已有16946人浏览学习,资源配套《Python数据挖掘课程》系列文章,作者为Eastmount。通过运行这些脚本,读者可快速复现豆瓣评论情感分析结果,掌握调用SnowNLP与词云库的基本方法,也能为后续文本挖掘和情感分析任务提供可直接改造的代码模板。
1. 当我在做评论分析时,为什么先选 SnowNLP 而不是上大模型
拿到豆瓣短评这类文本做情感分析,很多人的下意识反应是套个大模型 API,但实际跑完一圈你会发现:这批数据量不大、句子短、口语化严重,大模型的成本和调度成本都花在了没必要的地方。SnowNLP 是纯 Python 实现的中文情感分析库,模型文件只有几 MB,本地 pip 安装后离线就能跑,对“按条打分、按批次统计”这种最常见的评论分析场景来说,几乎是零部署成本。它内置了电商和影评数据训练过的先验模型,拿到豆瓣短评可以直接出 0 到 1 的情感分数,不用标注数据、不用微调,配合 jieba 分词和 wordcloud 就能把“哪句话是好评、差评集中在吐槽什么”落成看得见的词云图。这套流程适合三类人:刚入门 NLP 的学生、要做数据可视化的分析师、以及只想快速验证“这批评论到底什么风向”的开发者。
2. 环境与数据形态:先把评论数据收拾成 SnowNLP 认识的格式
2.1 SnowNLP 选型:为什么它适合豆瓣短评这种短文本
SnowNLP 的情感分析底层是基于贝叶斯分类器训练的模型,输出的sentiments是一个 0 到 1 的浮点数,越接近 1 表示越正面,越接近 0 表示越负面。它跟基于深度学习的大模型思路完全不同:不依赖 GPU、不需要构建复杂的数据管道,一个字符串丢进去就能出分。这对豆瓣短评这种“单条不超过 100 字、句子结构不完整、网络用语多”的文本来说,实用性反而比大模型更强。
豆瓣短评的特点是口语化、片段化严重,比如“绝了”“导演太会拍了”“全程无尿点”这类表达,传统规则词典很难覆盖全,而 SnowNLP 的先验模型在影评语料上做过训练,对这类短句的敏感度比通用情感词典高。我一般会把 SnowNLP 当成一个“够用的黑匣子”来用:不深究内部概率图模型的具体参数,重点管好输入文本的清洗和输出分数的阈值划分。
环境搭建没什么坑可踩,一条命令的事:
pip install snownlp jieba wordcloud matplotlib pandas这里snownlp是核心情感分析库,jieba用于中文分词,wordcloud负责生成词云图,matplotlib用来绘图和展示图片,pandas用来读评论数据文件。注意wordcloud在部分 Python 版本上需要matplotlib作为底层渲染依赖,所以两个最好一起装。
2.2 数据准备:从原始短评到标准化的 CSV 输入
跑情感分析之前,数据形态决定后面代码的复杂度。我碰过太多次“代码写到一半发现数据里有空行、有重复评论、有 JSON 串”的情况,所以建议第一步先统一成 CSV 格式。这里以一份模拟的豆瓣短评数据为例,文件名叫reviews.csv,结构就两列:comment存评论文本,label存人工标注的好恶(可选,后面用于验证)。模拟数据内容大概是“剧情紧凑,演员演技在线”“节奏太慢了,看得想睡觉”这类风格。
import pandas as pd df = pd.read_csv("reviews.csv", encoding="utf-8-sig") df["comment"] = df["comment"].astype(str).str.strip() df = df[df["comment"].str.len() > 0] df = df.drop_duplicates(subset="comment", keep="first") print(df.shape)这段代码做了三层清洗:首先用utf-8-sig编码读取,避免 Windows 下 Excel 保存的 CSV 带 BOM 头导致第一列名变成comment前面多一个空字符;然后把所有列强转成字符串并去掉首尾空格,防止个别单元格是 NaN 或数字类型导致后续SnowNLP报类型错误;最后删掉空行和完全重复的评论,保留第一条。df.shape打印出来的是清洗后的数据量,这一步的数据量直接决定后面情感分布统计的可靠程度。
清洗的原则是:不要做过度预处理,不要提前把停用词删掉。SnowNLP 的模型对完整句子的判断更准确,删词反而会破坏句子的情感线索。
3. 情感分析核心实现:打分、阈值与结果落盘
3.1 用 SnowNLP 批量打分:循环里最容易踩的性能坑
核心打分逻辑很简单,但很多人上来就在 DataFrame 里逐行apply,数据量一上万就卡得不行。先看标准写法:
from snownlp import SnowNLP def get_sentiment_score(text): try: s = SnowNLP(text) return round(float(s.sentiments), 4) except Exception: return None df["score"] = df["comment"].apply(get_sentiment_score) df = df.dropna(subset=["score"]) print(df[["comment", "score"]].head())apply在万级数据量下是够用的,真正影响性能的是SnowNLP(text)每次初始化都要加载模型文件。如果你的评论有几万条,建议把模型初始化挪到循环外面,或者直接改用SnowNLP底层封装好的批量接口。常见做法是保持apply写法但确认内存充足,因为每次调用都会触发一次模型预测,内存占用会随 DataFrame 行数线性增长,跑完记得del大对象。
round(float(s.sentiments), 4)把分数保留四位小数,方便后面画直方图时分组。捕获异常返回None是必须的,因为某些特殊字符(比如 emoji 或者残缺的 HTML 标签)会让内部的解析器抛异常,丢掉这几条比中断整个任务划算。
3.2 阈值策略:0.5 分界线没那么可靠,分段统计更实用
情感分 0 到 1,很多人直接拿 0.5 一刀切,这在豆瓣短评上会翻车。短文本、反讽、疑问句都会让分数聚在 0.4 到 0.6 之间,生硬切割会把大量中性评论污染成正面或负面。我一般会用分段策略:
def sentiment_tag(score): if score >= 0.7: return "正面" elif score >= 0.4: return "中性" else: return "负面" df["sentiment"] = df["score"].apply(sentiment_tag) group_counts = df["sentiment"].value_counts() print(group_counts)正面、负面、中性三档的划分避免了二分类的尴尬。阈值 0.7 和 0.4 是我在影评数据上试出来相对稳定的经验值,如果你跑自己的数据集,先用df["score"].describe()看分布,再微调这两个数。判断标准是:抽查 20 条人工看,误判率超过 15% 就把阈值往 0.5 收窄,反之放宽。这份模拟数据跑出来的分布一般是正面占四成、负面三成、中性三成左右,和豆瓣短评的真实生态比较接近。
结果落盘这一步容易被忽略,但一定要做,否则后面词云分析要重新算一遍:
df.to_csv("reviews_with_sentiment.csv", index=False, encoding="utf-8-sig")用utf-8-sig而不是utf-8,保命操作,后面词云读取和 Excel 打开都不会乱码。
4. 词云分析:分词、停用词和中文字体三座山
4.1 jieba 分词与自定义停用词表:词云好看全靠这一步
词云的基础是词频统计,中文必须分词。jieba 的默认分词对“演员演技在线”能切得不错,但会把“电影”“真的”“一个”这类高频无意义词也带进来,不处理的话词云图里全是噪音词。我维护了一份自己的停用词表,按行存在stopwords.txt里,格式很简单,每行一个词。
import jieba from collections import Counter stopwords = set() with open("stopwords.txt", "r", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) words = [] for comment in df["comment"].tolist(): seg_list = jieba.lcut(comment) for word in seg_list: word = word.strip() if len(word) < 2: continue if word in stopwords: continue if not word.isalpha() and not any('\u4e00' <= ch <= '\u9fff' for ch in word): continue words.append(word) word_freq = Counter(words) print(word_freq.most_common(20))jieba.lcut返回切好的词列表,比cut更省心。过滤规则有三条:单字词不要(语气词和量词为主),停用词表里的不要,非中文且非英文的符号串不要。最后用Counter统计词频,most_common(20)打印高频词,用于快速检查停用词表有没有漏——如果高频词里还有“这个”“什么”之类,说明停用词表该补词了。
停用词表不需要从零开始写,网上有现成的中文停用词表,下下来再往里面加豆瓣短评的特有词就行。我一般开局直接加:电影、影片、真的、觉得、感觉、一部、还是、一个、一部、这次。
4.2 wordcloud 生成词云:字体路径不设置就是一片方块
词云的代码本身简单,坑全在字体和图像参数上。先看代码:
from wordcloud import WordCloud import matplotlib.pyplot as plt wc = WordCloud( font_path="C:/Windows/Fonts/simhei.ttf", width=1200, height=800, background_color="white", max_words=100, colormap="plasma" ) wc.generate_from_frequencies(word_freq) plt.figure(figsize=(12, 8)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.savefig("sentiment_wordcloud.png", dpi=200, bbox_inches="tight")font_path必须指向系统中文字体文件,Windows 是simhei.ttf(黑体),macOS 通常是/System/Library/Fonts/PingFang.ttc,Linux 一般是/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc。不加字体,词云图里所有中文都会渲染成方块。generate_from_frequencies接收前面Counter的结果,比传原始文本更可控。max_words=100限制词云中显示的词数量,词太多就会密密麻麻;colormap="plasma"控制配色,这个纯看审美,你喜欢什么色系就换什么。dpi=200保证输出图片清晰,公众号插图或 PPT 直接能用。
如果词云只显示几个超大的词,说明max_words值太小或者停用词过滤不过关,高频词被几个词垄断了。处理办法是放宽max_words到 150,同时继续补停用词。
5. 避坑指南:SnowNLP 与词云实战中的五个翻车现场
5.1 情感分数全部接近 0.5:模型对超短文本失效
现象:批量打分后df["score"].describe()显示方差极小,所有分数都挤在 0.48 到 0.52 之间。
原因:SnowNLP 的贝叶斯模型在训练时见过的句子长度偏向完整句,遇到豆瓣短评这种三五字的超短文本时,特征稀疏,概率趋近先验均值 0.5。这是模型本身对短文本的钝感导致的,不是代码问题。
解决:先人工判断句子长度,字数小于 8 的评论单独拎出来,用规则补充。常见做法是维护一份情感种子词表,命中“绝了”“烂片”“难看”“推荐”这些强情感词直接打标,不经过 SnowNLP;长度足够的文本再走模型打分。这样混合策略能明显改善短句被压到 0.5 的问题。
5.2 词云中文全部变成方块
现象:词云图渲染出来,所有汉字显示为空心矩形。
原因:WordCloud默认使用英文渲染引擎,不指定中文字体路径,绘图时找不到能覆盖 CJK 字符的字体。
解决:font_path参数指向系统中文字体。Windows 用户最容易踩的坑是路径写反斜杠报转义错误,代码里用正向斜杠/或双反斜杠\\;macOS 用户别用/System/Library/Fonts/PingFang.ttc发现不存在,先fc-list :lang=zh查实际字体名。建议把字体文件复制到项目目录下,用相对路径引用,这样换机器跑不踩雷。
5.3 词云里全是“真的”“感觉”“一个”
现象:most_common(20)打印出来的高频词几乎全是无实义的虚词和口语填充词。
原因:停用词表不够用。豆瓣短评的口语化词和通用停用词表的重合度不高,通用表里不会有“真的”“感觉”“一部”“还是”这种在影评语境里无区分度的词。
解决:跑完第一次词频统计后,把most_common(50)的结果人工扫一遍,凡是跟电影评价无关的词全部追加进stopwords.txt。这个步骤至少迭代两轮,第一轮去掉通用虚词,第二轮去掉领域虚词。我的经验是,最终停用词表稳定下来大约 300 到 500 词,之后词云的质量会有质的飞跃。
5.4 CSV 读取后列名带\ufeff前缀
现象:代码里写df["comment"]报 KeyError,打印df.columns看到第一列名是\ufeffcomment。
原因:CSV 文件是 UTF-8 with BOM 格式,pandas默认按 UTF-8 无 BOM 读,BOM 头被当成列名的一部分。
解决:pd.read_csv(..., encoding="utf-8-sig")一步到位。这个坑在 Windows 上用 Excel 保存 CSV 时 100% 触发,我吃过两次亏以后,现在所有读取 CSV 的操作一律utf-8-sig,宁可多写几个字符也不给排查留麻烦。
5.5 SnowNLP 把“???”识别成强烈负面
现象:一条评论“这片子真的绝了??”被打了 0.2 分,人工理解是夸赞。
原因:问号和感叹号在模型训练语料中大量出现在低分评论里,模型把这个标点符号特征学到了。这是先验模型没有上下文理解能力的典型表现。
解决:打分前做标点归一化,连续的??!!!压缩成单个,避免多个标点叠加放大情感权重。同时这种句子在短文本场景下大概率命中 5.1 的处理路径,由种子词表接管,不让模型背锅。
6. 进阶:分组词云对比与分布直方图,把情感分析做成决策依据
基础版词云是全部评论混在一起做,进阶用法是把正面和负面评论分开生成词云,对比着看。这一步的价值在于:正面词云的高频词告诉你这部片子的卖点在哪,负面词云的高频词告诉你观众的不满集中在哪,对比结果可以直接支撑内容运营决策。
实现上只需要在生成词云前加一个筛选:
positive_df = df[df["sentiment"] == "正面"] negative_df = df[df["sentiment"] == "负面"]然后对两个子集分别走一遍分词和WordCloud,把输出的图片拼在同一张画布上。我一般会在代码里顺手把情感分布直方图也画出来:
plt.hist(df["score"], bins=20, range=(0, 1), color="#4C72B0") plt.xlabel("sentiment score") plt.ylabel("comment count") plt.title("Sentiment Score Distribution") plt.savefig("score_distribution.png", dpi=200)直方图比阈值切割更直观,能一眼看出这批评论的情感是双峰分布还是单峰偏向一侧。如果分布图显示峰值在 0.45 附近且两边各有个小峰,说明评论区存在明显的阵营分化,这是正常现象;如果峰值偏 0.3,说明整体口碑偏冷,这时候再去看负面词云里到底是不是集中在“剧情”“节奏”“剪辑”这些具体点上,就很有信息量了。
两条分组词云并排生成后,保存成图片之前还有一个细节:两张图的max_words保持相同,否则对比时词的大小基准不一致,视觉上会误判词频差异。代码里把WordCloud的参数抽成一个函数避免复制粘贴,这里就不展开了。
另外提一下模型微调。如果你手上有一批人工标注好的豆瓣评论,可以用SnowNLP自带的训练接口做增量训练:
from snownlp import sentiment sentiment.train("positive.txt", "negative.txt") sentiment.save("sentiment.marshal")训练数据文件格式是每行一条评论,正负分开存放。微调的意义在于让模型更贴合你当前领域的数据分布,但需要注意训练数据量建议不低于 1000 条每类,样本太少反而会让模型在原有基础上产生偏移。我通常先把规则和阈值调到位,最后才考虑微调这一层。
从那以后,我每次拿到新的评论数据都会先画一张分布直方图,再决定阈值落在哪,而不是拍脑袋取 0.5。这套从清洗、打标、可视化到对比的流程跑熟之后,情绪判断就不再是两眼一抹黑地读评论了。希望帮到你。
本文还有配套的精品资源,点击获取