
简介以电影《雄狮少年》为案例的网络舆情分析项目源码包面向高校计算机、数学、电子信息等专业学生适用于课程设计、期末大作业及毕业设计参考。包内共157个文件主要包含44个Python脚本、40个CSV数据文件另有Markdown说明文档、HTML可视化页面、图片与文本数据整体约40.47MB覆盖网络数据采集、文本预处理、情感分析、结果可视化等完整流程。资源收录了知乎、百度资讯、微博等多平台关于该电影的相关讨论数据并附有项目说明文档方便理解分析思路和模块设计代码结构清晰、注释较完整可直接运行也便于二次开发或更换研究案例。目前已有65人学习浏览适合希望以真实影视话题开展舆情分析实训、需要可复现案例的开发者参考借鉴。1. 拆解《雄狮少年》舆情数据CSV、来源与认识误区《雄狮少年》在上映期间出现了典型的口碑与票房背离豆瓣评分不低社交平台却吵成两派知乎相关问题的浏览量高得反常。很多课程设计拿到这包资源后第一反应是跑一下情感分析看“好评还是差评”但真正有价值的洞察恰恰藏在那些无法被简单归类的文本里——比如批评画风的和称赞剧情的往往来自不同渠道讨论“高口碑低票房”的帖子和讨论“吴京发文”的帖子在时间上有明显错峰。这个基于电影《雄狮少年》的网络舆情分析项目本质是一次完整的中文舆情分析链路示范从采集自百度资讯、百家号、知乎的多个 CSV 语料出发到清洗去重、分词、情感打分、主题聚类最后落成可视化图表非常适合作为计算机、数学、电子信息专业的课程设计或毕设入门模板。这也是为数不多拿到手就能改字段名复用到其他热点事件的资源但前提是先搞清楚每个 CSV 到底采集了什么、有多少脏数据、时间字段能不能对齐。下面按照数据工程的实际顺序来拆。2. 数据底账理清 CSV 来源字段与文本去重策略2.1 资源包内文件构成与采集口径解压后能看到all.csv、data.csv、百度资讯-百家号搜索采集.csv以及若干以知乎问题命名的 CSV比如如何看待动画电影《雄狮少年》.csv、如何看待《雄狮少年》高口碑低票房.csv、如何看待演员吴京发文支持动画电影《雄狮少年》.csv。文件名基本就是采集来源这是舆情项目最常见的数据组织方式先按平台、按问题分别落盘再合并成统一格式。all.csv和data.csv看起来像已合并或部分合并的结果也可能是早期清洗后人肉整理的版本实际动手时建议先对一遍行数和字段避免后面重复处理。每个 CSV 的字段大致包含标题、正文摘要、来源站点、发布时间、原文链接以及采集时抓到的阅读量或评论数取决于采集脚本当时是否勾选了该字段。这类字段结构非常典型正是舆情分析里说的“弱结构化文本”标题和正文不一定同时完整来源站点决定了文本风格差异发布时间则用来做时序分析。拿到资源后我一般会先做一个字段摸底确认每个 CSV 的列名是否一致因为从不同平台采集的文件往往列名略有出入比如有的叫publish_time、有的叫ptime这会直接导致后续合并报 KeyError。import pandas as pd import glob csv_files glob.glob(*.csv) for f in csv_files: df pd.read_csv(f, encodingutf-8-sig, nrows5) print(f, -, df.columns.tolist(), rows:, len(pd.read_csv(f, encodingutf-8-sig)))这段代码用glob扫描当前目录下所有 CSV逐个读取前 5 行打印列名再统计总行数。encodingutf-8-sig是为了兼容从 Windows 端采集工具导出的带 BOM 文件否则单独用utf-8可能在列名首字符上出现\ufeff前缀后续所有groupby和rename都会踩坑。先跑这一步目的是确定哪些文件需要统一列名映射哪些文件存在空标题但正文有内容的情况。2.2 跨 CSV 合并与 URL 去重舆情文本最容易被重复数据带偏合并文本类 CSV 有个容易被忽略的问题同一篇文章可能被百度资讯和百家号各采集一次或者同一篇知乎回答同时在标题和摘要里出现。如果不做去重情感分析和主题建模的统计结果会被重复文本放大——比如某条极端负面内容被采集了三次负面占比会凭空上涨几个百分点。去重的首选键是原文链接 URL其次才是文本哈希。def load_and_merge(csv_file_list, text_colcontent, url_colurl): frames [] for f in csv_file_list: df pd.read_csv(f, encodingutf-8-sig) # 统一列名映射避免不同采集批次列名不一致 rename_map {publish_time: pub_time, ptime: pub_time, title: headline, content: content, 正文: content} df df.rename(columnsrename_map) if text_col not in df.columns: # 某些文件可能只有标题回退拼接标题和摘要 df[text_col] df[headline].fillna() df.get(summary, ).fillna() frames.append(df[[headline, text_col, pub_time, url_col]]) merged pd.concat(frames, ignore_indexTrue) merged merged.dropna(subset[text_col]).drop_duplicates(subset[url_col], keepfirst) return merged这里的关键参数是drop_duplicates(subset[url_col])按 URL 去重会保留最早采集到的那条记录keepfirst因为早期采集的版本往往带更完整的原文。rename_map里的映射是常见兼容层实际运行时你可以打印df.columns.tolist()把缺失字段补齐。URL 去重后建议再做一层文本归一化去重——将空格、换行、全角半角统一后再计算内容哈希进一步清理同文不同链路的转载文本。这一步做不做直接决定后面 LDA 主题数选出来是真实话题还是重复内容噪声。3. 中文分词与情感倾向从 jieba 自定义词典到 SnowNLP 得分修正3.1 为什么不能直接对影评文本做英文式情感分析中文舆情文本与英文 sentiment analysis 最大的差异在于分词边界和否定词缀。《雄狮少年》语料里大量出现“舞狮”“岭南”“阿娟”“咸鱼强”这类专有名词通用分词器往往把“舞狮”切成“舞”和“狮”或者把“阿娟”切成“阿”和“娟”。情感词被切碎之后情感打分几乎等于随机。另外电影名本身在文本里高频出现如果直接进 TF-IDF 或 Word2Vec它会把话题特征淹没需要用到停用词表和自定义词典配合处理。一个比较稳的做法是先加载用户词典把电影角色名、导演名、方言词汇、剧情关键词强制绑定成完整词再做分词。项目中可以看到jieba.load_userdict(userdict.txt)的影子这类文件通常每行一个词可附带词频和词性比如阿娟 100 nr。词频建议设置在 50 到 200 之间太低会让新词在文本里过于强势太高则可能覆盖原有正确切分。以下代码可以验证自定义词典是否生效import jieba import jieba.analyse jieba.load_userdict(userdict.txt) text 阿娟在废弃工厂里练习舞狮最后天台一跃让人热血沸腾 print(jieba.lcut(text)) # 输出应为: [阿娟, 在, 废弃, 工厂, 里, 练习, 舞狮, , 最后, 天台, 一跃, 让人, 热血沸腾] tags jieba.analyse.extract_tags(text, topK5, withWeightTrue) for word, weight in tags: print(word, weight)lcut返回全模式分词列表如果你看到“阿娟”被拆成“阿”和“娟”说明用户词典没有加载成功检查userdict.txt路径是否在运行目录下、编码是否为 UTF-8。extract_tags底层是 TF-IDF 的变体实现topK控制返回关键词数量withWeightTrue返回权重。这个权重不是概率而是经过 IDF 修正的归一化得分用于后续词云可视化时确定文字字号很合适。3.2 情感打分SnowNLP 的默认模型偏差与业务校准方法SnowNLP 是中文文本情感分析里最常用的轻量库自带电商评论语料训练的模型。直接拿它跑电影舆情会出现两个问题第一它对“燃”“炸裂”“泪目”这类电影情绪词偏向中性甚至负面因为电商语料里这些词不常见第二对“画风丑”“人物建模奇怪”这类具体批评反而可能给出偏正面的得分因为句子结构里没有明显否定词。所以直接把 SnowNLP 的得分当作结论在课程设计答辩时容易被一句话问住。常见做法是用人工标注的一小批样本做分数校准。从资源包里抽取二百条文本按“非常负面到非常正面”五个档手工打标然后对比 SnowNLP 原始得分与标注值的偏差学习一个线性修正参数。具体实现是把 SnowNLP 得分作为唯一特征用逻辑回归或简单最小二乘拟合修正函数。from snownlp import SnowNLP import pandas as pd def sentiment_label(text: str) - float: s SnowNLP(text) return s.sentiments # 原始得分 0~1越接近1越正面 sample pd.DataFrame({ text: [天台舞狮那段太燃了, 人物画风真的劝退, 剧本扎实配乐加分], human: [1.0, 0.1, 0.8] }) sample[snownlp_score] sample[text].apply(sentiment_label) print(sample)SnowNLP(text).sentiments返回的是一个 0 到 1 之间的浮点数越接近 1 代表越正向。注意它对短文本非常敏感一句“太燃了”可能只有 0.6但如果加上“天台舞狮”主题背景人工判断会更极端。所以项目里建议的修正思路是对原始分数做一次clip(score, 0.05, 0.95)避免极端值干扰再按人工标注样本做一阶线性回归。如果人工标注样本太少至少要做分位数切分把连续得分映射为“负向/中性/正向”三档这样可视化堆叠柱状图时更稳定不怕个别噪声点。3.3 分平台情感对比百度资讯与知乎问答的口径差异舆情项目和普通情感分析的一个明显区别是必须考虑平台口径。百度资讯和百家号采集到的文本多为自媒体文章标题特点是耸动、夸大、情绪词密度高一条标题可能承载整篇文章的核心态度知乎问答则通常有完整论述情绪分散在长段落里直接对全文打分容易被中立叙述稀释。实践中最实用的折中方案是对标题和正文分别计算情感得分然后加权合并。def weighted_sentiment(row): title_score SnowNLP(row[headline]).sentiments if isinstance(row[headline], str) else 0.5 body_score SnowNLP(row[content]).sentiments if len(str(row[content])) 20 else title_score return 0.6 * title_score 0.4 * body_score merged[sentiment_score] merged.apply(weighted_sentiment, axis1) merged[sentiment_label] pd.cut( merged[sentiment_score], bins[0, 0.35, 0.65, 1.0], labels[negative, neutral, positive] )这里pd.cut的bins边界是常见经验值0.35 以下视为负向、0.65 以上视为正向中间算中性。加权比例 0.6/0.4 表示更相信标题的倾向性因为在自媒体语境下标题是作者态度的浓缩表达。实际操作时你可以分别统计两类的均值如果知乎问答的情感均值明显低于百度资讯往往不是观点差异而是长文本打分被中性段落稀释了——这是项目说明里不会写但答辩时特别容易被问到的一个观察点。4. LDA 主题建模与舆情焦点演化困惑度选择、中文停用词与时间趋势验证4.1 从 CSV 到 LDA 输入分词清洗与向量化的顺序LDA 主题模型是这类舆情分析项目的标准配置用来回答“大家到底在吵什么”。做 LDA 之前必须先完成分词、去停用词、去掉纯数字和单字词等步骤。中文里“了”“的”“吧”“吗”这类词如果不过滤每个主题都会呈现出一堆虚词模型跑完也看不出差异。需要准备一份中文停用词表常见做法是把哈工大停用词表、百度停用词表和四川大学机器智能实验室停用词表合并去重。项目中可以把停用词表命名为cn_stopwords.txt放在与源码同目录然后用以下代码加载import pandas as pd from sklearn.feature_extraction.text import CountVectorizer import jieba def tokenize_for_lda(text: str) - list: words jieba.lcut(text) stopwords set() with open(cn_stopwords.txt, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) return [w for w in words if w.strip() and w not in stopwords and len(w) 1 and not w.isdigit()] corpus merged[content].dropna().tolist() vectorizer CountVectorizer(tokenizertokenize_for_lda, max_features3000, min_df5, max_df0.6) doc_term_matrix vectorizer.fit_transform(corpus)max_features3000控制词典规模舆情语料常见词在几千量级设得太大主题会发散太小则丢失低频但关键的事件专有词。min_df5表示至少要在 5 篇文本中出现过滤掉只出现过一两次的人名拼写错误或采集产生的乱码词max_df0.6表示在超过 60% 文本中都出现的词会被过滤掉这类词通常是“电影”“动画”“影片”这类话题公共词对区分主题没有贡献反而会让每个主题看起来都一样。值得注意tokenizer参数接收的是函数对象不是函数调用结果写错成tokenizertokenize_for_lda()会在拟合时报 missing positional argument。4.2 困惑度与主题数选择不要只盯着最低点LDA 聚类效果对主题数 K 的选择很敏感。舆情语料不像新闻分类语料那样有明确类别边界不同人群对“讨论什么”的感知不同因此不能单纯靠困惑度曲线取最小值。常见做法是分别训练 K3 到 K10 的模型输出每个模型的困惑度和主题词列表靠人工判断主题词是否可解释。from gensim.models import LdaModel from gensim.corpora import Dictionary from gensim.models.coherencemodel import CoherenceModel texts [tokenize_for_lda(doc) for doc in corpus] dictionary Dictionary(texts) bow_corpus [dictionary.doc2bow(text) for text in texts] coherence_scores [] for k in range(3, 11): lda LdaModel(bow_corpus, num_topicsk, id2worddictionary, passes15, random_state42) cm CoherenceModel(modellda, textstexts, dictionarydictionary, coherencec_v) score cm.get_coherence() coherence_scores.append((k, score)) print(fK{k}, coherence{score:.4f})passes15控制遍历语料的次数数值越大模型越稳定但时间开销线性增长。舆情语料通常只有几千条15 次足够如果语料超过几万条可以降到 5 到 8 次。random_state42固定随机种子保证可复现否则跑两次实验主题顺序不一致没法写进报告。coherencec_v是最常用的主题一致性指标一般选择得分局部峰值且主题词列表语义清晰的 K 值。实践里《雄狮少年》这套语料通常 K5 或 K6 时效果比较好主题大致落在画风争议、剧情评价、票房讨论、导演与行业观点、角色塑造几个方向。4.3 舆情时间演化用 pyLDAvis 验证主题出现的时间窗口主题模型跑完后如果只是打印主题词列表说服力不够。舆情分析更关心的是“高口碑低票房”这个争议什么时候开始升温、吴京发文事件带来了多少新增讨论、画风争议是否一直占据主导。做法是把文档按发布时间聚合计算每个主题在每个时间窗口内的平均文档占比。import pyLDAvis.gensim.models import pyLDAvis pyLDAvis.enable_notebook() vis pyLDAvis.gensim.models.prepare(lda, bow_corpus, dictionary) pyLDAvis.display(vis)pyLDAvis.gensim.models.prepare会把每个主题下的词语分布投射到二维平面左边选择主题右边看到每个词该主题内的权重。如果某个主题的词全部挤在一团说明主题粒度太粗如果某个词的圆圈很大但距离主题中心很远说明这个词是跨主题的公共词停用词表还应该再加词。时间维度上可以对每个主题选出占比最高的 20 个文档观察它们的发布时间分布占比高且时间集中在同一周的文档往往是某个事件驱动的热点。这一步做完整个分析就有了“话题是什么 什么时候讨论 谁在讨论”的完整链条。5. 项目复用与坑位排查把《雄狮少年》模板迁移到新热点事件这套源码最值得带走的不是某个具体结果而是从 CSV 输入到图表输出的整条流水线。迁移到新事件时只需要替换语料文件、更新自定义词典和停用词表、调整 LDA 主题数别名映射规则可以原样复用。一个实操技巧是把词典和停用词表放在独立配置目录用pathlib.Path定位资源文件避免迁移时出现相对路径找不到文件的报错。from pathlib import Path BASE_DIR Path(__file__).parent DICT_PATH BASE_DIR / data / userdict.txt STOPWORD_PATH BASE_DIR / data / cn_stopwords.txt jieba.load_userdict(str(DICT_PATH))用Path(__file__).parent的好处是无论当前工作目录在哪路径都基于源码文件所在目录计算不会出现从命令行运行时路径失效的问题。data目录建议放一份原始 CSV、一份清洗后的 long 格式 CSV、一份主题聚合结果 CSV数据血缘清晰课程设计报告里能直接贴目录结构图。排查报错时按频率排序最高发的是三个问题。第一pd.read_csv报UnicodeDecodeError这是采集文件混入 GBK 编码所致处理办法是捕获异常后换编码重读或者用errorsignore忽略非法字符但要注意后一种做法可能丢字。第二LDA 训练时报ValueError: not enough values通常是min_df设太高导致词典为空或太小调低到 3 或 2 即可。第三情感得分全部集中在 0.4 到 0.6 之间这不是模型坏了而是长文本的中性化稀释效应需要像第 3 章那样做标题加权的条目级分数修正。def safe_read_csv(file_path: str): try: return pd.read_csv(file_path, encodingutf-8-sig) except UnicodeDecodeError: return pd.read_csv(file_path, encodinggbk, errorsignore)另一个容易被忽略的细节是all.csv与data.csv中可能存在完全一样的文本但 URL 字段为空这样按 URL 去重无法生效。此时切换为按内容 SHA256 哈希去重哈希键为标题加正文拼接后的净化字符串。净化规则包括全角转半角、统一大小写、删除连续空白。对舆情分析而言重复数据是系统性偏差比如“如何评价动画电影《雄狮少年》”这个 CSV 下的回答如果被并入data.csv后再被合并主题分布会被单边拉偏务必在两个合并层级都检查去重逻辑。本文还有配套的精品资源点击获取