ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微博舆情分析全流程:爬虫、LDA主题与情感分析实战及避坑指南

微博舆情分析全流程:爬虫、LDA主题与情感分析实战及避坑指南 简介基于微博数据的舆情分析项目源码资料定位为面向计算机、通信、人工智能、自动化等专业学生与从业者的可运行毕业设计/课程设计参考。项目整合微博爬虫、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, pages5): results [] for page in range(1, pages 1): url https://m.weibo.cn/api/container/getIndex params { containerid: f100103type1q{keyword}, page_type: searchall, page: page } try: resp requests.get(url, headersheaders, paramsparams, timeout10) 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它决定了搜的是关键词还是某个用户的主页。100103type1q是移动端关键词搜索的标准拼接方式page_typesearchall表示综合搜索想只看热门微博可以改成searchhot。time.sleep(random.uniform(1, 3))是给每次请求之间加的随机间隔别小看这一行舆情采集不是瞬时任务频率太高大概率触发风控。timeout10设短一点失败就跳过不要无限重试。另外说一句合规问题。微博的公开内容可以用于个人学习和研究但要控制采集频率、不对外传播原始数据、不做商业化用途。舆情项目最忌讳的是把采集器写成了高压请求器被封号只是小事惹上法律风险就得不偿失了。2.2 采集管线三件套限速、去重、断点续采单次搜索跑完只是第一步舆情项目往往要连续采很多天甚至每小时跑一次。这时候就要给采集流程加三个模块限速、去重、断点续采。限速在上一段代码里已经做了雏形去重和断点续采我建议直接用 SQLite轻量、零部署、单文件几百 MB 的数据完全扛得住。import sqlite3 def init_db(db_pathweibo.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_weibo和save_posts串起来外层套一个for循环遍历关键词列表每个关键词之间 sleep 10 到 30 秒这是控制风险最有效的手段。2.3 微博签到数据与地域舆情一种低成本的扩展采集很多舆情需求不只关心「说了什么」还关心「人在哪里说」。微博的签到数据就是一个低成本的切入口。m.weibo.cn 的地点签到接口返回用户地理位置和签到时间字段里带poi_id和location按城市或商圈维度聚合就能画出舆情的空间分布。# 查询某个 POI 地点的签到微博 curl https://m.weibo.cn/api/container/getIndex?containerid230283_${POI_ID}page1 \ -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, encodingutf-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(rhttps?://\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_below5, no_above0.5) bow_corpus [dictionary.doc2bow(doc) for doc in corpus] coherence_values [] perplexity_values [] for k in range(4, 15): lda LdaMulticore( corpusbow_corpus, id2worddictionary, num_topicsk, workers4, passes10, random_state42, alphaauto ) cm CoherenceModel(modellda, textscorpus, dictionarydictionary, coherencec_v) coherence_values.append(cm.get_coherence()) perplexity_values.append(lda.log_perplexity(bow_corpus))filter_extremes(no_below5, no_above0.5)的含义是词频低于 5 的词剔除减少低频拼写错误干扰出现在超过 50% 文档里的词剔除去掉「微博」这类太多见无区分度的词。一致性指标选c_v它比u_mass更接近人工判断跑出来的值趋势是「先升后降」选峰值对应的 K 就是推荐值。注意每次跑 LDA 最好固定random_state否则结果不可复现参数调优时没法对比。跑完 K 值曲线不要直接信峰值要拿峰值附近的两个 K 值都跑一遍人工看主题词。峰值的 K 大约是 1010 个主题里有两个主题词长得差不多就减到 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, encodingutf-8).read().splitlines()) NEG_WORDS set(open(neg_dict.txt, encodingutf-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.txt和neg_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轴为datey轴为mean这里有一个容易被忽略的参数daily[count] 10。某天全部微博只有 3 条这天的情感均值没有任何统计意义画到图里就是一根尖锐的噪声毛刺会误导决策。过滤条件可以按自己的数据量调节一般不要低于 5否则整条曲线都在跳。图出来后重点看拐点哪个时间点情感分突然下滑再看当天主题分布就能定位到是什么事件引发的负面情绪。5. 舆情分析避坑清单爬虫封禁、LDA 翻车与情感误判的典型现场5.1 采集请求被重定向到登录页现象爬虫跑了一会儿返回的数据从 JSON 变成了 HTML 页面打开看是登录页或者接口 302 跳转到passport.weibo.com。原因Cookie 过期、请求频率超限、或 User-Agent 太单一被识别。解决先用浏览器重新登录拿到新 Cookie再检查请求间隔是否低于 1 秒把sleep调大到 35 秒。如果还不行把采集时间切到凌晨低峰期。这里没有后悔药Cookie 被封之后只能等冷却不要试图硬闯越闯封得越久。5.2 LDA 出来的主题全是「微博、转发、链接」现象print_topics出来每个主题前几个词都是「微博、转发、网页链接、图片」完全看不出议题。原因清洗环节漏了平台通用词filter_extremes的no_above参数又没挡住高频词。解决在停用词表里手工追加「微博、转发、链接、图片、话题、分享、视频」这些词把no_above从 0.5 调到 0.3顺便把no_below从 5 调到 10让词表更聚焦。这个坑如果不解决后面所有主题标签都是白做的。5.3 「太好看了」被判成负面现象用词典法跑「这个电影太好看了」打分是 -1归到负面。原因分词后是「太 / 好看 / 了」词表里只有「好看」没有处理「太…了」的句式强度词被当成独立词导致加权规则没生效。解决加一条后验规则如果「太」后面跟的是正面词且句尾是「了」把情感极性反转回来或者把「太好看」直接加进自定义情感词典。更稳妥的方式是同时跑 SnowNLP看两个分数差异如果差距超过 0.4输出置信度低标记成待人工审核。5.4 中性分类占比异常高现象情感分布里中性占了 60% 以上正面和负面被稀释到没有分析价值。原因把大量不表达观点的微博也送进了打分器比如「转发微博」「今天好累」这类句子本身就没有明确情感词得分接近 0。解决在进入情感分析之前加一个观点句过滤只有命中情感词、程度词、感叹号或表情符号的文本才进入打分其余归为「无观点」。过滤之后中性占比会明显下降正面和负面的比例才真实。5.5 断点续采不是万能的现象脚本崩了重跑跑完发现某几条微博永远采不到。原因翻页采集是按偏移量走的如果有新微博发布偏移量会整体后移下一页的开头几条会重复而上一页的末尾几条可能被跳过。解决采集时额外记录每页最后一条微博的created_at或mblogid翻页时用这个值做游标代替纯数字页码。代码改动不大但能保证数据不漏不重舆情分析最怕漏数漏掉的可能恰恰是事件的关键节点。6. 把源码改成自己的项目验证指标、报警规则与多模态进阶验证一个舆情分析系统靠不靠谱不要只看 Loss要做抽样标注。我一般从库里随机抽 100 条逐条人工标成正面、负面、中性再跑一遍情感打分算准确率。100 条里词典法能到 70% 就算及格想上 80% 就得靠模型融合。这一步必须做因为微博文本变化太快上个月还准的词表这个月就不准了。进阶方向有三个。第一是加报警规则把每日情感均值做成滑动窗口连续两天均分低于阈值且下降超过 10%就输出一条预警消息配合钉钉或其他 webhook 推给业务方。第二个方向是多模态情感分析微博里大量观点藏在图片和表情符号里一条微博配一个愤怒表情文字可能是中性只做文本分析会漏掉信号。第三个方向是主题趋势追踪把 LDA 输出的主题占比按天做差分哪个主题声量突然放大就说明有新事件发生了。我最开始在主题数 K 上翻过车拿着困惑度指标选出个 K跑出来的主题词都不像人话后来才明白模型指标只是参考落地要的是业务能解释、能汇报的结果。这个项目里最容易让人挫败的正是这些细节但也是这些细节把一个课设变成了能用的系统。数据层面的坑十有八九靠清洗和规则解决模型只是最后一公里。希望帮到你。本文还有配套的精品资源点击获取
返回列表