微博舆情分析全流程:爬虫、LDA主题与情感分析实战及避坑指南
2026/9/24 21:56:30 网站建设 项目流程

简介:基于微博数据的舆情分析项目源码资料,定位为面向计算机、通信、人工智能、自动化等专业学生与从业者的可运行毕业设计/课程设计参考。项目整合微博爬虫、LDA主题分析与情感分析三条主线,从数据采集、文本清洗、分词处理到模型构建与可视化均有完整实现,适合小白入门,也可作为二次开发基础。资源共39个文件,压缩包16.16MB,以Python脚本为主,辅以Markdown说明、文本语料、词向量模型与Excel结果表,各模块目录清晰,便于按需调用与复现实验。目前已有210人学习下载。整套项目曾获答辩评审98分,代码经过调试验证,可直接运行;除源码外还附带正向/负向语料、停用词表、近义词表以及多日期降维、热度和情感均值计算等扩展脚本,便于理解舆情分析全流程,也能支撑期末大作业、课程设计与毕业设计的改造升级。

1. 从一条微博到一份舆情报告:这个项目到底值不值得跑

基于微博数据的舆情分析项目,听起来是套「微博爬虫 + LDA + 情感分析」的标准组合,真正跑一遍才发现,最耗时间的不是训练模型,而是把采集到的微博数据洗干净、把三个模块串成一条能复现的流水线。微博爬虫本身不复杂,难在采集策略和去重;LDA 主题分析也不难,难在主题数 K 怎么定、结果怎么解释;情感分析同样如此,短文本、网络用语和反讽会让词典和模型同时失效。这篇文章按爬虫、LDA、情感分析、避坑、落地的顺序完整拆开,给出可执行命令与参数,适合想快速跑通、并把它扩展成自己监控体系的一线工程师。

2. 微博爬虫:选对接口、守住合规边界,把采集流程跑通

2.1 为什么首选 m.weibo.cn 移动端接口而不是 Web 端或开放平台

做微博数据采集,第一步不是写代码,是选入口。Web 端 weibo.com 页面里大量内容靠异步渲染,接口参数带加密签名,自己解析费时费力;开放平台虽然正规,但个人开发者申请不到全量搜索权限,接口配额也只够做 demo。行业内常见的做法是走移动端网页版 m.weibo.cn,它的接口路径短、参数简单、字段完整,登录后复制 Cookie 就能直接拉数,适合舆情分析这种对数据量要求不高、对字段完整性要求高的场景。

获取 Cookie 的方式不复杂:用浏览器打开 m.weibo.cn,登录账号,打开开发者工具,找到任意一条请求,复制请求头里的 Cookie 字符串。注意要把整个 Cookie 都复制下来,只复制半截会在请求时报 401。拿到了 Cookie,先验证一下能不能拉到数据,再上规模。

import requests import time import random headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15", # 用你自己的 Cookie 替换下面这行,越完整越好 "Cookie": "换成登录后复制的完整 Cookie", "Referer": "https://m.weibo.cn/" } def search_weibo(keyword, pages=5): results = [] for page in range(1, pages + 1): url = "https://m.weibo.cn/api/container/getIndex" params = { "containerid": f"100103type=1&q={keyword}", "page_type": "searchall", "page": page } try: resp = requests.get(url, headers=headers, params=params, timeout=10) data = resp.json() except Exception as e: print(f"第{page}页请求失败: {e}") time.sleep(random.uniform(5, 10)) continue cards = data.get("data", {}).get("cards", []) for card in cards: mblog = card.get("mblog", {}) if not mblog: continue results.append({ "id": mblog.get("idstr"), "text": mblog.get("text"), "created_at": mblog.get("created_at"), "user": mblog.get("user", {}).get("screen_name"), "reposts_count": mblog.get("reposts_count"), "comments_count": mblog.get("comments_count"), "attitudes_count": mblog.get("attitudes_count") }) time.sleep(random.uniform(1, 3)) return results

这段代码最核心的参数是containerid,它决定了搜的是关键词还是某个用户的主页。100103type=1&q=是移动端关键词搜索的标准拼接方式,page_type=searchall表示综合搜索,想只看热门微博可以改成searchhottime.sleep(random.uniform(1, 3))是给每次请求之间加的随机间隔,别小看这一行,舆情采集不是瞬时任务,频率太高大概率触发风控。timeout=10设短一点,失败就跳过,不要无限重试。

