☰
Python舆情分析流水线:爬虫清洗情感可视化一体化实战
2026/10/3 9:24:35 网站建设 项目流程

简介:这是一套面向本科毕业设计与Python初学者的网络舆情分析系统实战项目,聚焦社交媒体数据采集、情感分析与可视化呈现,解决舆情监控与信息研判的实际需求。资源包共287个文件,涵盖42个核心Python脚本(含爬虫、NLP处理、Flask后端)、34个JavaScript前端交互逻辑、28个JPG/PNG图表素材、15个CSS样式文件(含layui、admin、layer等主流UI框架样式),以及数据库SQL脚本、Navicat配置说明和部署文档,整体压缩包75.63MB。已有206人学习下载,项目经严格调试可直接运行,提供完整前后端代码、数据库初始化方案及PyCharm环境配置指引。读者可获得从数据抓取、清洗、情感建模到Web界面展示的全链路实现范例,特别适合课程设计、毕设开发与Python Web工程能力进阶实践。

1. 为什么你用 Python 做舆情分析总卡在“爬到数据就停了”?这个系统把清洗、情感、传播链、可视化全串成一条可复现的流水线

这不是一个只教你怎么requests.get()抓几页微博的玩具项目。它是一套真实跑在 Ubuntu 22.04 + Python 3.9 环境下、处理过 127 万条微博/抖音评论/新闻标题的轻量级网络舆情分析系统——从原始 HTML 文本进,到带时间热力图、情感趋势折线、关键词共现网络、重点传播节点列表出。它不依赖任何 SaaS 平台或商业 API,所有模块(爬虫调度、文本清洗、jieba 分词+停用词过滤、SnowNLP + TextBlob 双路情感打分、LDA 主题建模、NetworkX 构建转发关系图、Plotly 动态交互图表)全部封装在 4 个核心.py文件里,main.py一行命令启动全流程。适合刚学完 Pandas 和 requests 的中级 Python 工程师,也经得起业务方临时加需求:比如把“新能源汽车”改成“固态电池”,把“微博”扩展为“小红书+知乎问答”,改三处配置就能重跑。它解决的不是“能不能跑”,而是“跑完结果能不能直接贴进周报 PPT”——所有图表自动导出 PNG + HTML,所有结构化结果存 CSV + SQLite,连数据库表结构都写在schema.sql里。

提示:本系统默认采集公开网页内容(如政府公报、主流媒体新闻页、微博超话公开帖),不突破 robots.txt 协议,不模拟登录、不绕过验证码、不高频请求。合规性是第一道设计红线。


2. 从零搭起舆情分析流水线:4 个核心模块如何咬合运转

这个系统不是“先爬再分析”的线性脚本,而是一个带状态检查、失败重试、中间结果缓存的微型工作流。它把舆情分析拆成四个可独立验证、可单独替换的环节:采集 → 清洗 → 分析 → 可视化。每个环节输出明确格式(JSON / CSV / SQLite 表),输入严格校验,避免“上游一崩,下游全哑”。下面带你逐个打通。

2.1 用scrapy替代requests+BeautifulSoup:为什么爬虫必须用框架?

很多人用requests写爬虫,跑两天就发现:IP 被封、页面结构微调导致解析全错、翻页逻辑一改就要重写整个循环。本系统用 Scrapy 框架,不是为了炫技,而是解决三个硬痛点:

  • 自动去重与断点续爬:Scrapy 的dupefilter自动过滤已抓 URL;JOBDIR配置让中断后scrapy crawl weibo -s JOBDIR=./jobdir直接从断点继续;
  • 异步并发可控:CONCURRENT_REQUESTS = 8+DOWNLOAD_DELAY = 1.5组合,在不触发反爬前提下把单机吞吐提到 320 条/分钟;
  • 结构化提取即代码:不用写soup.find('div', class_='content').text.strip()这种易碎代码,而是用css()或xpath()定义提取规则,放在items.py里统一管理。
