简介:一份基于微博数据的舆情分析Python项目源码,整合了微博爬虫、LDA主题分析和情感分析三大核心环节,面向计算机、通信、人工智能、自动化等专业的学生、教师与从业者,可应用于期末课程设计、课程大作业或毕业设计。资源包共39个文件,以23个Python脚本为主,涵盖微博评论爬虫、用户信息爬虫、数据清洗、热度计算、情感分析的API/SDK两种实现、正向比重与评论均值统计、折线图绘制、LDA主题建模、分词处理、主题余弦相似度、词向量训练等模块,另有7个Markdown说明文档、7个TXT正负向语料与停用词表、w2v模型文件及xlsx结果表格,压缩包整体约16.16MB。项目按heat calculation、emotional analysis、weibo-crawler、LDA等目录分类组织,代码均经过调试可运行,方便按模块理解、改造或复现。已有211人学习下载,对于希望通过完整案例掌握微博舆情分析全流程、快速上手自然语言处理实践的学生或开发者,具有较好的学习借鉴价值。
1. 基于微博数据的舆情分析项目是什么,解决什么问题
很多开发者第一次接触舆情分析,都是从“爬点评论、画个词云”开始的,真到要输出结论时才发现:微博数据远比想象的脏,文本短、口语化重、新词多,常规 NLP 流程跑在上面各种翻车。基于微博数据的舆情分析项目,核心就是把「微博爬虫采集 → LDA主题分析 → 情感分析」这三段串成一条能落地的流水线,最终回答“大家最近在讨论什么、情绪是正还是负”。
这个项目最打动我的不是模型多高级,而是它把中文短文本处理里最痛的几个环节都覆盖了:爬虫要解决登录态与反爬,LDA 要处理短文档稀疏性,情感分析要面对阈值漂移。无论你是学生做课程设计,还是团队要搭一个舆情监控 Demo,这套组合都是最稳妥的起步方案。接下来我按一条完整链路来讲,不绕弯路,每步都给可以直接改着用的代码。
2. 微博爬虫:选接口、写采集、做清洗,三步搭起数据通道
2.1 为什么优先选移动端接口而不是网页端渲染页
微博网页端的 DOM 结构每隔几个月就会调整一次,而且大量内容是 JavaScript 异步渲染的,用 Selenium 硬抓不仅慢,还容易半夜被限制。做舆情项目,我一般优先选m.weibo.cn的移动端接口。它返回的是结构化 JSON,字段完整,开发效率高,稳定性也远比解析 HTML 好。
移动端接口的核心是/api/container/getIndex,一个通用容器接口,搜索、热门流、用户主页都能通过它拿数据。只要控制好请求频率和 Cookie 有效期,单机一天跑十万级数据量是可行的。这里的关键点在于 URL 参数containerid,它决定了你拉取的是哪个数据集合,搜索场景下格式是100103type=1&q=关键词。
注意:不要拿主账号登录抓取。微博对高频请求的账号会做限制,轻则临时封禁接口,重则要求短信验证。我一般单独申请一个低权重账号,专门跑采集。
2.2 一个可以直接运行的微博爬虫最小实现
这里给出一版我常用的搜索采集代码,去掉了业务封装,保留最核心的请求与解析逻辑:
import time import json import requests from bs4 import BeautifulSoup API_URL = "https://m.weibo.cn/api/container/getIndex" HEADERS = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1", "Referer": "https://m.weibo.cn/", "Cookie": "这里替换为你登录后的cookie", } def parse_mblog(mblog: dict) -> dict: """解析单条微博的卡片数据,去掉HTML标签与链接""" soup = BeautifulSoup(mblog.get("text", ""), "html.parser") for a in soup.find_all("a"): a.replace_with(a.get_text()) text = soup.get_text() return { "id": mblog.get("idstr"), "time": mblog.get("created_at"), "content": text, "reposts_count": mblog.get("reposts_count"), "comments_count": mblog.get("comments_count"), "attitudes_count": mblog.get("attitudes_count"), } def search_weibo(keyword: str, page: int) -> list: params = { "containerid": f"100103type=1&q={keyword}", "page_type": "searchall", "page": page, } resp = requests.get(API_URL, headers=HEADERS, params=params, timeout=10) data = resp.json() cards = data.get("data", {}).get("cards", []) results = [] for card in cards: if card.get("card_type") != 9: continue mblog = card.get("mblog", {}) if mblog.get("is_forward") == "1": # 过滤转发,只看原创 continue results.append(parse_mblog(mblog)) return results if __name__ == "__main__": keyword = "杭州亚运会" for page in range(1, 11): items = search_weibo(keyword, page) for item in items: print(json.dumps(item, ensure_ascii=False)) time.sleep(3) # 高频请求会被限制,这个等待不能省逻辑说明:card_type是卡片类型标识,值为 9 表示普通博文卡片,其他类型如热点聚合、推荐用户都要过滤掉。is_forward参数避免转发微博重复计入主题分析,这一点在舆情分析中很重要,转发内容与原微博在 LDA 里会被算成两篇重复文档,导致主题权重失真。
关于频率控制,time.sleep(3)是我验证过的下限。低于这个值容易触发风控;高于 8 秒数据采集速度会明显下降。如果想提速,做多账号轮换远比压低间隔更有效也更安全。
2.3 字段入库前的清洗链路与 Cookie 池机制
采集到的content字段还带一堆 HTML 标记和链接,直接拿去分词会掺入大量噪声。我通常在入库前做三件事:去 HTML 标签、去链接、截断过长的文本。
import re def clean_content(text: str) -> str: text = re.sub(r"http\S+", "", text) # 去掉 http/https 链接 text = re.sub(r"#.*?#", "", text) # 去掉话题词 text = re.sub(r"\[.*?\]", "", text) # 去掉 [haha] 这类表情占位符 text = re.sub(r"\s+", " ", text).strip() return text[:200] # 微博正文取前200字足够用于分析这里#.*?#是处理热搜话题的标准写法。话题词在舆情分析里其实有价值,但如果目的是做情感和主题,话题词乱入会让 LDA 的“主题”直接退化成“话题名集合”,所以我在预处理阶段选择拿掉它们。
Cookie 池的常规做法是准备三个账号的 Cookie,放进一个列表,每次请求前随机取一个。当接口返回的ok字段不为 1 时,自动切换到下一个 Cookie 并间隔 60 秒重试。不要用raise直接抛异常,微博接口经常返回“成功-但无数据”的伪失败状态。
3. LDA 主题分析:从微博短文本里提炼出人话能懂的主题
3.1 预处理决定主题质量:词典、停用词和文档合并
LDA 是经典的概率主题模型,但对中文微博这种“洗漱短文本”非常不友好。一条微博平均才几十个字,LDA 需要从文档-词共现矩阵里学习主题分布,文档太短、词汇太稀疏,主题就分不开。我在项目里用的核心策略是按用户+日期合并文档:把同一个用户在同一天发出的微博拼成一段文本,再送入 LDA。这样文档数量变少,单篇文档长度变长,主题分布稳定性明显改善。
分词环节,我使用结巴分词加载微博自定义词典。以下是一个实际跑通的预处理管线:
import jieba # weibo_dict.txt 每行一个词,例如: # 特种兵旅游 # 电子榨菜 # 冲上热搜 jieba.load_userdict("weibo_dict.txt") # 停用词表至少包含:展开、全文、图片、转发、超话、网页链接 stop_words = set([line.strip() for line in open("stopwords.txt", encoding="utf-8")]) def segment_and_filter(text: str) -> list: words = jieba.lcut(text) words = [w for w in words if len(w) > 1] # 去掉单字,纯语气词 words = [w for w in words if w not in stop_words] return words参数说明:len(w) > 1建议保留。单字在微博里基本都是“哈、嗯、啊、了”这类高噪声字,对主题区分没有任何帮助,还会挤占词典的有效词空间。自定义词典的价值在于让结巴分词不把“特种兵旅游”切成“特种兵”和“旅游”两个词,丢失语义信息。每次遇到新的热点事件,往词典里补 10 到 20 个新词,主题可解释性立竿见影。
3.2 K 值怎么选:别迷信一致性分数,人读得懂才作数
LDA 里最无从下手的是主题个数 K。官方教程里常用困惑度和一致性分数来选,但在微博场景我吃过亏:一致性分数最高的 K 值,往往主题区分度反而最差,因为短文本里词共现的随机性太大。
我现在的做法是先看主题词,再看分数。以下是我在项目中常用的 K 值试探逻辑:
- 从 K=8 开始训练,打印每个主题的前 10 个词。
- 如果某个主题的 10 个词能自然串成一句描述性的话,说明这个主题成立。
- 如果多个主题的前 5 个词大面积重叠,说明 K 偏大,减到 6。
- 如果不同主题之间完全看不出边界,全是零散词汇,说明 K 偏小,加到 12 或 15。
按我的经验,微博热点舆情的有效主题个数在 8 到 12 之间。K 超过 20 后,主题就开始拼凑词汇,基本不可读。做舆情报表时宁可 8 个主题也更愿意用 15 个主题,因为报表的阅读者是业务方,主题每个都要能起名字。
3.3 用 Gensim 训练 LDA:核心参数与完整代码
下面是我在项目中沿用的训练代码,参数都是调试过的:
from gensim import corpora, models from gensim.models import CoherenceModel # texts: 已经过分词和合并的文档列表,形如 [["亚运会", "开幕式", "烟花"], ...] dictionary = corpora.Dictionary(texts) dictionary.filter_extremes(no_below=5, no_above=0.5) corpus = [dictionary.doc2bow(doc) for doc in texts] lda_model = models.LdaModel( corpus=corpus, id2word=dictionary, num_topics=10, alpha=0.1, eta="auto", passes=20, iterations=200, random_state=42, # 固定种子,保证可复现 ) # 打印主题词 for topic_id in range(lda_model.num_topics): topic_words = lda_model.show_topic(topic_id, topn=15) print(f"主题 {topic_id}: " + " ".join(w for w, _ in topic_words)) # 计算一致性分数(辅助参考,不是唯一标准) cm = CoherenceModel( model=lda_model, texts=texts, dictionary=dictionary, coherence="c_v", ) print(f"Coherence: {cm.get_coherence():.4f}")逻辑说明:filter_extremes(no_below=5, no_above=0.5)是两把剪刀——no_below=5去掉出现次数少于 5 次的词(多为打错的字和个人化拼写),no_above=0.5去掉在超过一半文档中出现的词(多为微博平台通用词,如“微博”本身)。这两个参数对主题质量的影响比调 K 值更直接。
alpha=0.1是文档-主题先验的稀疏化设置。默认alpha="auto"会让模型自己学习,但在数据量不足时容易收敛到文档集中于单个主题的结果。我固定为 0.1 后,文档的主题分布更均匀,对后续按日聚合主题趋势有帮助。eta="auto"保留自动学习,因为主题-词分布的稀疏程度在不同语料上差别很大,人工指定容易出现极端结果。
4. 情感分析:从粗筛到人工校准,拿一个可信的舆论倾向
4.1 词典法与模型法怎么选:短期出数选前者,长期可控选后者
情感分析是整个舆情项目里最容易“表面繁荣”的环节。跑一遍出一堆 0.6、0.7 的分数,但把结果按时间画个曲线,正负比例永久贴着 50:50,这就是典型的“模型没有区分力”。我见过太多人拿着通用情感词典直接套微博文本,得到毫无意义的输出。
两条当前主流路线的选择标准如下:
- 词典打分法(SnowNLP、BosonNLP):零成本、跑得快,适合冷启动粗筛。但词典覆盖的是新闻和电商评论语料,对微博新词、谐音梗、反讽几乎没有招架之力,尤其“阴阳怪气”句式会被完全判反。
- 预训练模型微调(BERT 系列):效果上限高,能吸收微博特殊表达,但需要先人工标注几百到上千条样本。数据质量不好时,模型会把人工标注的偏差放大十倍。
我在实际项目中的选择是:先用 SnowNLP 跑全量,再用人工标注修正边界。这个策略一周内能出可用结果,成本低,而且不阻碍后续升级到模型微调。
4.2 用 SnowNLP 粗筛 + 阈值分段:代码与三维标签
下面是一段可以直接用的粗筛代码,产出正、负、中三个标签:
import pandas as pd from snownlp import SnowNLP df = pd.DataFrame(records) # records 是采集清洗后的微博数据列表 def sentiment_score(text: str) -> float: if not text or len(text) < 2: return 0.5 return SnowNLP(text).sentiments # 0~1,越接近1越正面 df["score"] = df["content"].apply(sentiment_score) def to_sentiment(x: float) -> str: if x >= 0.65: return "正面" elif x <= 0.35: return "负面" else: return "中性" df["sentiment"] = df["score"].apply(to_sentiment) print(df["sentiment"].value_counts(normalize=True))参数说明:阈值0.65/0.35不是通用参数,而是我实测后的经验值。SnowNLP 的分数分布有个特点:大量文本集中在 0.4 到 0.6 之间,直接按 0.5 分界会把中性文本硬拆成正反两半,制造出“正负均衡”的假象。把中间带定义为中性后,正负比例才会显出真正的波动。
有人会觉得这么设阈值太粗暴。我的回应是:情感分析在舆情项目里的产出不是“单条准不准”,而是“趋势可信不可信”。中性样本被分到正或负,对整体比例的干扰远大于保留为中性,两害相权,必须取中。
4.3 聚合指标:让情感分数和互动数据产生化学反应
舆情分析最后交给业务方的不是一条条的分数,而是一张“随时间变化的正负占比图”。我常用的聚合方式是考虑互动量的加权情感均值:
import pandas as pd # 假设 df 包含 sentiment 标签和点赞数 attitudes_count df["weight"] = df["attitudes_count"] + 1 # 平滑处理,避免全0 pivot = df.groupby(["hour", "sentiment"])["weight"].sum().unstack(fill_value=0) pivot["total"] = pivot.sum(axis=1) pivot["正占比"] = pivot["正面"] / pivot["total"] pivot["负占比"] = pivot["负面"] / pivot["total"]逻辑说明:加 1 是为了让那些只有 0 赞但有真实情绪的样本不至于被完全忽略。按小时聚合后,你可以做两件事:一是看整体情绪随事件演变的涨落,二是把负面占比突增的时间点提取出来,反查该时间段的原始微博。这种“指标波动 + 抽样复核”的闭环,是让业务方信服分析结论的最有效方式。
5. 舆情分析项目最常翻车的 4 个坑位与排查方案
5.1 爬虫请求返回 200,但数据是空 cards
现象:爬虫运行一段时间后,接口正常返回,但data.cards变成空列表,且不是关键词没有结果。
原因:两种最常见的诱因。一是 Cookie 被服务端标记失效,二是单 IP 请求频率触发临时限制。两者返回的 JSON 几乎一样,不打印原始响应很容易误判为“数据源没有内容”。
解决:在异常分支里打印resp.text前 200 个字符,检查是否包含WARN或risk字样。如果是频率控制,把请求间隔从 3 秒提高到 8 秒并等待 10 分钟;如果是 Cookie 失效,立刻切换到备用 Cookie 并重试。代码里不要用except Exception: pass吞掉异常,至少留下logger.warning。
5.2 LDA 主题词表里全是英文和“展开、全文”这类词汇
现象:训练出的主题词包括https、pic、com,甚至还有“展开”、“全文”这样的界面文字。
原因:微博正文的text字段里,长微博会被截断,并用“展开全文”按钮包裹实际内容。采集代码只剥离了<a>标签,但按钮文字留在纯文本里;图片的alt属性也被当成正文抓了进来。
解决:在清洗函数里显式增加一条规则:移除“展开全文”及兄弟节点。虽然这个文字本身不是正常用户言论,但它频繁出现在每条长博文里,会直接污染词典。
def clean_weibo_text(html: str) -> str: soup = BeautifulSoup(html, "html.parser") for span in soup.find_all(string="展开全文"): span.extract() # 直接删除该节点 for a in soup.find_all("a"): a.decompose() return soup.get_text()另外在停用词表里固定放入“全文、展开、图片、转发、超话”这几个微博高频界面词,双保险。
5.3 多个 LDA 主题高度重合,看不出边界
现象:主题 0 和主题 1 的前十个词完全重叠,或者所有主题都收敛到地名、时间和通用动词上。
原因:这是微博短文本在 LDA 场景下的典型病。单条微博只有 30 个字,LDA 无法从这么短的文档中学习稳定的词共现模式。再加上没有过滤低频词,噪声词的共现概率会主导主题生成。
解决:做文档合并。把同一用户同一天的微博合并成一条“伪文档”,合并后文档平均长度能到 300 字以上,主题区分度会明显改善。合并的代价是丢失单条微博的粒度信息,但舆情分析关注的是群体趋势,这个代价可以接受。如果合并后仍然同质,把 K 值往下降两档再试。
5.4 情感分布永远 50% 正面、50% 负面
现象:无论爬哪个关键词,正负占比都接近 50% 且几乎不动,情感曲线画出来像一条直线。
原因:这不是模型坏了,而是默认阈值 0.5 恰好落在语料分布最密集的区间。SnowNLP 对中性表达的打分通常在 0.45-0.55 之间,用 0.5 一刀切,中性文本被随机分成正和负,大量随机性抹平了真实信号。
解决:抽 100 条分数落在 0.35 到 0.65 之间的样本做人工标注。看真实标注里中性占比多少,再把阈值往中性区间边界推。多数情况下 0.65/0.35 是合理的起点,但必须用标注数据验证,不是抄参数。
6. 让舆情分析真正可信:小时级曲线校验与主题轮动验证
最后的交付不是模型本身,而是你能指着输出说“这个趋势是真的”。我每次跑完数据,都会多做两步验证。
第一步是画小时级情感曲线。用采集到的created_at字段按小时聚合情感均值,如果某小时的有效样本少于 10 条,我会直接过滤掉。低样本量时段的情感均值全是噪声,画进去只会让报表阅读者产生困惑。
df["hour"] = pd.to_datetime(df["time"]).dt.floor("H") hourly = df.groupby("hour")["score"].agg(["mean", "count"]) valid = hourly[hourly["count"] >= 10] # 低样本时段丢弃第二步是主题轮动验证。把 LDA 输出的文档-主题分布按天汇总,看每个主题的当日权重和前一天比是否稳定。一个可靠的舆情分析结论,应该能复现同一事件次日同主题的权重走势,而不是在结果里随机冒出一个从未见过的新主题。
doc_topics = lda_model.get_document_topics(corpus) # doc_topics[i] 是文档 i 的主题分布,结合日期字段聚合成日级主题占比 day_topic = pd.DataFrame( [ {"day": day, "topic": topic_id, "weight": weight} for doc_id, day in enumerate(days) for topic_id, weight in doc_topics[doc_id] ] ) pivot_topic = day_topic.pivot_table( index="day", columns="topic", values="weight", aggfunc="sum" ).fillna(0)我个人的习惯是:每次训练完模型后,强制自己复盘一遍采集日志和清洗脚本,再去看模型输出。很多舆情结论失真是从源头丢数据开始的——Cookie 池空了没报警、清洗正则把关键表情吞了、文档合并粒度不对。这些环节都查一遍,再谈模型调参,否则只是给错误数据披了件漂亮外衣。
做法一套完整的链路,足够支撑大多数舆情场景。如果你正准备入这个方向,先把爬虫的 Cookie 池和清洗规则做好,再谈主题和情感模型。希望这些踩坑记录能帮你省掉几周的调试时间,也让你交付的每一轮舆情分析结论都能经得起复核。
本文还有配套的精品资源,点击获取