资讯动态

豆瓣短评情感分析实战:爬虫、情感模型与词云避坑指南

发布时间:2026/10/14 0:58:03 来源:尧图企业网站定制
简介一份基于Python的豆瓣电影评论情感分析与关键词提取WordCloud设计源码面向NLP、文本挖掘及数据可视化方向的开发者与研究人员。项目围绕main.py构建完整处理链路先进行评论清洗、去停用词等预处理再结合TF-IDF/TextRank等方法提取关键词利用机器学习模型判别情感极性最后以词云图、情感分布图和趋势图呈现结果。压缩包共39个文件包括1个py主程序、19个PNG图表词云图、情感分布图、趋势图、6个xlsx分析报告、4个xml文件、4个txt文本含停用词表、过滤文本、2个otf中文字体及IDE辅助配置整体大小约44.31MB按功能模块组织便于逐项研读。目前已有183人学习资源内已集成唐探1900、哪吒之魔童闹海、熊出没·重启未来等六部电影的真实评论分析案例覆盖从数据预处理、特征提取、模型训练到报告输出的完整流程适合用于对照学习情感分析管线、关键词权重计算及中文词云渲染技巧。1. 基于 Python 的豆瓣短评情感分析与 wordcloud 词云别把模型输出当真基于 Python 的豆瓣电影评论情感分析与关键词提取再配一张 wordcloud 词云图是我见过最适合 Python 入门者练手的真实数据项目。它能解决一个具体问题把某部电影几千条短评抓下来判断整体口碑是正向还是负向找出观众反复提到的点最后把高频词画成一张能放到汇报里的词云图。但别急着开跑默认的情感模型拿去做电影短评经常翻车这是这个项目里第一个值得你记住的教训。它适合刚学完基础语法、想用真实数据把 requests、jieba、pandas 串起来的人也适合需要快速出舆情结论的运营同学。2. 把短评抓下来豆瓣评论区的请求头、翻页与清洗落库2.1 先看数据结构短评藏在 HTML 的哪个节点里豆瓣短评页的常见做法是直接抓网页 HTML而不是调用 App 端签名接口。评论主体都在div.comment-item这个节点里每条评论包含正文、星级、时间和点赞数。用浏览器打开任意一部电影的短评页按 F12 看到的 DOM 结构大致是div classcomment-item>import time import random import requests from bs4 import BeautifulSoup headers { 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 ), Referer: https://movie.douban.com/, Cookie: 你的登录 Cookie建议从浏览器复制完整值, } def fetch_comments(movie_id: str, start: int 0, limit: int 20) - list[dict]: url fhttps://movie.douban.com/subject/{movie_id}/comments params { start: start, limit: limit, sort: new_score, status: P, } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) result [] for item in soup.select(div.comment-item): short_el item.select_one(span.short) rating_el item.select_one(.comment-info span.rating) time_el item.select_one(.comment-time) if short_el is None: continue rating 0 if rating_el is not None: for cls in rating_el.get(class, []): if cls.startswith(allstar): rating int(cls.replace(allstar, )) / 10 result.append({ text: short_el.get_text(stripTrue), rating: rating, time: time_el.get(title, ) if time_el is not None else , }) return result这段代码里params的四个参数都有具体含义start控制偏移量第 0 条开始limit每次取 20 条豆瓣对单次分页上限控制得很保守sortnew_score表示按热门短评排序sorttime则按时间排序statusP只看“看过”的用户评论这样能过滤掉没看过就打分的水评。resp.raise_for_status()会在状态码非 200 时直接抛异常方便在批量翻页时快速发现被封。list[dict]类型标注在 Python 3.8 里已经不需要from typing import List直接写就行。2.3 翻页循环、限速与存储为结构化 CSV单页 20 条显然不够翻页循环里真正重要的不是循环本身而是两个保护措施空结果立即停止、每次请求之间随机休眠。把start从 0 递增到几百每步加 20直到某页返回空列表就 break。不要在拿到一批数据后立刻发起下一次请求time.sleep(random.uniform(1, 2.5))是这条链路里性价比最高的反封号手段。movie_id 1292052 # 示例用《肖申克的救赎》换成你要分析的 ID all_comments [] for start in range(0, 260, 20): comments fetch_comments(movie_id, startstart) if not comments: break all_comments.extend(comments) print(f已获取 {len(all_comments)} 条) time.sleep(random.uniform(1, 2.5))抓下来之后最忌讳把数据直接塞进变量然后反复重跑抓取。把清洗后的结果落成结构化 CSV后续情感分析和关键词提取都从文件读抓取脚本只跑一次。字段就三个text正文、rating星级、time完整时间。写入时用utf-8-sig而不是utf-8否则用 Excel 打开 CSV 会看到中文乱码这是 PC 端 Windows 的经典坑。import csv with open(douban_short_comments.csv, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnames[text, rating, time]) writer.writeheader() writer.writerows(all_comments)newline是 Python 写 CSV 时避免空行的标配写法csv.DictWriter按字段名顺序写出字典列表。我的习惯是这套脚本按fetch / sentiment / wordcloud三个模块拆成独立文件抓取模块输出原始 CSV情感分析模块读入后输出带标签的新 CSV最后一个模块只管出图。这样每一步都有中间产物翻车时能清楚定位是数据问题还是模型问题而不是对着一个混沌的长脚本干瞪眼。3. 情感分析SnowNLP 的阈值划分与自定义词典修正3.1 为什么先用 SnowNLP而不是一上来就上大模型或多模态方案情感分析在这个项目里是第一主角选型却最容易纠结。常见做法是先用 SnowNLP 跑一个离线可复现的基线而不是一上来就接大模型 API。原因很实际短评平均不到 30 个字大模型在这种长度上优势不大还要处理 key、配额和成本多模态情感分析虽然能融合弹幕、语音转写和画面内容但那需要额外采集渠道入门阶段碰它等于给自己挖坑。先把文本基线立住用真实结果说服自己再谈升级。SnowNLP 内部是朴素贝叶斯分类器训练语料偏向电商评论这是它最大的黑匣子也是电影短评场景翻车的主要来源。同一个词在电商语境和影视语境里的情感极性完全可能相反模型不知道“演技炸裂”是夸人。理解这一点比背 API 更重要因为后面所有阈值调整和词典修正都在为这个偏差买单。在 Python 3.8 这类常见环境里pip install snownlp之后直接 import 就能用老库的好处就是没什么依赖陷阱。3.2 跑通第一版sentiments 分数、批量调用与 0.4/0.6 阈值SnowNLP(text).sentiments返回 0 到 1 之间的浮点数越接近 1 越正向。但 0.5 并不是天然的切割点我的习惯是把 0.4 以下划为负向、0.6 以上划为正向、中间算中性。这个 0.4/0.6 的阈值比 0.5 单点切割更稳因为短评里大量文本本来就模棱两可硬把 0.52 算成正向会污染后续的词云对比。import pandas as pd from snownlp import SnowNLP df pd.read_csv(douban_short_comments.csv, encodingutf-8-sig) df df.dropna(subset[text]) df df[df[text].str.len() 2] def sentiment_score(text: str) - float: return SnowNLP(text).sentiments df[sentiment] df[text].apply(sentiment_score) df[label] pd.cut( df[sentiment], bins[0, 0.4, 0.6, 1.0], labels[负向, 中性, 正向], rightTrue, ) df.to_csv(comments_with_sentiment.csv, indexFalse, encodingutf-8-sig) print(df[label].value_counts())pd.cut的三个区间边界是核心参数rightTrue表示左开右闭0.4 会被分到中性区间而不是负向。长度过滤str.len() 2是为了把“好”“烂”这类单字评论先筛掉它们信息量太低还容易让后面的关键词提取跑偏。批量调用 SnownLP 是逐条算的1000 条短评大约要跑一两分钟先把结果写回comments_with_sentiment.csv后面反复调阈值就不用重新跑模型。跑完看一眼label的分布如果正向占比超出了直觉不要立刻改阈值先去看具体影评。分布整体偏正对电影来说正常但如果一部口碑明显分裂的片子也被判成一窝蜂正向那就是语料偏差在起作用得进入下一步。3.3 自定义词典与加权修正把「演技炸裂」拉回正向SnowNLP 的自定义训练接口需要准备大批标注语料对入门项目性价比太低。我一般用词表加权做轻量修正准备一个正向词集合和一个负向词集合统计文本里命中次数在模型原始分数上做加减。修正幅度要克制一个词命中加 0.08因为一次命中不该彻底推翻模型输出只有多个同极性词同时出现才应该明显偏移。POSITIVE_WORDS {演技炸裂, 神作, 经典, 震撼, 值得二刷, 惊喜} NEGATIVE_WORDS {烂片, 浪费时间, 尴尬, 拖沓, 难看, 骗钱} def adjusted_sentiment(text: str) - float: base SnowNLP(text).sentiments pos_hit sum(1 for w in POSITIVE_WORDS if w in text) neg_hit sum(1 for w in NEGATIVE_WORDS if w in text) score base pos_hit * 0.08 - neg_hit * 0.08 return round(max(0.0, min(1.0, score)), 4) df[sentiment] df[text].apply(adjusted_sentiment) df[label] pd.cut( df[sentiment], bins[0, 0.4, 0.6, 1.0], labels[负向, 中性, 正向], rightTrue, )0.08 这个步长的选择逻辑是命中 3 个正面词才加 0.24能把临界文本从负向拉到中性但不会把一个明显的负向评语直接改写成正向clamp到 0-1 是为了防止叠加越界。词表里的词要自己根据影片类型迭代动作片和文艺片的常用评价词完全不同。另一个隐藏参数是词的长短“炸裂”这种被拆分后语义反转的词比“演技炸裂”更容易误伤所以词表里优先放短语而不是单字词。两个词集合不要有交集否则同一条评语会同时加减等于白做。验证修正效果的标准做法是用豆瓣星级当硬标签4 星以上算正向2 星以下算负向然后和模型 label 做交叉表看负向召回是否明显提升。具体代码放在最后一章那里一起给出完整验证流程。4. 关键词提取与 wordcloud 可视化TF-IDF、停用词、中文字体4.1 关键词提取选 TF-IDF 还是 TextRank短评场景的取舍关键词提取在短评场景里默认选jieba.analyse.extract_tags也就是 TF-IDF 路线而不是 TextRank。原因在于 TextRank 依赖词与词在文本窗口里的共现关系短评单条太短拼接成大文本后窗口关系混乱提取结果往往不如 TF-IDF 稳定。TF-IDF 的核心逻辑是“在一个文档里频繁出现、但在整个语料里少见”的词更重要这正好匹配短评里那些反复被观众提到的记忆点。import jieba import jieba.analyse STOP_WORDS set() with open(stopwords.txt, encodingutf-8) as f: for line in f: word line.strip() if word: STOP_WORDS.add(word) all_text .join(df[text].tolist()) keywords jieba.analyse.extract_tags( all_text, topK30, withWeightTrue, allowPOS(n, vn, v, ns, a), ) keywords [ (word, weight) for word, weight in keywords if word not in STOP_WORDS and len(word) 2 ]topK30决定词云最多考虑多少个词withWeightTrue让结果带上 TF-IDF 权重后面喂给 wordcloud 时直接当词频用。allowPOS是词性过滤n名词、vn动名词、v动词、ns地点、a形容词这一组合拳能滤掉“我们”“真的”“什么”这类口语化高频词。这里有个隐藏细节extract_tags本身没有内置停用词参数常见做法是提取出结果后用自己维护的STOP_WORDS集合二次过滤同时用len(word) 2挡住单字噪音。停用词表不一定要下载现成的我习惯先跑一版把 top 词里明显无语义贡献的词手工补进 txt 再重跑。比如“电影”“一部”“感觉”这类词在不同项目里要按需决定是否保留反而比通用表更贴合业务。跑完打印前 20 个词如果还能看到代词和语气词说明停用词表不够这是后面避坑章节的核心话题之一。4.2 wordcloud 参数从字体到 mask 遮罩的完整配置词云图翻车九成出在中文字体上。wordcloud默认字体不支持中文不指定font_path的话输出全是方块。Windows 常见做法是指向simhei.ttfmacOS 指向PingFang.ttcLinux 可以先执行fc-list :langzh查可用字体再填路径。字体这事没有玄学就是每次换环境都别忘了确认一次。import numpy as np from PIL import Image from wordcloud import WordCloud mask np.array(Image.open(mask.png).convert(L)) wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width1200, height800, max_words100, background_colorwhite, maskmask, colormapRdYlGn, contour_width0.5, contour_colorblack, ) freq {word: float(weight) for word, weight in keywords} wc.generate_from_frequencies(freq) wc.to_file(douban_wordcloud.png)参数作用常见值font_path中文字体路径不设必乱码simhei.ttf / PingFang.ttcmask词云轮廓的 numpy 数组白底黑轮廓的 PNGmax_words最多显示词数80-120background_color画布底色white / 按汇报风格colormap配色方案RdYlGn / viridiscontour_width轮廓线粗细0.5不要轮廓则设 0最关键的是generate_from_frequencies(freq)而不是generate(text)。generate会对传入的文本重新分词如果你传入的是“词 词 词”这种用空格拼好的字符串分词器会把整串当英文处理词云直接碎掉。generate_from_frequencies接收的是{词: 权重}字典权重是 0 到 1 还是几万都不影响wordcloud 内部会自己归一化。mask.png建议用纯白底、黑色轮廓的图片.convert(L)转灰度图能避免透明通道干扰轮廓识别。4.3 让词云有业务意义好评与差评各自出图整批评论出一张词云只能看个热闹真正能放进汇报的是正负向对比。按上一章的label字段把数据拆成两部分分别拼文本、提取关键词、生成词云输出两张图。这样一眼就能看出好评集中在“演技”“剧情”“经典”差评集中在“拖沓”“特效”“烂尾”。for label_name in [正向, 负向]: sub df[df[label] label_name] text .join(sub[text].tolist()) kw jieba.analyse.extract_tags( text, topK30, withWeightTrue, allowPOS(n, vn, v, ns, a), ) kw [(w, f) for w, f in kw if w not in STOP_WORDS and len(w) 2] wc.generate_from_frequencies({w: float(f) for w, f in kw}) wc.to_file(fwordcloud_{label_name}.png)出图之后不要只看图好不好看要回到数据看比例如果负向短评只有 80 条而正向有 500 条差评词云的样本量可能撑不起 topK30 的统计意义。此时可以把差评的 topK 调低到 15或者干脆在图上标注样本量。文件命名里带上标签和日期比如wordcloud_负向_20240115.png防止迭代几轮后分不清哪张是旧图。这套流程跑通后换任何一部电影都只需要改movie_id重跑源码的组织方式决定了它的复用成本。5. 避坑排查编码、字体、反爬、情感偏差的五个典型翻车现场5.1 视觉系翻车词云方框与 CSV 乱码现象wordcloud 生成的 PNG 里全是方块一个汉字都显示不出来。原因很直接WordCloud 默认字体是 DroidSansMono它没有中文字形。解决把font_path参数指到系统里真实存在的中文字体文件。Windows 下常见路径是C:/Windows/Fonts/simhei.ttfmacOS 是/System/Library/Fonts/PingFang.ttcLinux 先跑fc-list :langzh看有哪些字体再填。我一开始也以为换个字体就行结果换了个不含中文字形的开源字体照样方块后来养成了“出图前先检查字形”的习惯。现象CSV 文件用 Excel 打开中文全乱码。原因Windows Excel 默认用 GBK 读取 CSV而 Python 写入时用的是 UTF-8。解决写文件时把 encoding 从utf-8改成utf-8-sig它会在文件开头加一个 BOM 标记Excel 就能正确识别编码。另一个相关坑是短评正文里可能带换行符会让一条评论在 CSV 里被拆成两行导致错位写入前先text.replace(\n, ).replace(\r, )处理一遍。这个坑非常隐蔽因为 pandas 读回 CSV 时不会报错只有后续按行统计时才发现条数对不上。5.2 数据系翻车403 与“请登录”情感分数和星级打架现象翻页翻到一半接口开始返回 403或者页面里只剩“请登录”字样评论列表为空。原因豆瓣反爬对连续高频请求有限流未登录的 Cookie 能看到的页数更少。解决把两次请求之间的休眠时间拉长到random.uniform(1, 3)单次运行只抓 200 到 500 条别开多线程硬刚。Cookie 会过期我把它放到环境变量里而不是写死在源码中失效后重新登录浏览器复制一次修改成本很低。还有一种常见做法是每天跑一次增量采集而不是一次性抓全量这样既躲限流又能让后续的情绪时间线分析有数据支撑。现象豆瓣星级打了 5 星的短评SnowNLP 判成负向情感分数和直觉明显矛盾。原因SnowNLP 训练语料偏电商“演技炸裂”里的“炸裂”在电商语境是负面的模型不知道影视语境里它是夸人。解决先用星级做硬标签交叉验证看负向召回率再用 3.3 节的自定义词表做加权修正。注意修正只解决高频词如果整体分数分布系统性偏移更稳的做法是把 0.4/0.6 阈值整体调整比如把负向边界抬到 0.45。这里不要迷信黑匣子模型输出永远只是特征不是结论。5.3 内容系翻车停用词污染 topK现象关键词提取出来 top 10 全是“我们”“真的”“没有”“什么”业务词一个都看不见。原因短评是口语化文本代词和副词出现频率天然高TF-IDF 的 IDF 部分在数据量不够时压不住它们。解决建停用词表提取结果出来后二次过滤同时用allowPOS限定词性。停用词表别指望一次建完第一版跑完把噪音词手工补进去来回迭代两三轮就稳定了。还有一个被忽略的细节单字词要过滤len(word) 2这个条件能把“烂”“好”“爽”挡在外面它们在词云里既不好看也没统计意义。建议把停用词表单独存成 txt每行一个词方便在多个电影项目间复用。6. 跑通之后的验证技巧用星级硬标签和日期聚合检查分析没跑偏6.1 交叉验证与每日情绪均值词云好看不等于分析正确我用两个方法验证整个 pipeline 有没有跑偏。第一个方法是星级硬标签交叉验证把豆瓣星级转成真实情感标签和 SnowNLP 的预测结果做交叉表直接揭露模型在哪类评论上翻车。rated df[df[rating] 0].copy() rated[hard_label] rated[rating].apply( lambda r: 正向 if r 4 else (负向 if r 2 else 中性) ) print(pd.crosstab(rated[label], rated[hard_label]))看交叉表时我关注两个数负向短评里被模型正确召回的比例以及正向短评的误杀率。某部文艺片可能有大量 3 星的“还行”被模型判成正向这不是模型坏了而是中性区间太窄。调整阈值到 0.35/0.65 再看交叉表效果往往立竿见影。第二个方法是日期聚合把短评按天分组求平均情感分存入 csv 后画一条折线。这个图能看出口碑随时间的变化比如上映首日和次周的分差比静态词云信息量大得多。df[date] df[time].str[:10] daily df.groupby(date)[sentiment].mean().reset_index() daily.to_csv(daily_sentiment.csv, indexFalse, encodingutf-8-sig)我第一次跑完整条链路时兴奋地把词云图拿给同事看结果 top 10 全是“我们”“真的”“没有”被当场追问结论是什么。从那以后我养成两个习惯任何词云出图之前先跑停用词过滤任何情感结论落地之前先和豆瓣星级对一次交叉表。这个项目的源码本身不难难的是不被中间产物骗过去。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