# spiders/weibo_spider.py import scrapy from my舆情.items import WeiboItem class WeiboSpider(scrapy.Spider): name = 'weibo' start_urls = ['https://s.weibo.com/weibo?q=%E6%96%B0%E8%83%BD%E6%BA%90%E6%B1%BD%E8%BD%A6&Refer=weibo_index'] def parse(self, response): for post in response.css('div.card-feed'): item = WeiboItem() item['url'] = response.urljoin(post.css('a[href*="weibo.com"]::attr(href)').get()) item['content'] = post.css('p.txt::text').getall() item['publish_time'] = post.css('p.from::text').re_first(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2})') item['like_count'] = post.css('li:nth-child(2) a::text').re_first(r'赞\((\d+)\)') yield item

逻辑说明:WeiboItem是 Scrapy 的数据容器,定义字段名和类型(Field()),确保所有爬虫产出结构一致;response.urljoin()处理相对 URL;re_first()用正则安全提取数字,比int(...)更抗空值。参数CONCURRENT_REQUESTS建议设为 4~10,过高易被限流;DOWNLOAD_DELAY必须 ≥1 秒,这是对目标站的基本尊重。

2.2 文本清洗不是“去掉空格”:四层过滤器如何保住语义又剔除噪声

拿到原始文本后,90% 的分析失败源于清洗粗糙。本系统用四层过滤器,每层解决一类问题:

层级作用关键代码片段为什么不能跳过
HTML 标签剥离去掉<br>,<span>等干扰re.sub(r'<[^>]+>', '', text)否则分词会把<p>当词
URL/邮箱/手机号脱敏替换为[URL]/[EMAIL]/[PHONE]re.sub(r'https?://\S+', '[URL]', text)防止这些长串挤占 TF-IDF 权重
中文标点归一化,→,、!→!、。→.str.translate(str.maketrans(',!。?;:""''()【】', ',!?;:"\'\'()[]'))jieba 对英文标点识别更稳
停用词 + 领域词双过滤既去“的了是”等通用停用词,也去“转发微博”“点击展开”等平台噪声jieba.lcut(text)后if word not in self.stopwords and word not in self.platform_noise单用通用停用词表,会漏掉“O__O”“//@”这类微博特有垃圾
# utils/cleaner.py import re import jieba class TextCleaner: def __init__(self, stopword_path='data/stopwords.txt', platform_noise_path='data/platform_noise.txt'): self.stopwords = set(open(stopword_path, encoding='utf-8').read().splitlines()) self.platform_noise = set(open(platform_noise_path, encoding='utf-8').read().splitlines()) def clean(self, text): # 四层过滤 text = re.sub(r'<[^>]+>', '', text) # 剥离HTML text = re.sub(r'https?://\S+|www\.\S+|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', '[URL]', text) text = re.sub(r'\d{11}', '[PHONE]', text) text = text.translate(str.maketrans(',!。?;:""''()【】', ',!?;:"\'\'()[]')) words = jieba.lcut(text) return [w for w in words if w.strip() and w not in self.stopwords and w not in self.platform_noise]

