简介:基于微博数据的舆情分析项目,整合微博爬虫、LDA主题分析与情感分析全流程,是一份面向计算机、通信、人工智能等专业学生及从业者的完整源码资料。项目源于个人高分毕业设计(答辩98分),代码均已调试运行,既适合新手循序渐进学习,也便于进阶者二次改造。压缩包共39个文件、16.16MB,以23个Python脚本为主体,覆盖数据采集、文本预处理、热度计算、主题建模、情感打分等环节;另有7个Markdown说明文档和7个文本语料(含正负向语料、停用词表),以及Word2Vec模型和结果表格,目录按heat、topic similarity、emotional analysis、weibo-crawler、LDA等模块划分,结构清晰。目前已有211人学习下载,适合作为课程设计、大作业或毕业设计的参考模板。从中可掌握微博爬虫反爬与评论采集技巧、LDA主题数调优方法、基于API/SDK的情感分析实现,以及热度与情感指标的可视化呈现思路。
1. 基于微博数据的舆情分析:爬虫、LDA主题与情感分析,这套源码能跑通到什么程度
做网络舆情分析绕不开微博:文本短、带表情、带转发链、情绪浓度高,是最典型的社交语料,也是最难爬、最脏、最难解读的数据。标题里这套「基于微博数据的舆情分析项目」核心资产是三件事——微博爬虫拿数据、LDA主题分析看大家在聊什么、情感分析看大家是夸还是骂;所谓源码+资料齐全,一般就是带可跑代码、停用词表、标注样本和说明文档的完整工程包。适合两类人:课程设计或毕业设计需要完整落地链路的在校生,以及小团队想快速搭一个舆情监控 demo 的从业者。这篇我按数据采集、主题建模、情感判定、工程交付的顺序,把能直接用、能踩得住坑的方案一次讲完。
2. 微博爬虫:从 Cookie 维护到去重入库,先把语料稳稳拿下来
2.1 为什么我选 m.weibo.cn 移动端接口而不是 PC 端
微博爬虫选哪个端,直接决定能不能稳定跑完。PC 端 weibo.com 的接口现在基本都要登录态,而且页面结构和接口签名改得频繁,爬虫写出来三个月内就可能报废。我一般首选 m.weibo.cn 的移动端接口,它的网络请求是普通的 JSON 接口,不涉及复杂签名,UA 伪装成 iPhone Safari 就能过基础风控,是目前社群公开源码里用得最多、也最省力的一条路。
另一个原因是移动端接口在弱网环境下做了大量字段精简,返回的数据结构相对固定。/api/container/getIndex这个接口用containerid参数区分搜索、热门、主页三个场景,搜索场景的 containerid 是100103type=1&q=关键词的拼接形式,翻页只要改page参数。这套结构在近几年的项目里基本稳定,所以我在这类舆情项目的爬虫部分,习惯性优先写移动端。
选移动端还有一个现实考量:反爬策略是跟着流量走的。PC 端页面承载更高的营销流量,风控规则更敏感;移动端反而长期保持一套「按频率封、放过低频」策略。只要请求间隔控制在 2 秒以上、单关键词单次抓取不超过 10 页,大多数情况下不会触发滑块验证。
2.2 按关键词抓取:爬虫最小实现与字段清洗
先写一个按关键词搜索微博的最小实现,这一步跑通,后面换热门、换用户主页都只是改参数:
import requests import time from urllib.parse import quote def fetch_weibo_search(keyword, page=1, cookie=""): # m.weibo.cn 搜索接口,containerid 里 q 参数是关键词,page 控制翻页 containerid = f"100103type=1&q={quote(keyword)}" url = "https://m.weibo.cn/api/container/getIndex" params = {"containerid": containerid, "page": page} 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", "Cookie": cookie, "Referer": f"https://m.weibo.cn/search?containerid=100103type%3D1%26q={quote(keyword)}", } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() return data.get("data", {}).get("cards", []) def parse_cards(cards): # card_type=9 是微博正文卡片,其他类型(广告、推荐用户)直接跳过 items = [] for card in cards: if card.get("card_type") != 9: continue mblog = card.get("mblog", {}) if not mblog: continue items.append({ "mid": mblog.get("id"), "text": mblog.get("text"), "user": mblog.get("user", {}).get("screen_name", ""), "reposts": mblog.get("reposts_count", 0), "comments": mblog.get("comments_count", 0), "likes": mblog.get("attitudes_count", 0), "pub_time": mblog.get("created_at"), }) return items逻辑说明:quote(keyword)对中文关键词做 URL 编码,避免请求参数乱码;card_type=9是接口里微博正文卡片的标准标识,过滤掉推荐位、广告位和用户卡片,避免脏数据混进分析语料。mid是微博唯一 ID,后续去重和增量全靠它。
参数说明:timeout建议固定 10 秒,防止某个慢请求拖死整轮抓取;UA 里的 iPhone 型号可以随便换,但必须保留 Mobile 标识,这是走移动端接口的身份前提。Cookie参数是维持登录态的关键,微博搜索接口不登录也能返回部分结果,但连续翻页到第 4、5 页后大概率被拦,所以正式跑批前一定要填有效的登录 Cookie。
拿到接口返回后,text字段是带 HTML 标签的富文本,包含a标签、br、缩略图链接等噪声。清洗时用正则去掉标签并保留正文即可,这一步放在预处理脚本里统一做,不在爬虫里做——爬虫只负责把原始字段落库,清洗交给分析阶段,这是舆情项目的通用分层。
2.3 增量去重与分页续抓:防止数据重复和断点丢失
舆情分析最怕数据重复,LDA 也好、情感统计也好,重复样本会把权重成倍抬高。去重方案我直接用 SQLite 建表,给mid加唯一索引,插入时用INSERT OR IGNORE,幂等又简单:
import sqlite3 def save_new_items(items, db_conn): new_count = 0 for it in items: # mid 是唯一键,重复插入会被 IGNORE 吞掉,天然完成增量去重 cur = db_conn.execute( "INSERT OR IGNORE INTO weibo_data " "(mid, text, user_name, reposts, comments, likes, pub_time) " "VALUES (?, ?, ?, ?, ?, ?, ?)", (it["mid"], it["text"], it["user"], it["reposts"], it["comments"], it["likes"], it["pub_time"]), ) new_count += cur.rowcount db_conn.commit() return new_count逻辑说明:mid一旦重复,SQLite 会因为唯一约束直接跳过该条,rowcount只统计真实插入的行数,所以返回值就是「这一轮新增了多少条」。这个数字很有用,可以写进日志,监控爬虫是不是空跑。
参数说明:建表时给mid加UNIQUE约束是前提,否则INSERT OR IGNORE不会生效。pub_time不要用字符串存,建议统一转成YYYY-MM-DD HH:MM:SS格式,方便后面按小时、按天做舆情趋势分析。
分页续抓是另一个容易翻车的点。微博接口的翻页不是一直翻到没数据,而是最多返回 20 页左右,之后即使page再大也返回空。所以抓取策略一般是:每轮每个关键词抓 5-10 页,通过max_id或时间窗口定位上次抓取位置,而不是靠 page 死磕到底。小规模舆情 demo 里最实用的做法是每轮抓完记住最新一条mid对应的pub_time,下一轮只抓这个时间点之后的微博。配合INSERT OR IGNORE的去重能力,即便时间窗口重叠也无所谓,重复数据会被静默过滤,不会污染语料。
注意:微博抓取只建议采集公开可见的数据,控制请求频率,数据仅用于学术分析或内部舆情研判。大批量、高频采集会触发账号风控,这个红线要心里有数。
3. LDA 主题分析:分词、K 值选择与主题解读,让舆情可读
3.1 微博语料预处理:jieba 分词、自定义词典与停用词表
微博文本进入 LDA 前要先过三关:HTML 标签、噪声词、分词质量。文本清洗我放在preprocess.py里统一处理,核心逻辑是去标签、去话题符号、用 jieba 分词后用停用词表过滤:
import re import jieba def clean_text(raw): text = re.sub(r"<[^>]+>", "", raw) # 去微博正文里的 HTML 标签 text = re.sub(r"#.*?#", " ", text) # 话题标签先去掉,后续可按需单独统计 text = re.sub(r"https?://\S+", " ", text) # 去链接 text = re.sub(r"@\S+", " ", text) # 去 @ 提及 words = jieba.lcut(text) stopwords = load_stopwords() kept = [] for w in words: w = w.strip() if w and w not in stopwords and len(w) > 1 and not w.isdigit(): kept.append(w) return kept逻辑说明:清洗顺序有讲究,先去掉链接和 @ 提及,再分词,最后过停用词表。len(w) > 1会把单字词过滤掉,微博文本里单字大多是语气词或错误分词,留着只会变成主题噪声。load_stopwords()从data/stopwords.txt读入停用词表,这个表要重点加入微博生态词:比如「转发」「微博」「网页链接」「收起」这类平台推送词,不干掉它们,LDA 的主题词会一直被「网页链接」这类词占据,这是新手最容易踩的坑。
还有一个提升分词质量的关键步骤:自定义词典。微博里大量网络热词、品牌词、人名是 jieba 默认词库里没有的,比如项目涉及的产品名、明星名、缩写词。分词不对,后面所有分析都会跟着偏。常见做法是在项目里维护一份userdict.txt,格式是「词 词频 词性」:
# data/userdict.txt 冰墩墩 10 nr 谷爱凌 10 nr 双减政策 10 n加载方式一行代码:jieba.load_userdict("data/userdict.txt")。词频给 10 就够了,表示这是一个独立词,不必让 jieba 自己猜着切。这个文件也是很多「资料齐全」项目会附带的资源,拿到手先看它内容是否匹配你的分析领域——如果源码项目自带的词典是电商领域的,直接套用到微博舆情上,分词效果会大打折扣。
3.2 词袋与 LDA 模型:gensim 参数这样调
预处理完的文本列表,转成 gensim 的Dictionary和Corpus,再训练 LDA。这里最容易犯的错是直接拿字符串列表丢进模型,gensim 要求的是「每个文档分词后的词列表」嵌套结构,不是原始文本:
from gensim.corpora import Dictionary from gensim.models import LdaModel # all_seg_list 是预处理后的文本集合,形如 [["冰墩墩", "可爱"], ["冬奥", "比赛", "精彩"]] dictionary = Dictionary(all_seg_list) # 去掉低于 5 条文档出现、或超过 80% 文档都出现的词 dictionary.filter_extremes(no_below=5, no_above=0.8) corpus = [dictionary.doc2bow(doc) for doc in all_seg_list] lda = LdaModel( corpus=corpus, num_topics=8, id2word=dictionary, alpha="auto", eta="auto", passes=20, random_state=42, ) for topic_id, topic_words in lda.print_topics(num_words=10): print(topic_id, topic_words)参数说明:num_topics=8是先随便给的主题数,后面要用困惑度和一致性去校正;alpha="auto"和eta="auto"让模型自动学习文档-主题分布和主题-词分布的稀疏度,对小语料效果比手动固定值好;passes=20是遍历语料的轮数,微博语料几千条规模 20 轮够用,太大会过拟合;random_state=42一定要固定,否则每次跑出来的主题词都不一样,后面做实验对比就是用户说的「黑匣子」,没法复现。
filter_extremes的参数要结合语料规模调:no_below=5表示词至少要出现在 5 条博文里,低于这个数的低频词会被剔除;no_above=0.8表示出现在 80% 以上文档的词会被当成主题间泛化词剔除。微博语料里「转发」「视频」这类词就算不在停用词表里,也会被这个阈值干掉。
3.3 主题数 K 怎么定:困惑度、一致性一起看
主题数 K 是 LDA 新手的第一个玄学决策点。K 太小,话题糊成一大锅;K 太大,主题高度重复、没有区分度。我一般用「困惑度 + 一致性」双指标选 K:
from gensim.models import CoherenceModel # 候选 K 从 4 到 16,分别训练并对比 def evaluate_k(corpus, dictionary, texts, min_k=4, max_k=16): results = [] for k in range(min_k, max_k + 1, 2): model = LdaModel( corpus=corpus, num_topics=k, id2word=dictionary, alpha="auto", eta="auto", passes=15, random_state=42, ) perplexity = model.log_perplexity(corpus) cm = CoherenceModel(model=model, texts=texts, dictionary=dictionary, coherence="c_v") coherence = cm.get_coherence() results.append({"k": k, "perplexity": perplexity, "coherence": coherence}) print(f"K={k}, perplexity={perplexity:.2f}, coherence={coherence:.3f}") return results逻辑说明:log_perplexity越低表示模型对语料的拟合越好,但它会随 K 增大单调下降,单独看没有意义;c_v一致性衡量主题内词之间的语义关联度,越高越好,但 K 太大时一致性也会虚高。正确的选择逻辑是:先看一致性曲线是否在某个 K 出现明显拐点或峰值,再回看该 K 下各主题词是否可读、有没有两两重叠严重。我见过很多人只盯着困惑度选了个很大 K,结果主题词全是零散专有名词,这就是指标单看的教训。
实际项目里我在一致性曲线找了峰值后,还会做一次「疲惫验证」:把主题词打印出来,让没参与项目的同事读一遍,看能不能猜出每个主题在讲什么。能猜出来就说明 K 选对了,猜不出来,指标再好看也要调。
3.4 主题解读落地:主题词表、权重与时间趋势
主题模型跑完不是终点,舆情分析要回答「人在聊什么、热度怎么变」。我习惯把每个主题解释成一个短标签,比如主题 0 的词是「涨价」「货源」「发货」,就标成「供应链问题」;然后按天统计每个主题的文档数占比,画出时序曲线,看话题什么时候发酵、什么时候消退。
这个统计环节有个细节容易忽略:用 LDA 给每篇文档推断主题分布时,get_document_topics的结果是(topic_id, prob)列表,只保留概率最大的那个主题作为该文档的归属:
def assign_topic(lda_model, corpus, doc_index): topics = lda_model.get_document_topics(corpus[doc_index]) # 按概率降序排,最大概率的主题作为该文档的归属 best_topic = max(topics, key=lambda x: x[1]) return best_topic[0]参数说明:minimum_probability默认 0.01,如果一条文档对所有主题的概率都低于这个值,返回结果里就没有该文档的主题,所以做归属统计时不能只拿返回值直接用,要先判空。一般舆情项目里我对「没进入任何主题」的文档单独聚成一组不放进来,避免拉低主题占比的准确性。
时间趋势的维度,我一般是pub_time按天截断后与主题归属做交叉统计。这一步产出两个核心可视化:主题随时间的热度堆叠图,和每个主题下的代表性博文列表。代表性博文可以选主题概率最高那条,也可以选转发量最高那条,两者表达的意思不同——前者是主题核心表达,后者是出圈爆点。源码项目里如果有 pyLDAvis 的.html可视化文件,那是给你交互式看主题词分布用的,但正式报告里我还是建议放自己画的时序图,决策者看趋势比看气泡图直观得多。
4. 情感分析:SnowNLP 适配微博语料,从 0.5 附近拉到可信区间
4.1 为什么 SnowNLP 开箱即用,却又经常翻车
情感分析在这类舆情项目里最常见的落地方式是 SnowNLP,原因很直接:不需要标注数据、几行代码出分数、库是纯本地离线推理。SnowNLP("今天的发布会真精彩").sentiments返回一个 0 到 1 的概率值,大于 0.6 多数人判为正向,小于 0.4 判为负向,0.4 到 0.6 判为中性。
但翻车现场也很经典:SnowNLP 默认模型是用商品评论语料训练的,迁移到微博这种短文本、高反讽、带表情的语料上,情感分大量集中在 0.5 附近,俗称「得分暧昧」。具体表现是,一条「这服务质量真是绝了」的吐槽,模型给 0.6 正分;「终于买到心心念念的演唱会票了」这种明显正向的表达,反而因为词库不认识「心心念念」给 0.45 分。这时候不要急着推翻模型,要做的是两件事:用它默认模型做人机对比,找到偏差分布;再做领域微调。
另一个问题是 SnowNLP 对否定句和反问句的处理很弱。「不好看」「不太好看」「不是不好看」这三句的语义梯度,默认模型的打分基本区分不出来,因为它本质是朴素贝叶斯对词袋概率的叠加,没有语法层面的反转逻辑。这就是为什么情感分析模块在真实舆情项目里要有「模型 + 词典修正」的复合结构。
4.2 用标注样本微调 SnowNLP 情感模型
解决领域偏差最直接的办法是给 SnowNLP 喂本领域的标注语料重新训练。SnowNLP 内置的sentiment模块支持训练和保存,你只要准备两个文本文件,每行一条语料:pos.txt放正向微博,neg.txt放负向微博:
from snownlp import sentiment # 每行一条微博短文本,正负样本量建议至少各 500 条 sentiment.train("data/pos.txt", "data/neg.txt") sentiment.save("models/sentiment.marshal")逻辑说明:sentiment.train会把两个文件读入,训练一个朴素贝叶斯分类器;save把训练结果存成序列化文件。训练完要让 SnowNLP 在运行时加载这份新模型,加载方式是全局替换,位置要在SnowNLP类被调用之前:
from snownlp import sentiment, SnowNLP sentiment.load("models/sentiment.marshal") # 替换内置模型 test_sents = [ "这次的活动办得真不错", "物流等了五天还没到,服了", "不是不好看,是根本没法看", ] for s in test_sents: score = SnowNLP(s).sentiments print(s, round(score, 3))参数说明:正负样本量直接决定微调效果。我实际经验是各 300 条能明显改善「得分暧昧」问题,各 1000 条基本达到稳定。样本不要找太大段的文章,每条控制在 50 字以内的微博原句,和预测时的输入分布保持一致,这是最容易忽略的分布偏差问题——用长评训练、拿短微博预测,效果一定打折。
微调后必须做一次回归测试。把原来预判是正向和负向的测试集各选 100 条重新打分,看均值是否拉开:正向样本均值应该在 0.7 以上,负向样本均值应该在 0.3 以下。如果正向均值还在 0.55 附近,说明训练语料里混入了中性表达,需要重新清理标注文件。
4.3 词典法兜底:正向词、负向词与否定逻辑
微调后的模型能解决大部分场景,但微博里高频出现的否定表达仍然会击穿它,所以我在每个舆情项目里都保留一个词典法打分器作为兜底,专门处理「不 + 正向词」这类组合:
pos_words = set(open("data/pos_words.txt", encoding="utf-8").read().split()) neg_words = set(open("data/neg_words.txt", encoding="utf-8").read().split()) deny_words = {"不", "没", "无", "非", "莫", "勿", "别", "未"} def dictionary_score(text): from jieba import lcut words = [w for w in lcut(text) if w.strip()] score = 0 deny_scope = 0 # 否定词影响后面 2 个词 for w in words: if w in deny_words: deny_scope = 2 elif deny_scope > 0: deny_scope -= 1 if w in pos_words: score -= 1 elif w in neg_words: score += 1 else: if w in pos_words: score += 1 elif w in neg_words: score -= 1 return score逻辑说明:deny_scope模拟否定作用域。遇到「不」之后 2 个词内的正负方向都反转,比如「不好看」中「好」是正向词但被「不」反转,得分从 +1 变 -1。这个逻辑很糙,但对短文本微博来说足够用,也比单纯词袋计数靠谱得多。词表资源一般项目资料里会带,如果没有,可以从网上的大连理工情感词汇本体库提取,或者用知网情感词典。
词典法和 SnowNLP 怎么协作?我的策略是分成两层:先用 SnowNLP 打分,分不出明显正负(得分在 0.4-0.6 之间)的文本,再交给词典法二次判定;如果词典法给出的正负词命中数为 0,才判定为中性。这样把「模型擅长识别复杂情感、词典擅长捕捉反转」互补起来,准确率比单跑 SnowNLP 高出不少。
4.4 向多模态情感分析延伸:表情、图片与转发语境
文本之外还有大量信号:微博自带的表情符号、图片、视频封面、点赞数、转发评论比。近几年多模态情感分析思路开始进入舆情工程,常见做法是给每条微博补一个「视觉情感」标签:用图片的情感极性去修正文本的极性。比如一条文案是反讽的负面吐槽,配图却是微笑表情,模型若只读文本容易误判,而图片特征能帮助识别这是反讽。
不过小规模源码项目直接上多模态大模型不现实,我一般分两步落地这个方向:第一步是把微博文本里的 emoji 做个简单映射,笑脸类属正向、哭脸类属负向,作为情感分的修正项;第二步才是对真正带图的微博抽帧或提取 CLIP embedding,做粗粒度正负分类。表情符号映射在工程上几乎零成本,效果提升却很明显——微博用户习惯用表情对冲文字表达的辛辣,比如「这个人真的太好了[狗头]」,读懂狗头是反讽就能避免一次严重误判。
多模态情感分析真正用起来是在事件级舆情上。单条微博的多模态融合容易过拟合,但把某个事件下所有文本分、图片分、互动数据一起聚合,就能得到更稳定的情感曲线。
5. 舆情分析项目避坑手册:爬虫风控、编码乱码与情感分漂移的排查清单
5.1 爬虫:Cookie 失效与验证码拦截的排查路径
现象:抓取前 5 页正常,第 6 页开始返回的cards为空,或者响应里的msg变成「系统繁忙」。
原因:连续翻页触发频率风控,或 Cookie 里的SUB字段过期。移动端接口对单 session 的请求频率限制大约在每分钟 30 次以内,超过后即使换关键词也会被临时拦截。
解决:请求间加time.sleep(random.uniform(2, 4));每轮抓取控制在 5-6 页内,下一轮换个时间窗口再抓;Cookie 失效就重新登录网页版微博,从开发者工具里拿新的SUB值,更新到config.py里。如果整个 IP 被限流,等 5-10 分钟会自动恢复,千万不要在同一 IP 下强行并发重试。
现象:返回的数据里text字段出现乱码或变成\u开头的转义序列。
原因:接口对部分 emoji 和生僻字做的是 Unicode 直传,JSON 解析后没做 unicode 转义还原。
解决:requests响应对象统一指定resp.json()解析即可,它自带 unicode 解码,不需要手写unicode_escape。真正容易乱码的地方是 SQLite 存储后再读出来,所以建库连接要加charset参数:sqlite3.connect("weibo.db")后用PRAGMA encoding = "UTF-8"确认一次。
现象:入库后总条数和接口返回不一致,莫名少了很多。
原因:card_type过滤条件写错。搜索接口里除了微博正文,还有「推荐阅读」「热门话题」「搜索发现」等卡片,如果过滤条件用的是card.get("card_type") == 9,而某些卡片 type 字段是字符串或缺失,就整卡被跳过。
解决:先打印所有不重复的card_type值,确认目标卡片的真实类型,再写过滤条件。这个坑我自己也踩过,后来养成了「第一轮抓取先 dump 原始 JSON,看结构再写解析」的习惯。
5.2 预处理与 LDA:乱码、分词错误与 K 值陷阱
现象:LDA 打印出来的主题词里,反复出现「微博」「转发」「网页链接」这类与舆情内容无关的词。
原因:停用词表没有覆盖平台专有词,filter_extremes的no_above阈值设得过高,导致这些高频泛化词进入主题。
解决:先补充停用词表,把「微博」「转发」「链接」「图片」「视频」加进去;再把no_above从默认的 0.8 调低到 0.5-0.6,让出现在大多数文档里的词直接被词典过滤。过滤后再训练,主题词会干净很多。
现象:不同时间跑同一个 LDA 脚本,主题词完全不同,说好的「可复现」消失了。
原因:LdaModel默认random_state为 None,每次训练初始化不同;或者语料顺序变了,影响了迭代收敛路径。
解决:固定random_state=42,同时固定语料顺序,预处理结果落盘成 Pickle 文件,后续训练全从这份文件读,不重新跑分词。这样即使过了两周重新执行,也能还原同一组主题。
现象:困惑度还在下降但主题已经糊成一团,主题数 K 总是选不对。
原因:只看困惑度选 K。困惑度随 K 单调下降是模型特性,不代表 K 越大越好,主题之间高度重叠时一致性指标会暴露出来。
解决:按我在 3.3 的方式跑 K=4 到 16 的一致性对比,选一致性曲线里的局部峰值或拐点,再人工读主题词验证可解释性。如果一致性峰值对应的主题词还是不可读,优先回看预处理环节,八成是停用词或词典没调对。
5.3 情感分析:默认模型偏差与标注口径不一致
现象:SnowNLP 对明显负面的吐槽给出 0.6+ 的正分,而且集中发生在「反讽」「言不由衷」的句子。
原因:默认模型基于电商评论训练,语气直白、正负分明;微博反讽式表达考验的是语用理解,词袋模型天然不擅长。
解决:按领域微调,同时引入 4.3 的词典法作为修正层。反讽类文本不要试图在朴素贝叶斯上解决,那是深度学习的事,词典法补一层「正向词 + 狗头表情」的反转规则,效果在工程上更可控。
现象:微调后情感分整体偏高,所有微博都被判成正向。
原因:训练语料里正负样本的标注口径不一致。比如把「还行」「一般般」标成正向样本,贝叶斯学习到的概率分布就会被这种弱正向文本污染。
解决:标注前定义清楚三条硬规则:积极词汇明确是正向,消极词汇明确是负向,「中性描述」不进入训练集;正负样本各 500 条起步,保持数量均衡;训练完用一小批验证集看均值,正向大于 0.7、负向小于 0.3,达不到就查标注文件。
现象:有人工标注好的舆情样本,但微调后效果反而变差。
原因:直接把人工标注的「正/负/中性」三分类标签丢给 SnowNLP 训练。SnowNLP 的情感训练只接受正负两类文件,中性样本混进去会被强行归类,反而破坏边界。
解决:中性样本单独存放,不进训练集。后续做效果评估时,再用中性样本检验区分度,而不是让模型学会「输出 0.5」。
6. 让舆情项目可交付:工程目录、定时增量与人工抽检闭环
6.1 工程目录怎么组织,README 和 requirements 先对齐
拿到任何舆情源码项目,第一步不是读代码,而是确认工程结构是否完整。一个能长期跑的舆情项目,目录至少是这样:
weibo_analysis/ ├── config.py # 关键词、Cookie、数据库路径 ├── crawler/ │ ├── fetcher.py # 移动端搜索抓取 │ └── storage.py # 去重入库 ├── analysis/ │ ├── preprocess.py # 清洗、分词、停用词 │ ├── lda_model.py # 主题模型训练与评估 │ └── sentiment.py # 情感打分与词典修正 ├── data/ # 停用词表、自定义词典、标注语料 ├── models/ # 训练好的情感模型文件 ├── output/ # 报告、图表、pyLDAvis 文件 └── run_pipeline.py # 一键跑全流程我一般拿到项目先看README和requirements.txt,确认依赖版本。SnowNLP、gensim、jieba 这三个库的版本兼容性有问题时,最典型的翻车现场是 gensim 新版本 API 变动导致老代码报错,用pip install -r requirements.txt时不要升级到最新版,锁定项目声明版本最省事。
6.2 让 Pipeline 定时跑起来:用脚本驱动而非 Jupyter
舆情分析的价值在持续跟踪,一次性跑完的舆情没有决策意义。run_pipeline.py把爬取、预处理、LDA、情感打分、输出报告串成一条命令,固定random_state保证可复现:
if __name__ == "__main__": cfg = load_config() cards = fetch_weibo_search(cfg.keyword, page=1, cookie=cfg.cookie) new_count = save_new_items(parse_cards(cards), db_conn) if new_count == 0: print("no new data, skip analysis") exit(0) docs = load_new_docs_from_db(new_count) seg_list = [clean_text(d["text"]) for d in docs] lda_model = train_lda(seg_list, num_topics=cfg.num_topics) for doc in docs: doc["senti"] = round(float(SnowNLP(doc["text"]).sentiments), 3) write_daily_report(docs, lda_model, output_dir=cfg.output_dir)调度的常见做法是 crontab,比如每天凌晨 2 点跑一次:30 2 * * * cd /home/weibo_analysis && /usr/bin/python3 run_pipeline.py >> logs/pipeline.log 2>&1。这里有个经验:exit(0)的分支不要省,舆情项目不是天天有新数据,没有增量就跳过整个分析流程,能省下大量无意义的计算时间。
6.3 结果验证:人工抽检、一致性指标与置信区间
最后一步是验证,也是很多源码项目缺的环节。我习惯在情感分析结果里随机抽 100 条,人工标注一次,和模型打分对比:如果准确率低于 70%,就把中性阈值区间从 0.4-0.6 扩大到 0.35-0.65,把更多模糊样本拦在中性里,避免强行分类带来的误差。主题分析则验证主题词的可解释性,抽三个主题让同事描述「这个主题是什么」,描述一致就说明主题切分成功。
这个项目做下来,最大的教训是「舆情分析的价值不在于模型多高级,而在于数据干净、流程可复现、结论能解释」。爬虫和预处理往往比模型参数更值得花时间,情感分也不是越接近 0 或 1 就是越准,中性样本保留下来本身就是结论。希望这篇踩坑梳理能帮你少走一段弯路,把这个方向做成一个可以长期跑、敢对决策负责的舆情看板。
本文还有配套的精品资源,点击获取