
简介一份基于微博数据的舆情分析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它决定了你拉取的是哪个数据集合搜索场景下格式是100103type1q关键词。注意不要拿主账号登录抓取。微博对高频请求的账号会做限制轻则临时封禁接口重则要求短信验证。我一般单独申请一个低权重账号专门跑采集。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: f100103type1q{keyword}, page_type: searchall, page: page, } resp requests.get(API_URL, headersHEADERS, paramsparams, timeout10) 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_asciiFalse)) 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(rhttp\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, encodingutf-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 值试探逻辑从 K8 开始训练打印每个主题的前 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_below5, no_above0.5) corpus [dictionary.doc2bow(doc) for doc in texts] lda_model models.LdaModel( corpuscorpus, id2worddictionary, num_topics10, alpha0.1, etaauto, passes20, iterations200, random_state42, # 固定种子保证可复现 ) # 打印主题词 for topic_id in range(lda_model.num_topics): topic_words lda_model.show_topic(topic_id, topn15) print(f主题 {topic_id}: .join(w for w, _ in topic_words)) # 计算一致性分数辅助参考不是唯一标准 cm CoherenceModel( modellda_model, textstexts, dictionarydictionary, coherencec_v, ) print(fCoherence: {cm.get_coherence():.4f})逻辑说明filter_extremes(no_below5, no_above0.5)是两把剪刀——no_below5去掉出现次数少于 5 次的词多为打错的字和个人化拼写no_above0.5去掉在超过一半文档中出现的词多为微博平台通用词如“微博”本身。这两个参数对主题质量的影响比调 K 值更直接。alpha0.1是文档-主题先验的稀疏化设置。默认alphaauto会让模型自己学习但在数据量不足时容易收敛到文档集中于单个主题的结果。我固定为 0.1 后文档的主题分布更均匀对后续按日聚合主题趋势有帮助。etaauto保留自动学习因为主题-词分布的稀疏程度在不同语料上差别很大人工指定容易出现极端结果。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(normalizeTrue))参数说明阈值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_value0) pivot[total] pivot.sum(axis1) 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( indexday, columnstopic, valuesweight, aggfuncsum ).fillna(0)我个人的习惯是每次训练完模型后强制自己复盘一遍采集日志和清洗脚本再去看模型输出。很多舆情结论失真是从源头丢数据开始的——Cookie 池空了没报警、清洗正则把关键表情吞了、文档合并粒度不对。这些环节都查一遍再谈模型调参否则只是给错误数据披了件漂亮外衣。做法一套完整的链路足够支撑大多数舆情场景。如果你正准备入这个方向先把爬虫的 Cookie 池和清洗规则做好再谈主题和情感模型。希望这些踩坑记录能帮你省掉几周的调试时间也让你交付的每一轮舆情分析结论都能经得起复核。本文还有配套的精品资源点击获取