另外说一句合规问题。微博的公开内容可以用于个人学习和研究,但要控制采集频率、不对外传播原始数据、不做商业化用途。舆情项目最忌讳的是把采集器写成了高压请求器,被封号只是小事,惹上法律风险就得不偿失了。

2.2 采集管线三件套:限速、去重、断点续采

单次搜索跑完只是第一步,舆情项目往往要连续采很多天,甚至每小时跑一次。这时候就要给采集流程加三个模块:限速、去重、断点续采。限速在上一段代码里已经做了雏形,去重和断点续采我建议直接用 SQLite,轻量、零部署、单文件,几百 MB 的数据完全扛得住。

import sqlite3 def init_db(db_path="weibo.db"): conn = sqlite3.connect(db_path) conn.execute("""CREATE TABLE IF NOT EXISTS weibo_posts ( id TEXT PRIMARY KEY, content TEXT, created_at TEXT, user TEXT, reposts_count INT, comments_count INT, attitudes_count INT, fetched_at TEXT DEFAULT (datetime('now')) )""") conn.commit() return conn def save_posts(conn, posts): for p in posts: try: conn.execute( "INSERT INTO weibo_posts (id, content, created_at, user, reposts_count, comments_count, attitudes_count) VALUES (?, ?, ?, ?, ?, ?, ?)", (p["id"], p["content"], p["created_at"], p["user"], p["reposts_count"], p["comments_count"], p["attitudes_count"]) ) except sqlite3.IntegrityError: # 主键冲突说明这条微博已经入库,直接跳过 continue conn.commit()

去重逻辑靠id TEXT PRIMARY KEY这一行完成,微博 id 是全局唯一的,重复插入时数据库会抛IntegrityError,跳过即可。断点续采的思路更简单:脚本崩了重新跑,已经入库的记录自动跳过,只需要在采集前加一个「查一下这条 id 在不在库里」的判断,或者干脆直接插入、由主键兜底。这种方案的优点是代码量少,缺点是如果采集崩在中间页,翻页偏移可能要重扫一页,但舆情分析对几页的重复无感,可接受。

实际跑采集时,把search_weibosave_posts串起来,外层套一个for循环遍历关键词列表,每个关键词之间 sleep 10 到 30 秒,这是控制风险最有效的手段。

2.3 微博签到数据与地域舆情:一种低成本的扩展采集

很多舆情需求不只关心「说了什么」,还关心「人在哪里说」。微博的签到数据就是一个低成本的切入口。m.weibo.cn 的地点签到接口返回用户地理位置和签到时间,字段里带poi_idlocation,按城市或商圈维度聚合,就能画出舆情的空间分布。

# 查询某个 POI 地点的签到微博 curl "https://m.weibo.cn/api/container/getIndex?containerid=230283_${POI_ID}&page=1" \ -H "Cookie: 你的Cookie" \ -H "User-Agent: Mozilla/5.0"

实际操作时,先用城市名做关键词搜索,从结果里提取poi_id,再用230283_{poi_id}去拉这个地点的全部签到微博。这种方式和前面关键词搜索的区别在于,签到数据天然带地理位置标签,后续做地域舆情分析时不用再依赖 IP 解析,数据可信度高很多。缺点是签到数据只覆盖主动打开定位并签到的用户,样本有偏,分析结论要注明这一点。

3. LDA 主题分析:把几万条微博压缩成可读议题

3.1 文本清洗与分词:jieba + 停用词 + 自定义词典

爬下来的微博正文带着 HTML 标签、@用户、话题和链接,这些噪声如果不清理,会直接污染 LDA 的主题词。常见的清洗步骤是一个正则管道:去标签、去 @、去话题、去 URL,最后再做分词。分词我用 jieba,配合一份通用停用词表,再加一个项目相关的自定义词典。