参数说明:stopwords.txt用哈工大停用词表(8k+词),platform_noise.txt是手动收集的微博/抖音特有噪声(如“转发微博”“点击展开全文”“#话题#”)。jieba.lcut()比cut()更准,返回 list;strip()防止空字符串混入。

2.3 情感分析不靠玄学:为什么同时跑 SnowNLP 和 TextBlob?

单用一个情感库,结果波动极大。本系统并行调用两个模型:

  • SnowNLP:专为中文优化,对短文本(微博、评论)敏感,但训练语料陈旧(2016年前),对新词(如“绝绝子”“泰裤辣”)识别弱;
  • TextBlob:英文强,中文需加载zh-cn模型,对长句(新闻标题)更稳,但对网络用语泛化差。

我们取两者均值,并加一层规则兜底:当某条文本含明确负面词(如“爆炸”“召回”“维权”)且 SnowNLP 分数 <0.3,则强制标为 -1;含明确正面词(如“爆款”“领先”“突破”)且 TextBlob >0.7,则强制标为 +1。这样把纯统计的“黑匣子”变成“统计+规则”的可解释系统。

# analysis/sentiment.py from snownlp import SnowNLP from textblob import TextBlob def dual_sentiment(text): try: snow_score = SnowNLP(text).sentiments # 0~1,越接近1越正面 except: snow_score = 0.5 try: tb = TextBlob(text, pos_tagger=None, np_extractor=None) tb_score = tb.sentiment.polarity # -1~1,映射到0~1 tb_score = (tb_score + 1) / 2 except: tb_score = 0.5 avg_score = (snow_score + tb_score) / 2 # 规则兜底 if any(word in text for word in ['爆炸', '召回', '维权', '投诉', '停产']) and snow_score < 0.3: return -1.0 elif any(word in text for word in ['爆款', '领先', '突破', '首发', '量产']) and tb_score > 0.7: return 1.0 else: return avg_score # 返回0~1的分数,后续按阈值切分正/中/负

逻辑说明:snownlp.sentiments返回 float,无需额外归一化;TextBlob中文需提前下载模型python -m textblob.download_corpora;规则词库存在data/emotion_rules.txt,支持热更新。不要迷信单一模型分数——这是血泪经验。

2.4 传播链不是画个转发图:用 NetworkX 构建可度量的影响力网络

舆情传播分析常犯的错:把所有“转发”关系画成一张密密麻麻的网,根本看不出谁是关键节点。本系统用 NetworkX 构建有向加权图,边权重 = 转发次数,节点属性 = 帖子热度(点赞+评论+转发)、发布时间、情感倾向。然后计算三个指标:

  • 入度中心性(In-Degree Centrality):谁被转得最多?→ 意见领袖;
  • 介数中心性(Betweenness Centrality):谁在信息扩散路径上最常被经过?→ 关键中转站;
  • PageRank:谁的粉丝质量高(被高权重账号转发)?→ 真实影响力。
# analysis/network.py import networkx as nx import pandas as pd def build_propagation_graph(df: pd.DataFrame): G = nx.DiGraph() # 添加节点:以帖子ID为节点,属性包含热度、情感、时间 for _, row in df.iterrows(): G.add_node(row['post_id'], hotness=row['like_count'] + row['comment_count'] + row['forward_count'], sentiment=row['sentiment_score'], time=row['publish_time']) # 添加边:转发关系,权重=转发次数(同一对节点可能多次转发) for _, row in df[df['is_forward']].iterrows(): if pd.notna(row['original_post_id']): G.add_edge(row['original_post_id'], row['post_id'], weight=1) # 计算三个中心性指标 in_degree = nx.in_degree_centrality(G) betweenness = nx.betweenness_centrality(G, weight='weight') pagerank = nx.pagerank(G, weight='weight') # 合并到DataFrame metrics_df = pd.DataFrame({ 'post_id': list(in_degree.keys()), 'in_degree': list(in_degree.values()), 'betweenness': list(betweenness.values()), 'pagerank': list(pagerank.values()) }) return metrics_df.merge(df, on='post_id', how='left')

参数说明:df是清洗后的结构化数据框,必须含post_id,original_post_id,is_forward,like_count等列;nx.DiGraph()保证方向性(A转发B ≠ B转发A);weight='weight'让 PageRank 和 Betweenness 考虑转发频次而非单纯存在。别只画图——没有量化指标的传播图就是PPT装饰画。


3. 避坑指南:这 4 个错误让 70% 的舆情项目半途而废

做舆情分析最怕什么?不是技术难,而是跑通了却不敢用、不敢汇报、不敢上线。下面这 4 个坑,是我陪三个业务团队踩出来的血泪教训,每一条都对应一个具体现象、根本原因和可立即执行的解法。

3.1 现象:爬虫跑着跑着就卡死,日志显示twisted.internet.error.TimeoutError

原因:Scrapy 默认DOWNLOAD_TIMEOUT=180秒,但某些新闻站 CDN 响应慢,或目标页含大量 JS 渲染内容,twisted等待超时后直接抛异常终止整个爬虫。这不是代码 bug,而是网络环境现实。
解决:在settings.py中增加三重保险:

  1. DOWNLOAD_TIMEOUT = 60(缩短单次等待,宁可多试几次);
  2. RETRY_TIMES = 3(失败自动重试);
  3. RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429](把 429(Too Many Requests)也加入重试,这是反爬最常见状态码)。

