
简介这是一套面向NLP学习者、舆情分析人员及数据挖掘初学者的微博评论分析项目完整覆盖从微博爬虫、数据清洗、分词预处理到LDA主题建模与情感分析的全流程代码与数据可直接运行复现。压缩包整体约16.16MB共39个文件其中23个py脚本承担爬虫、热度计算、LDA超参调优、情感分析及可视化等核心功能7个md说明文档帮助梳理项目结构与运行逻辑7个txt提供停用词表、正负向语料及自建词表另有w2v.model词向量模型与新增.xlsx结果数据。项目还给出主题余弦相似度计算、多日期降维、折线图绘制等细化脚本方便结合具体业务做扩展。目前已有2326人学习浏览。通过运行这套项目既能掌握LDA主题分析和情感分析在真实短文本数据上的落地方法也能获得一套可用于课程设计、毕业设计或舆情监控课题的完整参考工程。1. 为什么这套微博评论情感分析与 LDA 主题分析代码值得你拆一遍很多人在 PyCharm 里跑微博评论的 LDA 主题分析跑完一看主题词全是“哈哈”“转发”“微博”根本没法解释。问题多半不在模型而在数据清洗和分词环节。这套我们拆出来的项目代码把「微博爬虫 → 数据清洗 → LDA 主题建模 → 情感分析 → 热度与可视化」串成了一条完整链路每步脚本独立可跑数据也是直接给好的。和网上那些只讲原理、给你一段 demo、没有真实语料的教程不同它的文件清单里有评论爬虫、分词处理、LDA 与超参脚本、正向负向语料、SDK 和 API 两版情感分析以及热度计算的三个版本。对想用真实微博评论做舆情分析的研究生、做竞品口碑监控的产品经理、以及刚入门 NLP 的工程师来说这是能让你少踩坑、直接复现的好东西。2. 微博评论爬虫两类采集脚本、Cookie 处理与评论去重2.1 先看懂爬虫文件分工随机版、按年过滤版与用户信息版项目正文里爬虫部分有四个文件分工很明确comment crawler.py是基础版按照指定微博链接抓评论适合小批量定点采集comments-crawler_random.py加了随机延时和随机链接池适合大规模抓取降低被微博反爬风控的概率comments-crawler_random仅针对去年的评论.py是带时间过滤的版本只保留去年发布的评论做年度舆情对比时很省事user information crawler.py则是抓用户档案用于分析评论用户的粉丝量、地域、是否认证等画像信息。这套划分符合真实的舆情项目分工先定目标账号或话题链接——不定链接就把热门微博的评论全量滚下来——再按时间切片最后把用户信息和评论关联。代码里用的方案是直接构造 Ajax 请求拿评论 JSON而不是解析 HTML这样解析速度快、字段干净不做无用功。# comments-crawler_random.py 核心请求逻辑简化版 import requests import random import time headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, # 关键Cookie 用一个真实登录账号的 Cookie不登录会被重定向到 passport.weibo.com Cookie: 你的微博登录 Cookie, Referer: https://weibo.com } for page in range(1, 200): # 控制翻页深度 url https://weibo.com/ajax/comment/list params { id: blog_id, # 微博正文的 mid page: page, # 页码 filter: all # 全部评论不按关注人过滤 } resp requests.get(url, paramsparams, headersheaders, timeout10) data resp.json() comments data.get(data, {}).get(list, []) for item in comments: text item.get(text_raw, ).strip() # 过滤掉空评论和“转发微博”这类无意义占位内容 if text and text ! 转发微博: save_to_csv(text, item.get(created_at, )) # 随机延时 5-15 秒避免频率过高触发风控 time.sleep(random.uniform(5, 15))这段代码逻辑很直白用 Ajax 接口直接拿结构化数据逐页翻取评论正文和时间字段命中空文本或占位文本就丢弃。参数说明id是目标微博的 mid不是你看到的 URL 那串字母需要从微博详情页的 JS 变量里取filterall表示不加好友过滤拿的是全量评论。time.sleep(random.uniform(5, 15))这个随机区间是踩过坑之后的经验值固定 5 秒容易被识别成机器随机区间在 5 到 15 秒之间比较稳。2.2 数据清洗停用词、URL、用户和表情符的过滤顺序有讲究拿到原始评论后data cleaning.py做的是标准中文文本清洗流程。但和通用清洗不同微博评论有自己特有的脏数据短链网址、用户名、话题词、表情符、HTML 实体。我们试过先分词再清洗结果一堆 URL 碎片被分进主题里LDA 主题词惨不忍睹。正确顺序是先删除 URL 和 用户名再替换表情符为空格最后做去重和停用词过滤。# data cleaning.py 清洗流程核心片段 import re import pandas as pd df pd.read_csv(comments_raw.csv, encodingutf-8) text df[comment].astype(str) # 第一步去掉 URL text text.apply(lambda x: re.sub(rhttps?://\S|www\.\S, , x)) # 第二步去掉 用户名,保留后面的正文内容 text text.apply(lambda x: re.sub(r[\u4e00-\u9fa5\w\-][:]?, , x)) # 第三步去掉话题标签只保留话题内部的文字 text text.apply(lambda x: re.sub(r#[^#]#, , x)) # 第四步去掉转义字符和多余空格 text text.apply(lambda x: re.sub(r\\u\d{4}|\s, , x).strip()) # 第五步按停用词表做词级过滤 stopwords set(open(停用词表.txt, encodingutf-8).read().splitlines()) df[clean_text] text df.to_csv(comments_clean.csv, indexFalse, encodingutf-8)这里每一步都有讲究。先删 URL 是因为短链里的英文字母串会被分词器切成无意义 token污染词袋用户名删掉是因为它在 LDA 里毫无区分度所有评论都有属于高频冗余词话题标签我们选择删除而不是拆词因为舆情分析关注的是评论内容话题本身单独分析更合适。停用词表在项目目录里给了一份已经整理好的包含标点、语气词、常用虚词和“微博”“转发”“哈哈”这类评论高频无意义词。2.3 爬虫运行前必须处理的三个前置条件这套爬虫不是双击就能跑的运行前有三件事必须确认否则大概率白跑一趟。第一是 Cookie 有效期。微博的 Cookie 通常几小时到几天不等爬前先在浏览器登录微博账号再打开开发者工具把请求头里的 Cookie 复制进代码。写死一个过期 Cookierequests 返回 302 跳转到登录页解析出来是空数据很多人第一步就卡在这。第二是评论接口的id参数。它得是微博正文链接里的mblogid对应的mid不是 URL 里的那串显眼 ID最稳妥的办法是在微博正文页源码里搜mid字段。第三是数据落盘格式。代码里保存 CSV 时要带上created_at字段因为后面 LDA 按日期切片、情感分析按时间聚合都要用这个字段等清洗完再回去补时间戳会很痛苦。3. LDA 主题模型实战分词边界、超参调优与主题可解释性3.1 从分词到 LDAGensim 实现里的关键参数与文件分工LDA 部分项目里给了分词处理.py、LDA.py、LDA超参.py、excel转txt.py、自建词表.txt和近义词表.txt这几个文件。先说excel转txt.py微博数据爬回来存储格式可能是 xlsxLDA 读取需要纯文本这个脚本就是格式转换的桥梁。分词处理.py做的事情是逐条加载清洗后的评论用 jieba 分词加载自建词表去掉停用词输出成一行一个文档的格式。这里最容易忽略的是自建词表的作用。微博评论里大量出现品牌名、明星名、产品型号、网络新词比如“iPhone 15”“狂飙”“显眼包”。这些词 jieba 默认词典里没有会被拆成碎片“李佳琦”被切成“李佳”和“琦”主题结果完全没法解释。自建词表.txt就是干这个用的每个词一行在 jieba 初始化时加载进去。# 分词处理.py 核心片段 import jieba jieba.load_userdict(自建词表.txt) stopwords set(open(停用词表.txt, encodingutf-8).read().splitlines()) def segment(line: str) - str: words jieba.lcut(line.strip()) # 精确模式切分 # 过滤单字、纯数字、停用词 words [w for w in words if w not in stopwords and len(w) 1 and not w.isdigit()] return .join(words) with open(comments_clean.txt, r, encodingutf-8) as f: docs f.readlines() with open(comments_seg.txt, w, encodingutf-8) as f: for line in docs: seg segment(line) if seg.strip(): f.write(seg \n)这段代码里必须注意两个参数load_userdict(自建词表.txt)要在 jieba 第一次分词前执行放后面加载不生效过滤条件里len(w) 1会把单个字的重要词比如“该”这种语气词去掉干净但也损失极少信息舆情分析场景可以接受。分词后的结果存成comments_seg.txt每行是一篇文档这就是 LDA 的输入。3.2 LDA 训练Gensim 的 LdaModel 与超参选取逻辑分词完成后LDA.py和LDA超参.py做的是把分词结果转成词袋向量再用 Gensim 的 LdaModel 训练。LDA超参.py比LDA.py多做了网格搜索——对不同主题数 K 和超参 alpha、beta 组合计算主题一致性coherence选出最优组合而不是拍脑袋定一个 K。# LDA超参.py 核心片段 from gensim.corpora import Dictionary from gensim.models import LdaModel, CoherenceModel import numpy as np # 读取分词结果每行一个文档 with open(comments_seg.txt, r, encodingutf-8) as f: texts [line.strip().split() for line in f] # 建立词袋字典只保留出现次数 3 且出现在前 80% 文档中的词 dictionary Dictionary(texts) dictionary.filter_extremes(no_below3, no_above0.8) corpus [dictionary.doc2bow(text) for text in texts] best_coh, best_k, best_alpha, best_beta 0, 20, auto, auto # 常见做法固定主题数 10-40 之间走一遍alpha/beta 用对称参数试 for k in [10, 15, 20, 25, 30]: for alpha in [0.01, 0.1, symmetric]: for beta in [0.01, 0.1, auto]: model LdaModel( corpuscorpus, id2worddictionary, num_topicsk, alphaalpha, etabeta, passes20, # 训练轮数越多稳定性越好 iterations200, # 每次采样的迭代次数 random_state42 ) cm CoherenceModel( modelmodel, textstexts, dictionarydictionary, coherencec_v ) score cm.get_coherence() if score best_coh: best_coh, best_k, best_alpha, best_beta ( score, k, alpha, beta) print(f最优参数: K{best_k}, alpha{best_alpha}, beta{best_beta}, fcoherence{best_coh:.4f}) best_model LdaModel( corpuscorpus, id2worddictionary, num_topicsbest_k, alphabest_alpha, etabest_beta, passes20, iterations200, random_state42 ) best_model.save(lda_model)这个脚本把最常用的 LDA 超参都覆盖了。参数说明num_topics是主题数这是 LDA 唯一最敏感的变量取值范围依数据量走几千条评论 15 到 25 个主题比较合理数据量大到十万条以上再往 50 走alpha是文档-主题分布的先验值越小文档主题越集中symmetric适合均匀分布场景beta是主题-词分布的先验etaauto让模型自己学词的稀疏性passes20意味着模型对整个语料循环 20 遍太小模型不收敛太大训练时间暴涨。random_state42是固定随机种子保证每次训练结果一致之后换参数对比才有意义。Coherence 分数是一个相对值高代表主题词之间的语义关联性强一般大于 0.4 就是可解释的。3.3 主题可解释性检查词分布、主题权重与近义词拆分训出模型后真正费精力的是给每个主题起名字。项目里近义词表.txt用于辅助合并语义相近的主题比如“手机”和“苹果”如果落在不同主题但近义词表显示它们强相关就要考虑是不是 K 选大了。# 查看每个主题的前 10 个词与权重 import pandas as pd model LdaModel.load(lda_model) dictionary Dictionary.load(lda_model.dict) topic_words [] for topic_id in range(model.num_topics): word_probs model.show_topic(topicidtopic_id, topn10) words [w for w, p in word_probs] probs [round(float(p), 4) for w, p in word_probs] topic_words.append({ topic_id: topic_id, words: /.join(words), probs: probs }) df pd.DataFrame(topic_words) df.to_csv(topic_words.csv, indexFalse, encodingutf-8)拿到这张主题词表后得逐个主题检查主题词能不能归纳成一个显著话题比如“手机/质量/售后/客服/态度”可以归纳为售后体验如果“质量”和“手机”分开出现在不同主题说明模型还没把它们聚到位。常见做法是先把明显双胞胎主题合并再把无法解释的主题删除最后保留 5 到 8 个高解释性主题做报告不要照搬模型输出的全部主题。这个步骤看着土实际上是主题分析能否落地到舆情报告里的关键——纯机器输出的主题没人看得懂。4. 情感分析实现正向/负向语料、否定词处理和 API/SDK 双方案4.1 词典法情感分析正向语料、负向语料与多日期降维逻辑情感分析这块项目给了两套路径情感分析_API版.py和情感分析_SDK版.py走云端接口正向语料.txt、负向语料.txt是给本地词典法用的语料。先看本地方案它的逻辑是用正向词表和负向词表对每条评论打分正向词命中 1负向词命中 -1出现否定词如“不”“没”“无”则反转分数。# 情感分析_API版.py 中的本地词典打分简化逻辑 pos_words set(open(正向语料.txt, encodingutf-8).read().splitlines()) neg_words set(open(负向语料.txt, encodingutf-8).read().splitlines()) negation_words {不, 没, 无, 非, 莫, 别, 未曾} def sentiment_score(sent: str) - float: words jieba.lcut(sent.strip()) score 0.0 negate False for w in words: if w in negation_words: negate True continue if w in pos_words: score score - 1 if negate else score 1 elif w in neg_words: score score 1 if negate else score - 1 negate False # 否定词只作用于紧邻的下一个情感词 return score这段代码处理了两个关键细节第一否定词反转不仅针对积极的词也针对消极的词“产品质量不好”里的“不好”会让负向词“好”被翻转成负分第二否定词作用范围限定在紧邻的下一个情感词而不是整句“这个手机不便宜但很好用”如果整句反转就会算错取近邻更符合中文表达习惯。注意项目里叫 API 版但核心判断逻辑依然是词典打分真正的 API 调用是在云端接口版里封装的。多日期降维.py配合情感分析使用把情感分数按日期维度聚合得到每日的平均情感值作为舆情走势预测的基础数据。它对原始情感分数做的是日粒度聚合后再平滑不是直接对全部数据算均值这样能回避因个别极端评论导致的情感值失真。4.2 API 与 SDK 方案对比什么时候用云接口什么时候用本地词典对比项本地词典法正向/负向语料API/SDK 版云端 NLP成本零成本离线运行按调用量计费需要申请密钥速度毫秒级不依赖网络受网络和接口 QPS 限制精度对网络新词和反讽不敏感模型覆盖泛化好反讽识别更强隐私数据不出本机评论数据上传到云端注意合规适用批量历史语料复盘实时舆情接口接入项目里两个版本都放出来适合的做法是混合历史数据用本地词典法跑全量做离线统计分析实时新评论用 API/SDK 版做增量判断弥补词典法对“绝绝子”“yyds”这类网络新词的覆盖率不足。代码里 API 版需要申请云平台的密钥SDK 版也是同样的逻辑封装只是 HTTP 调用被 SDK 包了一层配置项无非是密钥、接入点 URL、超时重试。4.3 情感分析的正确打开方式先校准时域字段再聚合我在跑情感分析时翻过车直接把情感分数按天画折线图发现情绪波动毫无规律查了半天才发现是时间字段的问题。爬虫拿到的时间如果没清洗是 UTC 格式或“x分钟前”这种相对时间直接按日期分组会把大量评论错位。项目的修改日期格式.py解决的就是这个问题把爬虫输出的时间统一转成yyyy-mm-dd绝对日期再做聚合。落地时建议的流程是先把每一条评论用词典法打上情感分再按日期把每日情绪均值算出来同时对当天的评论量做计数。情感均值反映的是舆论的情绪倾向评论量反映热度两个指标叠在一起才看得出事件发酵和情绪拐点。5. 舆情分析避坑指南从爬虫、LDA 到情感分析的常见翻车记录这一章整理了拆项目过程中我踩过和见过别人踩的坑集中在爬虫、分词建模、情感判断三个阶段。每一条都是真实翻车现场附解决路径照着排查能省下大量无效时间。5.1 现象一爬虫抓到一半开始返回空列表程序不报错原因微博接口对短时间高频请求做了限流不返回 403 错误而是返回data.list为空的正常响应。解决不能只看状态码必须检测返回内容里data.list的存在性。如果连续三页都是空列表立刻停止并进入冷却休眠建议冷却时间从 5 分钟起比一直跑下去浪费时间靠谱。# 空列表检测与冷却机制 empty_pages 0 for page in range(1, 200): data fetch_comments(page) # 前面封装好的请求函数 if not data.get(data, {}).get(list): empty_pages 1 if empty_pages 3: print(连续 3 页无数据触发冷却) time.sleep(300) # 冷却 5 分钟 empty_pages 0 continue empty_pages 0 save_comments(data)这段代码的要点是连续空页计数器必须独立于正常页逻辑冷却后不能直接从当前页继续翻要从冷却前的页码继续否则容易漏数据。5.2 现象二LDA 主题词被单字和语气词占据主题完全不可解释原因分词阶段没有过滤纯语气词也没加载自建词表导致“的”“了”“啊”“呀”这类无意义词在主题中权重极高。解决先确认停用词表是否在 jieba 分词后立即生效然后检查filter_extremes的no_below参数把no_below提高到 5把低频孤词和极高频噪声同时压掉。这个参数组合很玄学建议先跑到 3看主题词不满意再加到 5。5.3 现象三情感分析把“电池不耐用”判成正向原因词典法打分时“耐用”在正向语料中权重高但前面的“不”字被分词拆开了否定词没起到反转作用。解决分词器要支持自定义词典把“不耐用”切成“不”“耐用”两个 token且否定词列表要覆盖“不”“不太”“不怎么”这些带程度否定的说法再把“不耐用”整体加入负向语料。仅仅加否定词列表还不够要重新跑一遍全部历史评论确保旧数据的情感分被修正。5.4 现象四按日期画情感折线图曲线毛刺多完全看不出趋势原因直接对每日情感均值画图没有做平滑而且周末评论量和工作日差异大导致部分小样本日期均值被极端评论拉歪。解决先对日期聚合后的情感序列做 7 日滑动平均再画折线图评论量小于 50 条的日期降权或直接标记缺失不参与平滑防止单日噪声污染整体趋势。这份代码里折线图绘制.py就是这类处理的标准模板。5.5 现象五训练 LDA 用全量数据性能慢到你怀疑人生原因没有先做抽样探索直接拿几十万条评论去 fine-tune 超参单次训练就是几十分钟网格搜索跑几十个组合等于把时间耗死在机器上。解决超参搜索阶段只随机抽 10% 数据且passes降到 5先找出相对好的参数区间确定参数后再用全量数据重新训练一次把passes升回 20。这样整体耗时能缩短到原来的三分之一以下且最终质量几乎不受影响。6. 用热度曲线、情感均值与主题相似度串起完整舆情分析报告工具链拆完最后这一步是把各个脚本的输出拼装成一份完整的分析结论。我的做法是固定走这一套流程每次有新数据都强制过一遍形成肌肉记忆。先看热度脚本。项目里给了热度_1.py、热度_2.py、热度_3.py三个版本它们代表三种热度口径版本一按评论数直接统计版本二把转发、评论、点赞按权重加权算热度权重经验值是转发 0.5、评论 0.3、点赞 0.2版本三在前者基础上引入时间衰减因子越是新发布的评论权重越高。做舆情危机预警时用版本三做季度分析用版本一就够。再看关联指标。Comment mean.py计算每条微博的平均评论数Emotional mean.py按日计算情感均值Number of new employees.py配合新增.xlsx画新增员工数曲线主题余弦相似度.py用w2v.model计算不同日期切片间的主题相似度。这四个指标组合起来就是一套完整的舆情体检指标脚本用途热度加权值热度_3.py判断事件发酵强度日情感均值Emotional mean.py判断舆论情绪消化程度主题余弦相似度主题余弦相似度.py判断话题是否偏移或发散新增员工数Number of new employees.py判断人工回复资源是否匹配最后写一段聚合代码把情感均值和热度画在双纵轴图上。这就是舆情监测里最常见的走势图——柱状是每日评论量折线是情感均值一眼看出某日爆发负面情感伴随评论量飙升这就是需要干预的舆情拐点。# 双纵轴评论量柱状 情感均值折线 import matplotlib.pyplot as plt import pandas as pd df pd.read_csv(daily_metrics.csv, parse_dates[date]) fig, ax1 plt.subplots(figsize(12, 6)) ax2 ax1.twinx() ax1.bar(df[date], df[comment_count], color#4C72B0, label评论量) ax2.plot(df[date], df[sentiment_mean], color#C44E52, markero, linewidth2, label情感均值) ax1.set_xlabel(日期) ax1.set_ylabel(评论量) ax2.set_ylabel(情感均值) plt.title(舆情热度与情感走势) plt.tight_layout() plt.savefig(opinion_trend.png, dpi200)这图的参数要注意twinx()创建双纵轴后两个轴的尺度各自独立评论量是几千的量级、情感均值是 -1 到 1 的区间强行共用纵轴会压平情感波动。dpi200是出图清晰度放到报告里至少 150屏幕预览 100 以下会糊。从那以后我每次跑微博舆情项目都是这套流程走到底先爬数据拿真实语料清洗到能见人再分词跑 LDA 把主题定出来再打情感分做时间序列趋势最后把热度、情感和主题相似度汇总成一页图。每次我在实测新方案时会强制在本地用小批量数据先验证再放到全量上跑避免把时间浪费在不收敛的训练和脏数据上。这套代码的完整文件里包含了今天讲的每一份脚本数据也是齐的直接在 PyCharm 里按顺序跑通就能复现全流程。希望帮到你。本文还有配套的精品资源点击获取