import jieba import re STOPWORDS = set() with open("stopwords.txt", encoding="utf-8") as f: for line in f: STOPWORDS.add(line.strip()) CUSTOM_DICT = "weibo_dict.txt" # 每行一个词,例如:网红带货 10 nz if CUSTOM_DICT: jieba.load_userdict(CUSTOM_DICT) def clean_text(text): text = re.sub(r"<[^>]+>", "", text) # 去 HTML 标签 text = re.sub(r"@[\w\u4e00-\u9fa5\-]+", "", text) # 去 @用户 text = re.sub(r"#[\w\u4e00-\u9fa5]+#", "", text) # 去话题 text = re.sub(r"https?://\S+", "", text) # 去链接 return text def tokenize(text): words = jieba.lcut(text) return [w.strip() for w in words if w.strip() and w not in STOPWORDS and len(w) > 1 and not re.match(r"^\d+$", w)] corpus = [tokenize(clean_text(t)) for t in texts]

这段代码的细节都在过滤条件里。len(w) > 1把单字词、语气词过滤掉,not re.match(r"^\d+$", w)把纯数字过滤掉。STOPWORDS建议收集三类词:第一类是「微博、转发、图片、网页链接」这类平台词,第二类是「今天、现在、真的、感觉」这类无观点词,第三类是「哈哈、啊啊、呜呜」这类网络语气词,第三类不处理,后面主题词里会出现一堆拟声词。自定义词典直接关系分词效果,比如一条微博里写「爱豆又出新歌了」,没有词典时 jieba 会把「爱豆」切成「爱」和「豆」,加了词典才能切出一个完整词。

3.2 主题数 K 怎么定:困惑度与一致性对比

LDA 里最常被问的问题就是「主题数设几个」。说实话,这个参数一开始是玄学,拍脑袋定一个数字跑出来也能看,但能不能用、准不准,要有量化依据。Gensim 提供了两个现成指标:困惑度和主题一致性。困惑度越低模型越好,但它在主题数太大时会过拟合,所以还要结合一致性来综合判断。

from gensim.corpora import Dictionary from gensim.models import LdaMulticore, CoherenceModel dictionary = Dictionary(corpus) dictionary.filter_extremes(no_below=5, no_above=0.5) bow_corpus = [dictionary.doc2bow(doc) for doc in corpus] coherence_values = [] perplexity_values = [] for k in range(4, 15): lda = LdaMulticore( corpus=bow_corpus, id2word=dictionary, num_topics=k, workers=4, passes=10, random_state=42, alpha="auto" ) cm = CoherenceModel(model=lda, texts=corpus, dictionary=dictionary, coherence="c_v") coherence_values.append(cm.get_coherence()) perplexity_values.append(lda.log_perplexity(bow_corpus))

filter_extremes(no_below=5, no_above=0.5)的含义是:词频低于 5 的词剔除(减少低频拼写错误干扰),出现在超过 50% 文档里的词剔除(去掉「微博」这类太多见无区分度的词)。一致性指标选c_v,它比u_mass更接近人工判断,跑出来的值趋势是「先升后降」,选峰值对应的 K 就是推荐值。注意每次跑 LDA 最好固定random_state,否则结果不可复现,参数调优时没法对比。

跑完 K 值曲线,不要直接信峰值,要拿峰值附近的两个 K 值都跑一遍,人工看主题词。峰值的 K 大约是 10,10 个主题里有两个主题词长得差不多,就减到 9 再跑一次。这一步是必要的,LDA 是概率模型,同一个 K 值不同次运行结果也有波动,模型评估是辅助,最终解释权在业务手里。

3.3 用 pyLDAvis 让主题从数字变成可汇报的结论

LDA 训练完,输出的是每个主题的词分布,直接给别人看数字没人愿意读。pyLDAvis 是 Gensim 生态里常用的可视化工具,它把主题变成一张交互式气泡图,左边是主题气泡,右边是关键词列表,气泡越大代表主题占比越高,拖到某个气泡上就能看到该主题的核心词。

import pyLDAvis.gensim vis_data = pyLDAvis.gensim.prepare(lda, bow_corpus, dictionary) pyLDAvis.save_html(vis_data, "lda_vis.html")

生成 HTML 文件后直接在浏览器打开,不需要起服务。看图的技巧是先点最大的气泡,把前五关键词抄下来,用一句话概括这个主题。比如主题词是「降价、补贴、购车、新能源、提车」,就能打标签为「新能源汽车促销」。所有主题标完,再统计每个主题在全部文档里的占比,这就是舆情议题分析的主产出了。占比本身就是业务方最关心的数据,相当于告诉他们:这几天大家讨论的热点里,哪个话题声量最大、涨得最快。

4. 情感分析:从词典打分到模型分类的落地取舍