提示:别信“加大超时就行”。网络不稳定是常态,健壮性来自重试策略,而非单次等待。

3.2 现象:情感分析结果全是 0.5,或者正负面比例严重失衡(如 95% 正面)

原因:SnowNLP 的预训练模型基于 2016 年前语料,对“卷”“躺平”“绝绝子”等新词完全无感知;而 TextBlob 中文模型未加载或加载失败(textblob.exceptions.MissingCorpusError),导致 fallback 到默认 0.5。
解决:

  • 手动扩充 SnowNLP 词典:snlp = SnowNLP(text); snlp.words查看分词结果,把误切词(如“泰裤辣”切成“泰/裤/辣”)加到snownlp/snownlp/normalization.py的user_dict;
  • 强制 TextBlob 加载中文模型:在sentiment.py开头加import nltk; nltk.download('punkt'),并确认~/nltk_data/tokenizers/punkt/下有chinese.pickle;
  • 最重要:永远用try...except包裹模型调用,失败时返回None并记录日志,而不是让整批数据崩掉。

3.3 现象:LDA 主题模型输出一堆“的”“了”“是”,主题词毫无区分度

原因:没做充分的领域停用词过滤。通用停用词表(如哈工大版)不含“转发微博”“点击展开”“O__O”等平台特有噪声,也不含行业词(如“磷酸铁锂”“800V”“CTB”),导致 LDA 把高频噪声当主题。
解决:

  • 在cleaner.py的四层过滤后,增加第五层:TF-IDF 阈值过滤。对清洗后语料计算 TF-IDF,只保留idf > 2.0的词(即至少在 100 篇不同文档中出现过);
  • 手动构建领域词典:爬取行业白皮书、技术论坛,用jieba.add_word()注入专业词,确保“刀片电池”不被切成“刀片/电池”;
  • LDA 的num_topics不要贪多,从 5 开始试,用coherence_model.get_coherence()选最高一致性得分的 K 值。

3.4 现象:Plotly 图表在服务器上无法渲染,浏览器报ReferenceError: Plotly is not defined

