ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

网络舆情分析系统实战:Python+Flask+MySQL情感分析闭环

网络舆情分析系统实战:Python+Flask+MySQL情感分析闭环 简介这是一份基于Python开发的网络舆情分析系统完整源码包面向舆情监控管理人员也适合毕业设计、课程设计场景。系统支持多用户同时使用管理员唯一核心功能包括用户言论采集、情感倾向分析、数据统计可视化以及用户信息管理饼状图可直观呈现舆论分布。开发环境为Python 3.6.8、MySQL 5.7采用前后端分离结构便于理解业务逻辑与二次扩展。压缩包共289个文件约83.39MB其中Python源码与pyc文件承载后端业务逻辑HTML/CSS/JavaScript构成前端交互界面SQL脚本用于初始化MySQL数据库GIF/JPG图片记录界面运行效果另附说明文档与答辩PPT类型丰富、目录结构清晰。资源附带详细说明文档和演示图例可帮助快速完成环境搭建、数据库初始化和功能复现是毕业设计或实训项目的高质量参考当前已有100人学习下载。1. 网络舆情分析系统到底是什么一套能应对毕设答辩的完整闭环你可能在各大源码站见过这个标题压缩包里除了源代码往往还带 MySQL 建表脚本、说明文档和 LW 材料。很多同学下载后第一反应是把后端跑起来结果卡在 MySQL 连不上、前端图表白屏、情感分析全是“中性”三个坎上。这篇文章不聊怎么下载就聊网络舆情分析系统从采集、清洗、情感分析到可视化的完整落地路径Python 后端 前端页面 MySQL 存储按毕设场景该怎么做、参数怎么调、坑在哪里。适合三类人做毕业设计需要可演示系统的在校生、想快速搭一个舆情监控 Demo 的全栈新手、以及需要给团队做内部事件预警工具的初级工程师。只要你愿意花一个周末把数据流跑通这套方案是能直接摆在答辩现场演示的。2. 先把系统拆开舆情分析系统的四个模块与一条数据流水线拿到一个“完整前后端 MySQL”的舆情项目第一步不是急着看代码而是先把代码目录映射成功能模块。我见过太多人打开 Flask 项目后满屏找“舆情分析”那个函数结果发现逻辑藏在好几个文件里。其实无论什么框架网络舆情分析系统的骨架都是固定的数据采集、数据清洗、情感分析、可视化展示。理解了这张地图你才能判断手里的源码缺了什么、要补哪里。2.1 功能模块划分一张表看懂系统在做什么下面这张表是按毕业设计答辩逻辑整理的模块清单你可以拿它对照手头的源码看每个模块是否完整、是否有能演示的入口模块核心职责常见技术实现验收标准数据采集从新闻/微博/论坛抓取文本requests 正则 / scrapy能定时采集并去重入库数据清洗去空、去 URL、去垃圾广告re / pandas入库文本无乱码、无空记录情感分析判断正负面倾向SnowNLP / 朴素贝叶斯抽样准确率大于 75%关键词提取找出热点话题jieba.analyse / TF-IDF能生成 Top N 热词统计汇总按时间/来源聚合SQL 定时任务图表数据可追溯可视化折线图、饼图、词云ECharts / 原生 Chart前端能按日期筛选刷新按照这个清单去核对源码你会发现大部分项目在“数据采集”和“可视化”做得很足“情感分析”往往只是调了一个第三方库就完事。这正是你可以在答辩时讲深的地方你不仅调了库还做了自定义词典和阈值调整。后面第 3 章我会展开讲。2.2 技术栈选型为什么是 Flask SnowNLP 而不是 Django BERT选型这件事在毕设场景里不能只看“技术天花板”要看“能否在一周内跑通、能否在答辩时讲清楚”。常见做法是后端用 Flask原因很简单Flask 单文件就能起服务路由和接口一目了然评审老师问“这个路由怎么走的”你三句话能讲完换 Django 的话中间件、ORM、Admin 这些概念会把答辩变成“背概念大会”。数据库用 MySQL 不是为了跑大数据量而是为了在文档里写出实体关系图、索引设计这些“看起来正规”的内容。情感分析层的选型更关键。BERT 类模型效果好但你要面对训练时间、显存、样本标注三座大山毕设时间根本不够。SnowNLP 是纯 Python 实现的中文情感分析库底层是朴素贝叶斯三行代码就能出情感分而且自带中文停用词和分词能力适合作为基线方案。它的缺点也明显对网络梗词、反讽语气基本失灵所以后面必须加规则覆盖。这套“基线模型 规则纠偏”的组合正好是面试官爱听的工程思路。2.3 数据流走一遍从原始文本到前端图表的四步旅程整个系统的数据流比多数人想的简单。第一步采集脚本定时把新闻标题、评论内容抓下来存进 MySQL 的原始表第二步后台任务读取原始表里还没分析的记录做清洗后交给情感分析模块把打分结果写回另一张结果表第三步Flask 接口按日期、来源从结果表聚合数据返回 JSON第四步前端页面用 ECharts 把 JSON 画成折线图和饼图。这里有个关键设计决策原始表和结果表要分开。为什么因为情感分析参数不是一次就调好的你需要反复回放原始数据重新计算。如果把结果覆盖在原始表上每次调参都要重新抓数据等于给自己挖坑。我一般会在原始表加一个is_analyzed标记字段分析任务只处理标记为 0 的记录处理完置 1。这个设计在答辩时一提就能说明你考虑过数据回溯问题。至于定时任务怎么跑毕设场景不需要上 Celery直接用 APScheduler 挂在 Flask 进程里就行。唯一要注意的是调试时把定时器关掉否则你还在改采集规则后台已经帮你把测试数据抓乱了。3. 情感分析怎么做从分词、评分到热度统计的 Python 可跑代码很多舆情项目的情感分析就是个“黑匣子”输入一句话输出一个正面或负面。但你如果只写到这一步答辩时老师问“阈值为什么是 0.6”你就会卡住。这一章我把整个分析链路拆开从清洗、分词、打分到热度统计给你一份能直接照着改的代码同时解释每个参数该怎么调。3.1 一段能直接跑通的情感分析核心代码先装依赖。这里的顺序有讲究先装 numpy再装 snownlp否则某些环境下 SnowNLP 会报ModuleNotFoundError。常见做法是pip install numpy pip install snownlp jieba pandas pymysql flask flask_cors dbutils装完之后写一个sentiment.py里面是情感分析的主逻辑import re import jieba from snownlp import SnowNLP def clean_text(raw: str) - str: 清洗文本去掉 URL、用户、多余空白保留中文和基础标点。 if not raw: return text re.sub(rhttps?://\S, , raw) # 去链接 text re.sub(r\w, , text) # 去 用户 text re.sub(r\s, , text).strip() # 合并空白 return text def analyze_sentiment(text: str) - dict: 返回情感分数和标签。分数范围 0~1越接近 1 越正面。 text clean_text(text) if not text: return {score: 0.5, label: 中性} s SnowNLP(text) score round(s.sentiments, 4) if score 0.6: label 正面 elif score 0.4: label 负面 else: label 中性 return {score: score, label: label} if __name__ __main__: samples [ 物流很快包装完好好评, 客服一直不回复等了三天体验很差, 东西一般般凑合用吧, ] for item in samples: print(item, -, analyze_sentiment(item))这段代码做了三件事清洗、情感打分、按阈值映射标签。clean_text必须放在打分之前因为 SnowNLP 对 URL 和 符号非常敏感一个残留的链接会让整句话的情感分被拉向中性。阈值 0.6/0.4 是经验值不是公式。你可以抽样 100 条人工标注数据跑一遍看误判分布再把阈值上下调 0.05 观察效果变化。3.2 自定义词典与停用词让模型更懂舆论场SnowNLP 自带词典偏正式语言面对“绝绝子”“yyds”“踩雷”“避雷”这类网络高频词时分词会碎成一地。常见做法是维护一个自定义词典比如舆情场景下的品牌名、产品名、网络热词用一行代码加载jieba.load_userdict(dict/opinion_words.txt)词典格式每行一个词可以带词频和词性比如踩雷 5 n。加载后jieba在分词时会优先按自定义词切开这直接影响后续情感计算对关键词的命中率。同时要准备一个停用词表把“的、了、嗯、哈、啊”这类无意义词过滤掉否则关键词提取时它们会高频出现干扰热点判断。停用词表不用自己从零写找一个通用中文停用词表再往里面追加你数据里的高频垃圾词即可。我习惯每跑完一批数据用collections.Counter统计词频人工把前 50 个无意义词加进停用词表迭代两轮之后热词质量会明显提升。3.3 从单条打分到热度统计按天聚合负面占比情感分析结果要变成图表必须做聚合。最实用的两个指标是“每日信息总量”和“每日负面占比”。负面占比比单纯的正负面数量更能反映舆情危机总评论量高但负面占比低说明只是话题热闹负面占比连续两天超过 40%才值得预警。import pymysql from datetime import datetime, timedelta conn pymysql.connect( host127.0.0.1, userroot, password123456, dbopinion_db, charsetutf8mb4 ) with conn.cursor() as cur: cur.execute( SELECT DATE(publish_time) AS d, COUNT(*) AS total, SUM(CASE WHEN sentiment_label 负面 THEN 1 ELSE 0 END) AS neg FROM t_comment WHERE publish_time %s GROUP BY DATE(publish_time) ORDER BY d , (datetime.now() - timedelta(days7),)) rows cur.fetchall() conn.close() for row in rows: neg_ratio row[2] / row[1] if row[1] else 0 print(row[0], 总量:, row[1], 负面占比:, f{neg_ratio:.1%})这段代码的关键是DATE(publish_time)做按天分组比在 Python 里遍历格式化时间再聚合要快一个量级。负面占比在 SQL 里用SUM(CASE WHEN ...)计算避免把全表数据拉回内存。这里的丁点边界问题别忽略如果某天总量为 0除零错误会直接中断任务所以neg_ratio那一行要留一手判断。3.4 模型边界与规则纠偏SnowNLP 会在哪里翻车SnowNLP 训练语料偏电商评论面对政治、娱乐、财经等领域的文本准确率会明显下降。最典型的是反讽和玩梗“我服了这操作太秀了”在 SnowNLP 眼里可能是正面因为“秀”被理解为夸赞。这种误判没法只靠调阈值解决常见做法是叠加一个敏感词规则层NEG_RULES [太秀了, 我服了, 避雷, 踩雷, 翻车] POS_RULES [好评, 推荐, 回购, 值得买] def rule_correct(text, predicted_label): if any(w in text for w in NEG_RULES): return 负面 if any(w in text for w in POS_RULES): return 正面 return predicted_label规则层放在模型打分之后属于“人工干预的最后一道闸门”。规则词不宜太多每个方向 20 个以内就够否则就是你自己在“写模型”失去统计意义。答辩时可以这样解释为什么要这一层模型的统计规律跟不上网络语言的演化速度规则层用来覆盖模型在特定表达上的系统性偏差。4. 用 MySQL 把数据存下来建表、连接池与 Flask 接口的落地写法MySQL 在舆情系统里不只是存数据它同时承担了聚合计算的职责。很多项目卡在“分析结果半个月没更新一查是定时任务里 SQL 写错了”。这一节从建表开始给你一套能直接执行的 MySQL 脚本再讲 PyMySQL 连接池怎么配、Flask 接口怎么把聚合结果吐给前端。4.1 建表原始评论表和每日统计表的设计建库第一步就定字符集这一步省掉后面 90% 的乱码问题。如果你还没装 MySQL先按 mysql 安装配置教程装好 8.0 版本安装时选 utf8mb4 字符集再执行下面的脚本CREATE DATABASE IF NOT EXISTS opinion_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE opinion_db; CREATE TABLE t_comment ( id INT AUTO_INCREMENT PRIMARY KEY, source VARCHAR(32) NOT NULL COMMENT 来源news/weibo/forum, url VARCHAR(255) COMMENT 原文链接, title VARCHAR(255) COMMENT 标题, content TEXT NOT NULL COMMENT 正文/评论内容, publish_time DATETIME COMMENT 发布时间, crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 抓取时间, sentiment_score DECIMAL(4,3) COMMENT 情感分 0~1, sentiment_label VARCHAR(8) COMMENT 正面/中性/负面, is_analyzed TINYINT DEFAULT 0 COMMENT 0未分析 1已分析, KEY idx_source_time (source, publish_time), KEY idx_label (sentiment_label) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT原始舆情数据表; CREATE TABLE t_daily_stat ( stat_date DATE PRIMARY KEY, total_count INT DEFAULT 0, pos_count INT DEFAULT 0, neg_count INT DEFAULT 0, mid_count INT DEFAULT 0, neg_ratio DECIMAL(5,4) COMMENT 负面占比, hot_keywords VARCHAR(500) COMMENT 热词 JSON 串 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT每日舆情统计表;建表有几个细节别踩content TEXT别用 VARCHAR新闻正文经常超过 255 字符超了会被静默截断你都不知道采集的数据丢了多少DECIMAL(4,3)存情感分精确到小数点后三位足够idx_source_time这个联合索引专门服务第 3 章那种按来源和时间分组的查询没有它数据量到十万级查询就开始明显变慢。4.2 Python 连接 MySQL连接池参数与事务提交直接pymysql.connect()在 Flask 里用会埋雷Web 请求频繁创建断开连接MySQL 默认的wait_timeout一到期老是报MySQL server has gone away。常见做法是引入DBUtils.PooledDB把连接复用一个池子参数怎么配看下面from dbutils.pooled_db import PooledDB import pymysql pool PooledDB( creatorpymysql, maxconnections10, mincached2, maxcached5, blockingTrue, host127.0.0.1, port3306, userroot, password你的密码, databaseopinion_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, autocommitTrue )maxconnections10对毕设足够了别调太大MySQL 默认最大连接数是 151你开 100 个连接会把数据库拖死。blockingTrue这个参数要特别解释一下当连接池被取空时请求会排队等待而不是报错“Too many connections”这在页面同时刷多个图表接口时是保命设置。autocommitTrue适合查询为主的系统省去手动 commit 的麻烦但如果你有批量写入任务改回autocommitFalse手动控制事务更安全。4.3 Flask 接口规范与前端渲染前后端分离的接口约定Flask 端不需要模板渲染直接返回 JSON 就行。为了不让前端解析时踩坑所有接口统一返回{code, msg, data}结构。注意 Flask 的jsonify默认会把中文转成\uXXXX需要设置app.config[JSON_AS_ASCII] False否则前端拿到的一串转义符没法直接展示。from flask import Flask, jsonify, request from flask_cors import CORS from datetime import datetime app Flask(__name__) app.config[JSON_AS_ASCII] False CORS(app) # 前后端分离调试时让浏览器放行跨域请求 app.route(/api/trend, methods[GET]) def api_trend(): start request.args.get(start, 2024-01-01) end request.args.get(end, datetime.now().strftime(%Y-%m-%d)) with pool.connection() as conn: with conn.cursor() as cur: cur.execute( SELECT stat_date AS date, total_count AS total, neg_count AS neg, neg_ratio AS ratio FROM t_daily_stat WHERE stat_date BETWEEN %s AND %s ORDER BY stat_date , (start, end)) rows cur.fetchall() return jsonify(code0, msgok, datarows) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这里把查询逻辑放在数据库层用t_daily_stat表而不是临时去t_comment聚合原因是t_daily_stat已经由定时任务算好了。接口永远别直接对原始表做聚合否则每次页面刷新都会触发全表扫描前端一卡你就得背“性能优化不足”的锅。前端那边用 jQuery 的$.ajax请求/api/trend拿回data数组直接塞给 ECharts 的series就能画折线图。5. 部署与运行避坑5 个让系统跑不通的典型问题代码写完了很多人以为“跑起来就行”结果从启动到演示能串出五个连环坑每一个都能耗掉半天。这里按“现象 → 原因 → 解决”来写都是我见过——也包括我自己踩过一遍的实操问题。5.1 中文全部乱码控制台和页面全是“锟斤拷”现象MySQL 里存的数据显示正常但 Python 打印出来是乱码网页端也显示问号。原因三处字符集不统一。最常见的是 MySQL 服务端、数据库表、Python 连接参数三方各说各话SQL 语句里的中文直接变成乱码。解决先确认建库时用了utf8mb4然后每个pymysql.connect()都带上charsetutf8mb4。最后在 Flask 里设置app.config[JSON_AS_ASCII] False。这三处对齐后中文从采集到展示都不会再花屏。5.2 MySQL 启动后 Flask 报“Access denied for user”现象接口一请求就 500日志里写Access denied for user rootlocalhost。原因MySQL 8.0 默认密码认证插件是caching_sha2_passwordPyMySQL 老版本不认识或者你 env 里配置的密码跟数据库实际密码不一致。解决先确认密码没问题再升级 PyMySQL 到最新版。还不行就在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;然后刷新权限。这条命令在 MySQL 8.0 安装配置教程里经常被忽略但偏偏是毕设项目的高频卡点。5.3 前端图表白屏后台接口却返回了数据现象浏览器访问接口能看到 JSON 数据但页面上的折线图和饼图区域空白控制台报错Cannot read properties of undefined。原因前端拿到的数据里字段名和后端对不上。Flask 返回的是stat_date、total_count前端代码里写的是date、total运行时读不到值图表自然画不出来。解决先在浏览器控制台手动请求一次接口把返回 JSON 的结构展开跟前端series里的字段名逐一比对。前后端分离项目实战里这类问题十有八九是字段命名不一致不是图表配置写错。养成“先看响应体再调图表参数”的习惯。5.4 情感分析全是中性正面负面样本寥寥无几现象跑了 1000 条数据情感分布变成 95% 中性5% 正面0 负面。原因清洗函数把有效文本误删了或者clean_text里正则把中文标点去掉后又把短文本整体判为中性。更隐蔽的原因是对舆情热词不识 ——文本只有一个词的短句子较多?flowers分不清。解决把清洗后的文本直接打印到日志里肉眼检查是否是“清洗过度”。再把第 3 章的rule_correct规则层加上用NEG_RULES强制救回一批误判的中性样本。记住一个原则情感分析是“模型 规则”的混合体单靠模型扛不住短文本。5.5 采集脚本一跑就断网站把你 IP 封了现象爬虫跑 5 分钟就超时抓回来都是空列表后来直接连不上目标网站。原因采集频率太高没有设置限速也没有带完整的请求头。目标网站的反爬机制把你当成了攻击流量。解决在采集循环里加随机延时比如time.sleep(random.uniform(1, 3))请求头补上User-Agent和Referer。这里说一句踩坑的话舆情采集的重点是“可持续性”不是“速度”。对毕设来说每天抓个几百条足够撑起演示图表完全不需要跑满带宽。6. 验证模型准不准用混淆矩阵和人工标注给舆情分析打分系统跑通了图表能动了你在答辩前还需要做一件事证明你的分析结果是可信的。很多人忽略这一步结果评审老师一句“你怎么知道它判断得对”直接问倒。验证方法不复杂人工标注 200 条数据和模型输出做对比算精确率、召回率、F1 三项指标。import pandas as pd # 人工标注结果1负面0非负面 df pd.DataFrame({ manual: [1, 0, 1, 1, 0, 0, 1], model: [1, 1, 1, 0, 0, 0, 1] }) y_true df[manual] y_pred df[model] tp ((y_true 1) (y_pred 1)).sum() # 模型判负面人工也判负面 fp ((y_true 0) (y_pred 1)).sum() # 模型判负面人工没判 fn ((y_true 1) (y_pred 0)).sum() # 人工判负面模型没判 precision tp / (tp fp) recall tp / (tp fn) f1 2 * precision * recall / (precision recall) print(f负面类精确率{precision:.2f} 召回率{recall:.2f} F1{f1:.2f})注意这里把二分类聚焦在“负面”上因为从舆情预警角度漏掉一条真正的负面评论比误判一条中性评论代价大得多。如果召回率低优先扩充NEG_RULES规则词如果精确率低说明规则覆盖太激进把“无害表达”也划成了负面需要删掉部分规则词。按这个思路迭代两轮F1 通常能从 0.6 提到 0.8 左右。迭代完之后还有一个被重复使用的进阶技巧是给 SnowNLP 做增量训练。SnowNLP 自带的train()方法允许你用它默认的贝叶斯结构在你的人工标注数据上继续训练。方法不复杂准备一份neg.txt和pos.txt每行一条文本然后调用SnowNLP训练接口最后把新模型放到sentiment.marshal里。这个技巧是有明显收益的升级方向——网上的教程很少展开讲但你在答辩时展示“我在开源模型基础上用领域数据做了增量训练”会比“我调了一个第三方库”更加分。最后说一个我的个人教训做舆情系统最容易忽视的是“结果可复现”。我一开始直接在原始表上反复覆盖分析结果调一次参数要重新抓一遍数据浪费了大半天。后来老老实实把原始表、结果表、规则表分开每次调参只改分析层数据回溯全靠is_analyzed字段控制整个系统才变成可维护的状态。希望你拿到源码后第一件事也别急着跑页面先把数据库三张表建好把数据流转顺序捋清楚再动代码。这样做你会发现后面的每个坑都在预期之内。希望这篇文章能帮你少走这几步弯路。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进