4.1 先跑通基线:情感词典 + 否定词 + 程度副词的打分器

情感分析的第一版做法,我建议用情感词典打分,别上来就上 BERT。词典法的优势是快、可解释、改起来容易,对微博这种噪声大的短文本,一个几百行的打分函数能覆盖相当一部分场景,而且跑完你就知道问题出在哪,再决定要不要升级模型。

POS_WORDS = set(open("pos_dict.txt", encoding="utf-8").read().splitlines()) NEG_WORDS = set(open("neg_dict.txt", encoding="utf-8").read().splitlines()) DEGREE_WORDS = {"超级": 2.0, "非常": 1.8, "很": 1.5, "有点": 0.7, "不太": 0.5} def sentiment_score(text): words = tokenize(clean_text(text)) score = 0.0 for i, w in enumerate(words): base = 0 if w in POS_WORDS: base = 1.0 elif w in NEG_WORDS: base = -1.0 else: continue # 否定词反转 if i > 0 and words[i-1] in {"不", "没", "无", "别"}: base = -base # 程度副词加权 if i > 0 and words[i-1] in DEGREE_WORDS: base *= DEGREE_WORDS[words[i-1]] score += base return score

打分器的核心规则就三条:情感词定极性,否定词做反转,程度副词做加权。pos_dict.txtneg_dict.txt可以直接用知网情感词典或者网络公开词表,按行存放,一个词一行。这个版本的优点是你能解释每一条微博为什么得分高、为什么得分低,比如「有点差」算出来是 -0.7,「不太差」算出来是 -0.5,逻辑透明,业务方追问起来你拿得出依据。

缺点也很明显:词典覆盖不全、否定词只处理了前一个词、反讽完全无效。所以这个版本定位是基线,用途是快速跑通流程,给后续模型版本提供一个对照分数。

4.2 升级到 SnowNLP 与领域迁移的边界

词典法做基线跑通以后,很多人会自然想到换 SnowNLP,它内置了一个训练好的情感模型,两行代码就能出一个 0 到 1 的情感得分,用起来很省事。但这里有个血泪经验:SnowNLP 训练语料主要来自电商评论,直接迁移到微博文本上,效果会明显下降,尤其是网络新词和反讽表达。

from snownlp import SnowNLP def snownlp_score(text): return SnowNLP(clean_text(text)).sentiments # 0~1,越接近1越正面

sentiments返回的是正面概率,0.5 以上算正面。实际测试中,一句「这手机信号真牛,一格都没有」在电商语料里大概率判成中性偏正面,因为模型没学过反讽。类似的迁移问题我以前做一个本地生活项目时也撞过,把微博数据训练的分词和情感权重直接搬到另一个平台,准确率直接掉一半。

所以我的建议是,用 SnowNLP 可以,但必须做两件事。第一,准备一批领域数据,调用它的*train*接口在自己的微博样本上做增量训练;第二,把词典法和 SnowNLP 做一个加权融合,词典法擅长处理明确褒贬词,模型擅长处理复杂句式,两者分数差异超过阈值时以模型为准,差异不大时取均值。融合之后再来评估,比单模型靠谱。

4.3 结果可视化:从整体分布到按天波动

情感分析最终要输出一张图,这张图要能回答两个问题:整体舆情是偏正向还是偏负向?这个倾向在时间线上是怎么变化的?我一般用 pandas 按天聚合情感均值,再用 pyecharts 或 matplotlib 画出折线图,横轴是日期,纵轴是当日平均情感得分。

import pandas as pd df = pd.DataFrame(posts) df["sentiment"] = df["content"].apply(sentiment_score) df["date"] = pd.to_datetime(df["created_at"]).dt.date daily = df.groupby("date")["sentiment"].agg(["mean", "count"]).reset_index() daily = daily[daily["count"] >= 10] # 样本量太少的日期不纳入分析 print(daily.head()) # 绘图:pyecharts 或 matplotlib 画折线图,x轴为date,y轴为mean

这里有一个容易被忽略的参数:daily["count"] >= 10。某天全部微博只有 3 条,这天的情感均值没有任何统计意义,画到图里就是一根尖锐的噪声毛刺,会误导决策。过滤条件可以按自己的数据量调节,一般不要低于 5,否则整条曲线都在跳。图出来后重点看拐点:哪个时间点情感分突然下滑,再看当天主题分布,就能定位到是什么事件引发的负面情绪。