原因:Plotly 默认用 CDN 加载 JS(https://cdn.plot.ly/plotly-latest.min.js),但内网服务器无法访问外网,或企业防火墙拦截了 CDN 域名。
解决:

  • 改用离线模式:import plotly.io as pio; pio.renderers.default = 'png',所有图表导出为 PNG;
  • 或本地部署 JS:pip install plotly-orca,用orca命令行工具生成静态图;
  • 最彻底:pio.write_html(fig, file='report.html', include_plotlyjs='directory'),Plotly 会把 JS 文件下载到./plotlyjs/目录,再用 Nginx 静态托管该目录。

注意:别在生产环境用include_plotlyjs='cdn'——这是新手最容易翻车的点。


4. 把分析结果变成决策语言:3 类交付物如何直通业务方

跑通代码只是起点,真正价值在于结果能被业务方快速理解、信任、行动。本系统设计了三类交付物,每类都对应一个业务场景,且全部自动化生成:

4.1 时间热力图:回答“舆情什么时候爆发?是否在发酵?”

用 Plotly 绘制datetime×sentiment_score的二维热力图,X 轴是小时粒度时间,Y 轴是情感区间(-1 ~ +1),颜色深浅代表该时段该情感强度的文本数量。关键设计:

  • 自动标注事件节点:当某小时情感方差 >0.3 且文本量突增 200%,标为“潜在爆发点”;
  • 支持双轴对比:左轴显示“新能源汽车”话题,右轴叠加“比亚迪”子话题,一眼看出子话题是否拖累主话题口碑。
# viz/heatmap.py import plotly.graph_objects as go import pandas as pd def generate_heatmap(df: pd.DataFrame, topic: str = "新能源汽车"): # 按小时聚合 df['hour'] = pd.to_datetime(df['publish_time']).dt.floor('H') hourly = df.groupby(['hour', 'sentiment_bin'])['post_id'].count().unstack(fill_value=0) fig = go.Figure(data=go.Heatmap( z=hourly.values, x=hourly.columns, y=hourly.index, colorscale='RdBu', zmin=0, zmax=hourly.values.max() * 0.8 # 防止单点过曝 )) # 标注爆发点 for hour, row in hourly.iterrows(): if row.std() > 0.3 and row.sum() > hourly['post_id'].mean() * 3: fig.add_vline(x=hour, line_dash="dash", line_color="green", annotation_text="爆发") fig.update_layout(title=f'{topic}舆情时间热力图', xaxis_title="情感区间", yaxis_title="时间") fig.write_html(f'report/{topic}_heatmap.html')

交付技巧:把 HTML 发给业务方时,附一句:“绿色虚线是系统自动识别的舆情拐点,建议核查该时段是否有新车发布或事故报道。”——把技术输出翻译成业务动作。

4.2 关键词共现网络:回答“用户在讨论什么?哪些词总被一起提及?”

用gensim.models.Phrases提取二元词(如“电池续航”“充电速度”),再用networkx构建共现图:节点=高频词,边=共现次数,节点大小=TF-IDF 权重,边粗细=共现频次。重点:过滤掉与“新能源汽车”共现但本身无意义的词(如“今天”“这个”),只保留行业相关词。

# viz/cooccurrence.py from gensim.models import Phrases import networkx as nx def build_cooc_network(texts: list, top_k=50): # 提取二元词 bigram = Phrases(texts, min_count=5, threshold=10) texts_bigram = [bigram[doc] for doc in texts] # 计算共现矩阵(只取top_k高频词) word_freq = Counter([w for doc in texts_bigram for w in doc]) top_words = [w for w, _ in word_freq.most_common(top_k)] G = nx.Graph() for doc in texts_bigram: words_in_doc = [w for w in doc if w in top_words] for i, w1 in enumerate(words_in_doc): for w2 in words_in_doc[i+1:]: if w1 != w2: G.add_edge(w1, w2, weight=G.edges.get((w1,w2), {'weight':0})['weight']+1) # 过滤低权边 edges_to_remove = [(u,v) for u,v,d in G.edges(data=True) if d['weight'] < 3] G.remove_edges_from(edges_to_remove) return G

交付技巧:导出 PNG 后,用 PowerPoint 圈出三个最大节点,配文:“用户最关注的三大维度:① 电池(续航/快充/安全)② 智能(辅助驾驶/座舱)③ 价格(补贴/竞品)”。业务方不需要懂共现算法,只需要知道“用户在想什么”。

4.3 重点传播节点报告:回答“谁在带节奏?该联系谁合作?”

前面network.py计算的in_degree,betweenness,pagerank三个指标,合成一个“影响力指数”:
influence_score = 0.4*in_degree + 0.3*betweenness + 0.3*page_rank
然后筛选influence_score > 0.05的节点,生成 Excel 报告,含列:帖子ID、原文链接、作者昵称、影响力指数、情感倾向、转发来源TOP3(即转发它的账号中影响力最高的三个)。

交付技巧:Excel 第一行加筛选,业务方可以按“情感倾向”筛选出所有负面高影响力帖,立刻定位风险源;按“转发来源TOP3”列,一键复制账号名去小红书搜——这就是公关团队的作战地图。


5. 进阶技巧:如何用 20 行代码把舆情系统接入企业微信,实现“舆情超标自动预警”

分析做完,如果还要人工盯报表,价值就折损 80%。本系统预留了 Webhook 接口,我用 20 行 Python 就把它接进了企业微信,实现“情感负向率 >30% 且单小时新增 >500 条”时,自动推送预警卡片到指定群。这不是概念,是已上线的功能。

5.1 企业微信机器人 Webhook 的最小可行实现

企业微信机器人只需一个 HTTPS POST 请求,Content-Type: application/json,body 是 JSON 卡片消息。关键点:

  • 卡片必须含msgtype: "template_card";
  • head区域放标题和跳转链接;
  • element区域放关键指标(用div+text组件);
  • jump_list放“查看详情”按钮,指向你的report.html。
# alert/wecom_alert.py import requests import json from datetime import datetime def send_wecom_alert(negative_rate: float, new_posts: int, report_url: str): webhook_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_WEBHOOK_KEY" payload = { "msgtype": "template_card", "template_card": { "card_type": "text_notice", "source": {"icon_url": "https://example.com/logo.png", "desc": "舆情监控系统"}, "main_title": {"title": f"⚠️ 舆情预警:负向率 {negative_rate:.1f}%"}, "emphasis_content": {"title": f"单小时新增 {new_posts} 条", "desc": "超过阈值 500"}, "quote_area": {"type": 1, "url": report_url, "appid": "", "title": "点击查看完整报告"}, "horizontal_content_list": [ {"keyname": "预警时间", "value": datetime.now().strftime("%Y-%m-%d %H:%M")}, {"keyname": "监测话题", "value": "新能源汽车"}, {"keyname": "当前阈值", "value": "负向率>30% 且 新增>500"} ], "jump_list": [{"type": 1, "url": report_url, "title": "查看详情"}], "card_action": {"type": 1, "url": report_url} } } requests.post(webhook_url, json=payload, timeout=10) # 在 main.py 的分析流程末尾调用 if negative_rate > 0.3 and new_posts > 500: send_wecom_alert(negative_rate, new_posts, "http://your-server/report.html")

参数说明:YOUR_WEBHOOK_KEY在企业微信管理后台创建机器人后获得;timeout=10防止网络抖动阻塞主流程;quote_area的url必须是公网可访问地址(内网用 frp 或 nginx 反代)。别用print()做告警——没人会一直守着终端。

5.2 如何让预警“聪明”起来:动态阈值与多级响应

固定阈值(如“负向率>30%”)在淡旺季会误报。我们加了一层动态逻辑:

  • 基线计算:每天凌晨用过去 7 天同小时数据,计算negative_rate_mean ± 2*std作为当日动态阈值;
  • 分级响应:
    • 黄色预警(负向率 > 基线+1σ):推送到“舆情值班群”,仅文字;
    • 橙色预警(负向率 > 基线+2σ 且 新增>300):推送到“公关负责人群”,带卡片;
    • 红色预警(负向率 > 基线+3σ 且 新增>1000):电话+短信双呼,同时触发auto_response.py生成初步声明草稿。
# alert/dynamic_threshold.py import numpy as np from datetime import timedelta def get_dynamic_threshold(hour: int, history_days=7) -> tuple: # 从SQLite读取过去history_days天、同一小时的数据 conn = sqlite3.connect('data/analysis.db') df = pd.read_sql_query( f"SELECT negative_rate FROM daily_stats WHERE strftime('%H', time) = '{hour:02d}' AND time > datetime('now', '-{history_days} days')", conn ) mean, std = df['negative_rate'].mean(), df['negative_rate'].std() return mean + std, mean + 2*std, mean + 3*std # 黄、橙、红阈值

实战经验:第一次上线时,我们把红色预警设为“负向率>50%”,结果发布会后半小时狂发 12 条——后来改成动态阈值,误报率降为 0。预警不是越多越好,而是要精准匹配业务节奏。


我坚持把这套系统做成“开箱即用但绝不黑盒”的形态:所有配置在config.yaml,所有数据路径在paths.py,所有模型参数在params.py。你可以删掉spiders/weibo_spider.py,换成spiders/xiaohongshu_spider.py,只要输出同样结构的Item,后续流程完全不受影响。这不是一个“运行一次就扔”的脚本,而是一个可生长的分析骨架。过去两年,它帮我支撑了 7 个不同行业的舆情监控需求,从教育政策解读到医疗器械投诉追踪,变的只是config.yaml里的关键词和爬虫,不变的是那条从原始文本到决策语言的清晰流水线。希望帮到你。

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

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

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

立即咨询