ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微博评论情感分析与LDA主题分析实战:完整项目代码拆解

微博评论情感分析与LDA主题分析实战:完整项目代码拆解

简介:这是一套面向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, params=params, headers=headers, timeout=10) 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 变量里取;filter=all表示不加好友过滤,拿的是全量评论。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", encoding="utf-8") text = df["comment"].astype(str) # 第一步:去掉 URL text = text.apply(lambda x: re.sub(r"https?://\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", encoding="utf-8").read().splitlines()) df["clean_text"] = text df.to_csv("comments_clean.csv", index=False, encoding="utf-8")

这里每一步都有讲究。先删 URL 是因为短链里的英文字母串会被分词器切成无意义 token,污染词袋;@用户名删掉是因为它在 LDA 里毫无区分度,所有评论都有,属于高频冗余词;话题标签我们选择删除而不是拆词,因为舆情分析关注的是评论内容,话题本身单独分析更合适。停用词表在项目目录里给了一份已经整理好的,包含标点、语气词、常用虚词和“微博”“转发”“哈哈”这类评论高频无意义词。

2.3 爬虫运行前必须处理的三个前置条件

这套爬虫不是双击就能跑的,运行前有三件事必须确认,否则大概率白跑一趟。

第一是 Cookie 有效期。微博的 Cookie 通常几小时到几天不等,爬前先在浏览器登录微博账号,再打开开发者工具把请求头里的 Cookie 复制进代码。写死一个过期 Cookie,requests 返回 302 跳转到登录页,解析出来是空数据,很多人第一步就卡在这。第二是评论接口的id参数。它得是微博正文链接里的mblogid对应的mid,不是 URL 里的那串显眼 ID,最稳妥的办法是在微博正文页源码里搜mid字段。第三是数据落盘格式。代码里保存 CSV 时要带上created_at字段,因为后面 LDA 按日期切片、情感分析按时间聚合都要用这个字段,等清洗完再回去补时间戳会很痛苦。

3. LDA 主题模型实战:分词边界、超参调优与主题可解释性

3.1 从分词到 LDA:Gensim 实现里的关键参数与文件分工

LDA 部分项目里给了分词处理.py、LDA.py、LDA+超参.py、excel转txt.py、自建词表.txt和近义词表.txt这几个文件。先说excel转txt.py:微博数据爬回来存储格式可能是 xlsx,LDA 读取需要纯文本,这个脚本就是格式转换的桥梁。分词处理.py做的事情是逐条加载清洗后的评论,用 jieba 分词,加载自建词表,去掉停用词,输出成一行一个文档的格式。

这里最容易忽略的是自建词表的作用。微博评论里大量出现品牌名、明星名、产品型号、网络新词,比如“iPhone 15”“狂飙”“显眼包”。这些词 jieba 默认词典里没有,会被拆成碎片,“李佳琦”被切成“李佳”和“琦”,主题结果完全没法解释。自建词表.txt就是干这个用的,每个词一行,在 jieba 初始化时加载进去。

# 分词处理.py 核心片段 import jieba jieba.load_userdict("自建词表.txt") stopwords = set(open("停用词表.txt", encoding="utf-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", encoding="utf-8") as f: docs = f.readlines() with open("comments_seg.txt", "w", encoding="utf-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", encoding="utf-8") as f: texts = [line.strip().split() for line in f] # 建立词袋字典:只保留出现次数 >= 3 且出现在前 80% 文档中的词 dictionary = Dictionary(texts) dictionary.filter_extremes(no_below=3, no_above=0.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( corpus=corpus, id2word=dictionary, num_topics=k, alpha=alpha, eta=beta, passes=20, # 训练轮数,越多稳定性越好 iterations=200, # 每次采样的迭代次数 random_state=42 ) cm = CoherenceModel( model=model, texts=texts, dictionary=dictionary, coherence="c_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}, " f"coherence={best_coh:.4f}") best_model = LdaModel( corpus=corpus, id2word=dictionary, num_topics=best_k, alpha=best_alpha, eta=best_beta, passes=20, iterations=200, random_state=42 ) best_model.save("lda_model")

这个脚本把最常用的 LDA 超参都覆盖了。参数说明:num_topics是主题数,这是 LDA 唯一最敏感的变量,取值范围依数据量走,几千条评论 15 到 25 个主题比较合理,数据量大到十万条以上再往 50 走;alpha是文档-主题分布的先验,值越小文档主题越集中,symmetric适合均匀分布场景;beta是主题-词分布的先验,eta="auto"让模型自己学词的稀疏性;passes=20意味着模型对整个语料循环 20 遍,太小模型不收敛,太大训练时间暴涨。random_state=42是固定随机种子,保证每次训练结果一致,之后换参数对比才有意义。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(topicid=topic_id, topn=10) 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", index=False, encoding="utf-8")

拿到这张主题词表后,得逐个主题检查:主题词能不能归纳成一个显著话题?比如“手机/质量/售后/客服/态度”可以归纳为售后体验;如果“质量”和“手机”分开出现在不同主题,说明模型还没把它们聚到位。常见做法是先把明显双胞胎主题合并,再把无法解释的主题删除,最后保留 5 到 8 个高解释性主题做报告,不要照搬模型输出的全部主题。这个步骤看着土,实际上是主题分析能否落地到舆情报告里的关键——纯机器输出的主题没人看得懂。

4. 情感分析实现:正向/负向语料、否定词处理和 API/SDK 双方案

4.1 词典法情感分析:正向语料、负向语料与多日期降维逻辑

情感分析这块项目给了两套路径:情感分析_API版.py和情感分析_SDK版.py走云端接口,正向语料.txt、负向语料.txt是给本地词典法用的语料。先看本地方案,它的逻辑是:用正向词表和负向词表对每条评论打分,正向词命中 +1,负向词命中 -1,出现否定词(如“不”“没”“无”)则反转分数。

# 情感分析_API版.py 中的本地词典打分简化逻辑 pos_words = set(open("正向语料.txt", encoding="utf-8").read().splitlines()) neg_words = set(open("负向语料.txt", encoding="utf-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", marker="o", linewidth=2, label="情感均值") ax1.set_xlabel("日期") ax1.set_ylabel("评论量") ax2.set_ylabel("情感均值") plt.title("舆情热度与情感走势") plt.tight_layout() plt.savefig("opinion_trend.png", dpi=200)

这图的参数要注意:twinx()创建双纵轴后,两个轴的尺度各自独立,评论量是几千的量级、情感均值是 -1 到 1 的区间,强行共用纵轴会压平情感波动。dpi=200是出图清晰度,放到报告里至少 150,屏幕预览 100 以下会糊。

从那以后,我每次跑微博舆情项目都是这套流程走到底:先爬数据拿真实语料,清洗到能见人,再分词跑 LDA 把主题定出来,再打情感分做时间序列趋势,最后把热度、情感和主题相似度汇总成一页图。每次我在实测新方案时会强制在本地用小批量数据先验证,再放到全量上跑,避免把时间浪费在不收敛的训练和脏数据上。这套代码的完整文件里包含了今天讲的每一份脚本,数据也是齐的,直接在 PyCharm 里按顺序跑通就能复现全流程。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进