5. 舆情分析避坑清单:爬虫封禁、LDA 翻车与情感误判的典型现场

5.1 采集请求被重定向到登录页

现象:爬虫跑了一会儿,返回的数据从 JSON 变成了 HTML 页面,打开看是登录页,或者接口 302 跳转到passport.weibo.com。原因:Cookie 过期、请求频率超限、或 User-Agent 太单一被识别。解决:先用浏览器重新登录拿到新 Cookie,再检查请求间隔是否低于 1 秒,把sleep调大到 3~5 秒。如果还不行,把采集时间切到凌晨低峰期。这里没有后悔药,Cookie 被封之后只能等冷却,不要试图硬闯,越闯封得越久。

5.2 LDA 出来的主题全是「微博、转发、链接」

现象:print_topics出来每个主题前几个词都是「微博、转发、网页链接、图片」,完全看不出议题。原因:清洗环节漏了平台通用词,filter_extremesno_above参数又没挡住高频词。解决:在停用词表里手工追加「微博、转发、链接、图片、话题、分享、视频」这些词,把no_above从 0.5 调到 0.3,顺便把no_below从 5 调到 10,让词表更聚焦。这个坑如果不解决,后面所有主题标签都是白做的。

5.3 「太好看了」被判成负面

现象:用词典法跑「这个电影太好看了」,打分是 -1,归到负面。原因:分词后是「太 / 好看 / 了」,词表里只有「好看」没有处理「太…了」的句式,强度词被当成独立词,导致加权规则没生效。解决:加一条后验规则,如果「太」后面跟的是正面词且句尾是「了」,把情感极性反转回来;或者把「太好看」直接加进自定义情感词典。更稳妥的方式是同时跑 SnowNLP,看两个分数差异,如果差距超过 0.4,输出置信度低,标记成待人工审核。

5.4 中性分类占比异常高

现象:情感分布里中性占了 60% 以上,正面和负面被稀释到没有分析价值。原因:把大量不表达观点的微博也送进了打分器,比如「转发微博」「今天好累」这类句子本身就没有明确情感词,得分接近 0。解决:在进入情感分析之前加一个观点句过滤,只有命中情感词、程度词、感叹号或表情符号的文本才进入打分,其余归为「无观点」。过滤之后中性占比会明显下降,正面和负面的比例才真实。

5.5 断点续采不是万能的

现象:脚本崩了重跑,跑完发现某几条微博永远采不到。原因:翻页采集是按偏移量走的,如果有新微博发布,偏移量会整体后移,下一页的开头几条会重复,而上一页的末尾几条可能被跳过。解决:采集时额外记录每页最后一条微博的created_atmblogid,翻页时用这个值做游标,代替纯数字页码。代码改动不大,但能保证数据不漏不重,舆情分析最怕漏数,漏掉的可能恰恰是事件的关键节点。

6. 把源码改成自己的项目:验证指标、报警规则与多模态进阶

验证一个舆情分析系统靠不靠谱,不要只看 Loss,要做抽样标注。我一般从库里随机抽 100 条,逐条人工标成正面、负面、中性,再跑一遍情感打分,算准确率。100 条里词典法能到 70% 就算及格,想上 80% 就得靠模型融合。这一步必须做,因为微博文本变化太快,上个月还准的词表这个月就不准了。

进阶方向有三个。第一是加报警规则:把每日情感均值做成滑动窗口,连续两天均分低于阈值且下降超过 10%,就输出一条预警消息,配合钉钉或其他 webhook 推给业务方。第二个方向是多模态情感分析,微博里大量观点藏在图片和表情符号里,一条微博配一个愤怒表情,文字可能是中性,只做文本分析会漏掉信号。第三个方向是主题趋势追踪,把 LDA 输出的主题占比按天做差分,哪个主题声量突然放大,就说明有新事件发生了。

我最开始在主题数 K 上翻过车,拿着困惑度指标选出个 K,跑出来的主题词都不像人话,后来才明白模型指标只是参考,落地要的是业务能解释、能汇报的结果。这个项目里最容易让人挫败的正是这些细节,但也是这些细节把一个课设变成了能用的系统。数据层面的坑,十有八九靠清洗和规则解决,模型只是最后一公里。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询