ARTICLE DETAIL

资讯详情

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

Python微博舆情爬虫与情感分析可视化系统:从数据采集到看板落地

Python微博舆情爬虫与情感分析可视化系统:从数据采集到看板落地 简介基于Python的微博舆情数据爬取与情感分析可视化系统面向毕业设计、课程实践及爬虫入门学习者。系统覆盖数据采集、情感倾向分析与可视化展示完整链路包含爬虫模块、自然语言处理单元与交互式图形界面代码分模块组织并配有详细注释。资源包共175个文件约3.85MB以19个py脚本为程序主体配合14个html及css/js文件构成前端界面csv/sql存放采集结果jpg/png记录界面效果另有pyc、zbak等辅助文件结构清晰便于按模块阅读。数据可视化组件支持情感分布图谱、话题演化趋势及热点传播路径展示部署过程已标准化可快速完成环境配置与系统初始化。已有119人浏览学习适合作为计算机专业毕业设计参考或文本挖掘综合实践案例从爬虫到分析再到界面呈现各环节均有对应实现便于对照源码掌握完整项目思路。1. 一条热搜到一张图这套舆情分析系统到底解决什么问题基于Python的微博舆情数据爬取与情感分析可视化系统概念不复杂真正跑上半年还能持续输出结论的却不多。多数人第一次做舆情项目卡住的往往就是三件事微博数据能不能稳定拿回来、情感判断到底可不可信、图表画完能不能讲清楚发生了什么。这篇按一条完整落地路径展开先解决数据获取再解决情绪判断最后把结果变成一块能说明问题的看板。适合做品牌口碑监测、事件趋势研究或舆情研判参考的人不求量大面全但求可控、可改、可复现。下面所有做法都以最小可运行系统为前提不引入重型框架。数据量在几万条级别时这套方案的维护成本低到可以忽略。如果你正在被“爬虫写好了但情感分析不准”之类的问题卡住直接照着后面章节逐段排查就好。2. 技术栈选型先想清楚这三个模块再写第一行代码2.1 数据源选择官方开放平台不是默认答案m.weibo.cn 接口成本最低写舆情爬虫第一反应是申请微博官方开放平台接口。实际做过一轮就会发现搜索类接口的申请审核周期长权限分批开放等着审批下来一个热点事件已经过了发酵期。更现实的起点是微博移动端网页接口也就是 m.weibo.cn。它在浏览器里访问搜索页就可以抓包看到返回的 JSON 结构清晰没有 OAuth 签名流程维护一个登录后的 Cookie 就能持续获取搜索结果。常见的数据采集路径有三种我在实际项目里做过对比方案获取难度数据完整度适合场景官方开放平台 API高需认证审核高字段规范长期正式项目预算充足m.weibo.cn 网页接口低抓包即可中含正文与互动数个人研究与原型验证第三方舆情数据服务收费依赖供应商报价不等不差钱、急于出数个人和小团队先用 m.weibo.cn 网页接口跑通全流程数据量大了、需求明确了再考虑官方 API 或第三方服务。选择它的理由不止是门槛低还因为响应体是标准 JSON字段直接对应微博正文、发布时间、转发评论点赞数解析成本极低。搜索接口的关键参数是 containerid常见格式是100103type1q关键词。type1 综合搜索type2 正文搜索type3 用户搜索。新手最容易踩的坑是直接在 URL 里拼中文请求经常返回乱码或空结果。正确做法是把参数交给 requests 的 params它会按 URL 编码规则自动处理中文。2.2 情感分析选型先用 SnowNLP 跑基线不要一上来就微调大模型情感分析是这套系统的核心选型时容易走极端。一种只用规则词典遇到反讽和上下文转折直接失效另一种直接上 BERT 微调结果标注语料没有、GPU 也没有模型根本训不起来。折中的、也是大多数个人项目最合理的起点是 SnowNLP。SnowNLP 内置的情感分析基于朴素贝叶斯训练语料来自电商购物评论它把一个句子的情感倾向映射到 0 到 1。优势是离线可用、调用简单pip 安装后两行代码就能打分。缺陷也很明确微博文风是短句、反讽、表情和网络新词混杂直接用电商语料训练的模型经常把“哈哈哈我裂开了”判成低落把“这是人干的事”判成正向。所以我的做法是第一版用 SnowNLP 跑基线同时留出自定义词典和重训练接口等标注样本攒到一定量再提升。SnowNLP 自带重训练入口准备好正向和负向两个纯文本语料文件逐句一行调用训练接口保存模型即可from snownlp import sentiment # neg.txt 和 pos.txt 是逐句一行的标注语料 sentiment.train(neg.txt, pos.txt) sentiment.save(sentiment.marshal)逻辑说明train 读取两个语料文件重新估算朴素贝叶斯的先验概率与条件概率save 把训练结果写成本地 marshal 文件。之后 SnowNLP 构建时会优先加载新模型。参数要点是语料质量远比数量重要500 条准确的微博情绪标注效果往往好过 5000 条来自电商评论的脏数据。2.3 量化与调度Pyecharts 负责出图Flask 负责串联APScheduler 负责定时可视化用 Pyecharts 是最短路径它是 ECharts 的 Python 封装调用简单生成的 HTML 图表可离线打开不需要部署 Node 服务。相比直接写前端Pyecharts 把配置项封装成 Python 对象适合不擅长前端的分析人员。调度这一步容易被忽略。舆情采集要做成定时任务每天固定几个时间点跑一次否则数据只有事件发生当时那批没法看趋势。常见做法是 Linux 上配 crontab或者在 Python 进程里内置 APScheduler。APScheduler 的好处是任务状态、下次执行时间都在代码里排错直观from apscheduler.schedulers.blocking import BlockingScheduler from collector import run_all sched BlockingScheduler() # 每30分钟执行一次全量采集错开整点避免服务端压力集中 sched.add_job(run_all, interval, minutes30, idopinion_job) sched.start()逻辑说明BlockingScheduler 会占住当前进程适合单独跑采集服务如果同进程还要提供 Flask 页面要改用 BackgroundScheduler。间隔频率不建议低于 5 分钟微博搜索接口不是为高频轮询设计的。这三个决定做完系统骨架就定了网页接口采集、SnowNLP 打分、Pyecharts 可视化、Flask 搭 Web 入口。下一章开始写能直接跑的代码。3. 微博舆情数据爬取实战从 m.weibo.cn 搜索接口到 SQLite 落库3.1 用 requests 打通搜索接口登录 Cookie 与 containerid 参数真正动手写爬虫时卡点往往不是请求代码而是 Cookie。m.weibo.cn 的搜索接口未登录状态能返回少量数据翻几页就会被要求登录。第一步先把浏览器里的 Cookie 复制出来在浏览器登录微博打开开发者工具的网络面板找到 m.weibo.cn 的请求头把整段 Cookie 复制到代码里。import requests import time import random SEARCH_URL https://m.weibo.cn/api/container/getIndex HEADERS { User-Agent: (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36), Cookie: 你的登录Cookie, # 用浏览器登录后复制过期需手动更新 Referer: https://m.weibo.cn/search?containerid100103type%3D1%26q%3D新能源汽车, } def fetch_page(keyword, page1): params { containerid: f100103type1q{keyword}, page_type: searchall, page: page, } resp requests.get(SEARCH_URL, paramsparams, headersHEADERS, timeout10) data resp.json() if data.get(ok) ! 1: print(返回异常, data.get(msg) or data.get(ok)) return [] cards data.get(data, {}).get(cards, []) return cards逻辑说明requests.get 传入 params 后中文 keyword 会自动做 URL 编码不需要手动 quote。返回的 JSON 里最外层 ok 是状态码ok1 才是成功data.cards 是微博卡片列表。很多教程直接拿 resp.text 当 JSON 解析一旦遇到登录失效返回 HTML 或错误页json.loads 直接抛异常用 resp.json() 配合 ok 判断可以提前发现问题。参数说明page 从 1 开始累加单页大约 10 到 20 条结果page_typesearchall 是综合页会混入用户卡片和话题卡片后面解析时必须用 card_type 过滤。timeout 设 10 秒比较合理微博接口偶发慢响应超时后 catch 住重试即可。3.2 解析 cards 并清洗文本把 HTML 标签、短链和占位词挡在库外返回的卡片不全是微博正文。card_type9 是微博内容card_type11 是推荐用户还有话题聚合卡片。解析时只取 card_type9 的卡片避免把用户昵称当正文入库。import re def clean_text(raw_text): if not raw_text: return text re.sub(r[^], , raw_text) # 去掉HTML标签 text text.replace(分享图片, ).replace(分享视频, ) text re.sub(r#([^#])#, r\1, text) # 去掉话题符保留话题词 text re.sub(rhttps?://\S, , text) # 去掉短链 text re.sub(r\s, , text).strip() return text def extract_rows(cards): rows [] for card in cards: if card.get(card_type) ! 9: continue mblog card.get(mblog) or {} if not mblog.get(mid): continue rows.append({ mid: mblog.get(mid), user: (mblog.get(user) or {}).get(screen_name, ), created_at: mblog.get(created_at, ), text: clean_text(mblog.get(text, )), reposts_count: mblog.get(reposts_count, 0), comments_count: mblog.get(comments_count, 0), attitudes_count: mblog.get(attitudes_count, 0), }) return rows逻辑说明clean_text 里的替换顺序不能乱。先去掉 HTML 标签因为微博正文里的表情和话题会被包在 span 标签里再去掉“分享图片”“分享视频”这类占位文案最后去 URL否则短链会进入词频统计把词云带偏。话题“#新能源汽车#”提取后保留“新能源汽车”这个词它恰恰是分析主体。这里的边界坑是很多卡片字段缺失上面全部用 get() 兜底遇到少字段的卡片不会中断整个采集循环。采集中断最难受的不是重试而是前面翻过去的页要重新翻产生重复数据。3.3 增量去重与落库SQLite 够用主键设计从第一天做对数据规模在几万条以内SQLite 完全够用单文件备份方便不需要额外维护数据库服务。多人协作或数据量到百万级再考虑 MySQL。表结构里 mid 做唯一键keyword 单独一列因为一张表要存多个关键词的采集结果。import sqlite3 conn sqlite3.connect(weibo_opinion.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS weibo_post ( mid TEXT PRIMARY KEY, keyword TEXT, user TEXT, created_at TEXT, timestamp INTEGER, text TEXT, reposts_count INTEGER DEFAULT 0, comments_count INTEGER DEFAULT 0, attitudes_count INTEGER DEFAULT 0, crawl_time TEXT ) ) def save_rows(rows, keyword): import datetime now datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) for row in rows: cursor.execute( INSERT OR IGNORE INTO weibo_post (mid, keyword, user, created_at, timestamp, text, reposts_count, comments_count, attitudes_count, crawl_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , ( row[mid], keyword, row[user], row[created_at], row[ts], row[text], row[reposts_count], row[comments_count], row[attitudes_count], now )) conn.commit()逻辑说明mid 来自微博平台同一条微博在不同关键词搜索中会出现多次比如同时爬“新能源汽车”和“特斯拉”一条微博会被搜到两次。以 mid 为主键并用 INSERT OR IGNORE 写入重复数据直接跳过。timestamp 列是为时间趋势统计准备的。微博接口返回的 created_at 多是“3分钟前”“昨天 15:20”这类相对时间入库时最好解析成绝对时间戳否则跨天统计会乱。解析函数放到第五章专门讲那是一个容易踩的一整类坑。4. 情感分析与可视化落地从 SnowNLP 打分到 Flask 舆情看板4.1 SnowNLP 批量打分短文本过滤与正负阈值怎么定情感打分这一步很多新手直接对每条文本循环调用 SnowNLP数据到两万条就跑得很慢还会把空文本、纯表情文本也送去打分。先做两个前置过滤文本长度小于 2 的跳过纯 URL 组成的文本跳过。import pandas as pd from snownlp import SnowNLP def score_one(text): text (text or ).strip() if len(text) 2: return None return SnowNLP(text).sentiments df[score] df[text].apply(score_one) df df.dropna(subset[score]) POS_TH, NEG_TH 0.6, 0.4 df[emo_label] df[score].apply( lambda s: positive if s POS_TH else (negative if s NEG_TH else neutral) ) print(df[emo_label].value_counts())逻辑说明score_one 返回 SnowNLP 计算的正面概率越接近 1 代表越正向越接近 0 代表越负向。阈值用 0.6 和 0.4是因为电商语料训练的模型输出普遍偏向中间值直接以 0.5 为界会把大量中性微博硬分成正负反而失真。跳过短文本也很关键微博里常见的“哈哈哈”三个字SnowNLP 经常给出莫名其妙的低分。更稳妥的做法是对分类结果做人工抽查随机抽几十条看标签和文本对不对得上再决定阈值是 0.6 还是 0.55。阈值是项目参数不是真理。4.2 用 Pyecharts 做情绪趋势折线和关键词词云可视化第一步是拿数据说话不要一上来就追求花哨交互。最常用的三张图是情绪折线图、情感占比饼图、关键词词云。折线图用 LineX 轴是日期Y 轴是正向和负向微博数量能清楚看到事件爆发的拐点。from pyecharts.charts import Line, Pie, WordCloud from pyecharts import options as opts def draw_trend(date_list, neg_counts, pos_counts): line ( Line(init_optsopts.InitOpts(width1000px, height480px)) .add_xaxis(date_list) .add_yaxis(负向, neg_counts, is_smoothTrue) .add_yaxis(正向, pos_counts, is_smoothTrue) .set_global_opts( title_optsopts.TitleOpts(title舆情情绪趋势), tooltip_optsopts.TooltipOpts(triggeraxis), legend_optsopts.LegendOpts(pos_leftcenter), ) ) return line.render(trend.html)逻辑说明add_yaxis 的 is_smoothTrue 让曲线更平滑数据点稀疏时也能看出走势不会因为个别波动误导判断。tooltip 设为 axis 模式鼠标悬停能同时看到同一天的正面和负面数量。render 直接输出 HTML 文件浏览器打开即用。词云图要注意数据格式Pyecharts 的 WordCloud 接收的是 [(词, 权重), ...] 列表不能用字典直接传。做词频统计时去掉无意义的停止词比如“微博”“转发”“链接”这类高频但不携带情绪的噪音词。from collections import Counter import jieba stopwords {微博, 转发, 链接, 视频, 网页} def word_freq_from(texts): words [] for t in texts: words [w for w in jieba.lcut(t) if len(w) 1 and w not in stopwords] return Counter(words).most_common(100)逻辑说明jieba.lcut 是精确分词返回词列表。过滤条件里的 len(w) 1 去掉单字stopwords 去掉分析噪音。前面清洗阶段保留的“新能源汽车”会作为完整词进入词频成为词云里显眼的焦点词。4.3 Flask 极简看板后端只提供 JSON前端组装图表舆情看板不必搞成复杂单页应用Flask 加一个模板就够。后端提供 JSON 接口前端页面里用 Pyecharts 生成的图表串起来。省事的做法是后端把图表单独 render 成 HTML 片段前端页面直接嵌入。from flask import Flask, jsonify, render_template import sqlite3 app Flask(__name__) def fetch_trend(keyword, days): conn sqlite3.connect(weibo_opinion.db) rows conn.execute( SELECT date(timestamp, unixepoch, localtime) as d, SUM(CASE WHEN emo_labelnegative THEN 1 ELSE 0 END) as neg, SUM(CASE WHEN emo_labelpositive THEN 1 ELSE 0 END) as pos FROM weibo_post WHERE keyword? AND timestamp ? GROUP BY d ORDER BY d , (keyword, days * 86400)).fetchall() conn.close() return rows app.route(/) def index(): return render_template(index.html) app.route(/api/trend) def api_trend(): rows fetch_trend(新能源汽车, 7) return jsonify([{date: r[0], neg: r[1], pos: r[2]} for r in rows])逻辑说明route 接口把 SQL 执行结果转成 JSON 交给前端前端拿到后塞进 Pyecharts 的 data。日期处理用了 unixepoch 和 localtime 两个修饰符确保按本地时区而不是 UTC 时区分组否则凌晨的微博会被算到前一天。这是可视化阶段最容易出错的地方对应第五章第四条坑。后端和前端分离还有一个好处定时任务重建数据时页面刷新就能看到新结果不用重启 Flask 进程。5. 微博舆情系统常见问题与避坑清单五个高频翻车点及对策5.1 搜索接口返回 ok-100登录 Cookie 失效还是请求太密现象程序没有报错requests 请求正常返回但 data 里没有 cardsok 字段是 -100 或 0msg 提示“请先登录”。原因两种可能。一是 Cookie 过期二是同一 Cookie 短时间请求量太大触发了服务端的频率限制。解决先看时间点。如果开机第一次请求就失败多半是 Cookie 过期重新登录浏览器复制新 Cookie。如果是连续翻页到第 5 页之后开始失败说明单 Cookie 请求过猛把单页间隔从 1 秒调大到 5 到 8 秒并限制单次任务最多翻 10 页。再不够就把关键词拆开按小时分多次采集比如“新能源汽车 销量”和“新能源汽车 故障”分开搜索单次请求压力小很多。提示Cookie 是敏感信息自己本地研究没问题切勿把它提交到公开仓库否则可能被冒用。5.2 情感分析把微博反讽当正面硬负面规则先于模型现象一条“这操作真是666建议直接申遗”被标成正向但人眼一眼看出是嘲讽。原因SnowNLP 的语料来自商品评价模型不识别微博语境里的反讽和网络新词。电商评论里少见的语用表达在微博上是日常操作。解决在跑模型之前先过一层硬负面规则。规则命中直接判负不进模型。规则词要精不要多避免误伤。HARD_NEGATIVE [翻车, 离谱, 无语, 避雷, 气死, 被坑] def hard_rule(text): for w in HARD_NEGATIVE: if w in text: return negative return None逻辑说明hard_rule 只做短文本匹配命中返回 negative未命中返回 None。主流程里先调用它有结果直接赋值没有结果再交给 SnowNLP 打分。参数调整规则词不是字典越大越好。把“拆”“慢”这种多义词加进去会把“拆快递”和“慢慢变好”误判成负面。负面词表每加一个词至少用 50 条历史数据做一次回测确认误伤率。5.3 词云里全是表情和噪音词清洗顺序必须固定现象词云图里出现大量“—”“”“分享图片”“超话”等无意义词。原因微博正文里充满 HTML 标签、短链、表情符和运营位文案清洗时只剥了标签没有做完整过滤。解决把清洗顺序固定成一条流水线HTML 标签、短链接、分享占位词、连续空白、emoji。其中 emoji 要不要保留取决于分析目标。做词频统计时建议删除做情感分析时建议保留表情符号本身携带情绪但进了词云就是噪声。import re def cleaner(text, keep_emojiTrue): text re.sub(r[^], , text) text re.sub(rhttps?://\S, , text) text text.replace(分享图片, ).replace(分享视频, ) if not keep_emoji: emoji_pattern re.compile( [\U0001F300-\U0001F64F\U0001F680-\U0001F6FF \U0001F900-\U0001F9FF\U00002600-\U000027BF] ) text emoji_pattern.sub(, text) return re.sub(r\s, , text).strip()逻辑说明emoji 的正则范围覆盖了 Unicode 的几大 emoji 区段能去掉大部分常见表情。keep_emoji 参数让清洗阶段先保留词频统计阶段再决定是否过滤比清洗时无条件删除灵活得多。注意这个正则的字符区间在 Python 3.7 以上正常老版本 Python 或某些 Windows 控制台环境下运行可能报 Unicode 编码错误脚本文件头记得声明 utf-8。5.4 按天统计偏移八小时相对时间转时间戳的解析坑现象每天凌晨 0 点前后爬到的数据在趋势图里被归到前一天。原因微博 mblog 里的 created_at 返回的是“3分钟前”“昨天 15:30”这类相对时间入库时直接存了字符串。或者解析成时间戳时没有对齐东八区SQLite 分组时错位。解决建立统一的相对时间解析函数入库前把 created_at 统一转成时间戳。import re from datetime import datetime, timedelta def parse_weibo_time(created_at, nowNone): now now or datetime.now() if 刚刚 in created_at: return now m re.search(r(\d)分钟前, created_at) if m: return now - timedelta(minutesint(m.group(1))) m re.search(r(\d)小时前, created_at) if m: return now - timedelta(hoursint(m.group(1))) m re.search(r昨天 (\d):(\d), created_at) if m: return (now - timedelta(days1)).replace( hourint(m.group(1)), minuteint(m.group(2)), second0, microsecond0 ) m re.search(r(\d{4})-(\d{2})-(\d{2}), created_at) if m: return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3))) return now逻辑说明匹配顺序有讲究先匹配“分钟前”再“小时前”再“昨天 xx:xx”最后是完整日期。不能反过来因为“昨天 15:30”里的“30”如果先被“分钟前”正则捕获日期就错了。return now 兜底拿不准的文本不把程序搞挂。入库时把返回值转成 int 时间戳存到 timestamp 字段之后 SQLite 分组和 Flask 接口就都能对齐。5.5 重跑采集后统计数据翻倍主键去重与多关键词计数现象当天第一次跑负面数是 200第二次定时任务又跑负面变成 400趋势图每天跳变。原因同一个关键词重复采集中间产生的数据没有正确去重。或者之前清库重跑旧数据没清理干净。解决主键是底线遇到多种情况统一按 mid 做 INSERT OR IGNORE。多关键词场景还有另一种翻倍同一条微博在“新能源汽车”和“特斯拉”两个关键词下都被采集按关键词分组统计时这条微博在两组里都会计入总情绪占比加一块超过 100%。统计时只按 mid 去重后再分关键词或者最外层查询用 SELECT DISTINCT mid。另一个教训是采集脚本和统计脚本不要共用同一个“最近 N 天”过滤条件。采集脚本用“最近 1 小时新增”抓增量统计脚本用“最近 7 天全量”做分析两者逻辑分开比在采集端反复纠结是否重复友好得多。6. 进阶验证给舆情系统加一把尺子再谈扩展6.1 先做 30 条人工标注算算真实准确率系统搭完第一件事不是加功能而是验证输出可不可信。随机抽最近三天采集的文本按正负中性各抽一部分手工标注后与系统输出对比。准确率、召回率、F1 直接用 sklearn 算from sklearn.metrics import classification_report # y_true 是手工标注y_pred 是系统输出两者都是字符串列表 print(classification_report(y_true, y_pred, target_names[negative, neutral, positive]))如果总体准确率不到 70%不建议急着调阈值先看错误样本集中在哪类。常见错误分布在反讽句、语气词密集的短句和带表情符号的句子。找到主要错误模式再针对性加规则。6.2 用否定词反转做规则修正精度有明显提升规则先于模型这条实际改进比想象中大。一条微博无论 SnowNLP 给什么分数命中硬负面词就强制标负。否定词的修正是进阶操作字符串匹配会翻车分词后找否定词相邻词更稳只在“不、没、别”后面跟着正面词时翻转一次。neg_prefix {不, 没, 别, 无, 莫} def flip_negation(words): for i, w in enumerate(words[:-1]): if w in neg_prefix and words[i 1] in POSITIVE_DICT: words[i] 否定 return words逻辑说明POSITIVE_DICT 是自己维护的正面词表命中正面词且前一个词是否定词就把该词替换成“否定”。这个技巧比简单否定匹配准确得多但依旧处理不了“你说得没毛病”这类口语别指望规则解决全部问题。6.3 养成先跑一周再扩展的习惯每次做完这类系统我都会先拿一个自己熟悉的领域跑一周每天看它报出来的趋势和情绪占比。哪条数据明显不合理立刻追查是清洗环节还是打分环节出了问题。系统的可信度不是靠复杂模型撑起来的是这些最朴素的核对习惯一层层垒出来的。等一周后你对自己系统的边界有了具体感知再谈多模态扩展、引入预训练模型或者做事件检测都会踏实很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表