ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于Python的舆情分析系统设计与实现:Flask框架与情感分析实战

基于Python的舆情分析系统设计与实现:Flask框架与情感分析实战

简介:一套基于Python的网络舆情分析系统,面向舆情监控管理人员与高校相关专业学生,实现微博、微信公众号、贴吧等平台言论的采集、分析与可视化展示,协助管理人员快速排查本地负面舆情。系统允许多个用户同时使用,管理员拥有唯一管理权限,可维护用户信息并统计言论,通过饼状统计图直观呈现数据,普通用户也可自行修改密码等个人信息。

资源包共292个文件,含42个Python源码与35个pyc,34个前端JS、15个CSS、12个HTML页面,75个GIF动图以及36张JPG/PNG图片,同时提供SQL、CSV数据文件和字体配置等辅助内容,包体约115.16MB,结构清晰,便于部署与二次开发。目前已有150人学习下载。

除核心分析代码外,资源还附带界面样式、演示图片、数据集和数据库脚本,可支撑完整项目运行。读者可参考其多用户权限控制、言论统计展示逻辑与饼状图可视化实现,适用于课程设计、毕业设计或小型舆情监测系统搭建。

1. 舆情分析系统拆解:这套Python毕业设计到底能干什么

拿到这套基于Python的网络舆情分析系统源码包时,我第一反应是:这不就是典型的课设架子吗?但仔细翻完里面的代码和前端静态文件,发现它把“多用户登录 + 舆情言论管理 + 饼状统计图”这条线串得很完整,不像网上那些只贴个登录页的残废Demo。系统面向的是负责舆情监控的管理人员,支持多个普通用户同时使用,但管理员只有一个。核心功能是让用户把微博、公众号、贴吧等渠道的言论录进系统,自动做正负向判断,再通过列表和饼图把舆论倾向呈现出来。适合正在做Python Web课设、需要快速搭一个能演示的管理系统,或者想在公司内部做一个小型舆论监控工具的人。下面我把拆解过程和复现步骤都写出来,代码怎么改、坑在哪,一次说清。

2. 系统架构与数据流:从一条评论到饼状统计图要过几道关

2.1 用户角色与权限边界

这个系统的权限模型很典型:一张用户表里用字段区分角色,普通用户能录入言论、查看统计、改自己的密码;管理员除了这些,还能看到所有用户列表,并且能修改或删除其他用户。源码里对应的权限判断通常在视图函数开头,像下面这样:

def admin_required(view_func): @wraps(view_func) def wrapper(*args, **kwargs): if session.get('is_admin') != 1: return jsonify({'code': 403, 'msg': '只有管理员可操作'}) return view_func(*args, **kwargs) return wrapper

逻辑说明:这里用session['is_admin']区分身份,登录时写入这个值。装饰器包在需要管理员权限的路由上,一旦不带管理员标记就直接返回403。参数说明:is_admin字段在用户表里是整型,1代表管理员,0代表普通用户,默认注册用户都是0。

要注意一个细节:这个系统没有独立的“审核”角色,所以舆情数据一旦录入,谁录入谁负责。我看了视图代码,录入言论时会把user_id也存上,这为后面做“按用户筛选”留了口子。如果你要扩展成多级权限,建议在用户表加一个role_id,比现在硬编码is_admin更灵活。

2.2 言论数据是怎么进库的

源码里没有直接写爬虫,而是提供了表单录入和批量导入两条路。表单录入很简单,前端提交言论文本,后端保存到评论表。批量导入是我觉得最有用的部分,它接受CSV文件,每行一条言论,默认按逗号分隔。核心函数我简化后是这样的:

def import_comments(file_path): rows = [] with open(file_path, 'r', encoding='utf-8') as f: reader = csv.reader(f) header = next(reader, None) for row in reader: if len(row) < 2: continue rows.append({ 'content': row[0], 'source': row[1] if len(row) > 1 else 'unknown', 'created_at': datetime.now() }) db.session.bulk_insert_mappings(Comment, rows) db.session.commit() return len(rows)

逻辑说明:这段代码先跳过表头,然后逐行读取,第一列是言论内容,第二列是来源渠道,比如微博还是微信。使用bulk_insert_mappings一次性插入,比循环add快很多。参数说明:source字段建议填入weibo、wechat、tieba,后续做渠道维度统计时会用到。如果你只有文本没有来源,程序默认填unknown,饼图统计时这些会归入“其他”。

实际我复现时发现一个容易忽略的点:CSV文件如果是从Excel直接另存的,大概率是GBK编码,而程序默认用utf-8打开,会直接报UnicodeDecodeError。源码包里没做异常处理,我后面在避坑章里单独说。

2.3 饼状统计图的数据是怎么聚出来的

饼图数据不是前端现算的,是后端通过SQL聚合后返回JSON。评论表里有一个sentiment字段,录入时由规则分类器判断成positive、negative、neutral三档。统计接口大致长这样:

@app.route('/api/stats') def stats(): data = db.session.query( Comment.sentiment, db.func.count(Comment.id) ).group_by(Comment.sentiment).all() result = { 'categories': ['正面', '负面', '中性'], 'series': [0, 0, 0] } for sentiment, count in data: if sentiment == 'positive': result['series'][0] = count elif sentiment == 'negative': result['series'][1] = count else: result['series'][2] = count return jsonify(result)

逻辑说明:group_by(Comment.sentiment)按情感字段分组,再用count(Comment.id)统计每类数量。后端把三类的数量填进固定位置的数组里,前端拿到这个JSON直接渲染饼图。参数说明:categories与series必须一一对应,顺序不能乱,否则饼图和图例会错位。

我在调试时习惯先在浏览器访问/api/stats,如果返回{"categories":["正面","负面","中性"],"series":[12,3,5]}这种结构,说明聚合没问题,接着查前端。很多同学一上来就改前端,结果发现数据接口本身就是错的,白费功夫。

3. 本地跑起来:环境搭建、数据库初始化与启动命令

3.1 Python版本与依赖清单

这套系统源码包我解压后主要看到三部分:后端Python文件、templates模板目录、static静态目录(里面是layui.css、admin.css、layer.css等)。后端不是特别重,用Flask写的,依赖很常规。我把习惯用的依赖组合列一下,如果你手上的requirements.txt缺失,按这个补:

依赖包版本建议用途
Flask2.xWeb框架
Flask-SQLAlchemy3.xORM,操作SQLite
Werkzeug2.x密码哈希与校验
pandas1.5+处理导入的CSV

安装命令我一般指定国内镜像源,不然速度感人:

pip install flask flask-sqlalchemy werkzeug pandas -i https://pypi.tuna.tsinghua.edu.cn/simple

逻辑说明:Werkzeug是Flask的底层WSGI库,直接用它的generate_password_hash做密码加密,不要自己存明文。pandas只在导入CSV时会用到,如果你只做单条录入,不装也能跑,但批量导入功能会挂。

3.2 数据库表结构与初始化账号

数据库默认放在instance/目录下,文件名是comments.db。首次运行前要先建表、插入管理员账号。源码里通常有一段初始化脚本,我把它抽出来做成init_db.py:

from app import db, create_app from werkzeug.security import generate_password_hash from models import User app = create_app() with app.app_context(): db.create_all() if not User.query.filter_by(username='admin').first(): admin = User( username='admin', password_hash=generate_password_hash('admin123'), is_admin=1 ) db.session.add(admin) db.session.commit() print('管理员创建成功')

逻辑说明:create_all()只会建表,不会重复建已存在的表。generate_password_hash用默认的pbkdf2算法生成哈希,登录时用check_password_hash校验。参数说明:账号名admin、密码admin123是源码里默认的,上线必须改,这点我会在避坑章强调。

我执行时遇到最常见的问题是app模块和db变量的导入路径不对。这套源码的工程结构是app/__init__.py里写create_app(),models.py和app.py平级,所以from app import db没问题。如果你的包结构不同,注意改成相对导入。

3.3 启动服务与登录验证

初始化完数据库就可以启动:

python app.py

默认监听127.0.0.1:5000,控制台看到Running on http://127.0.0.1:5000就说明起来了。浏览器打开/login页面,用刚才创建的admin / admin123登录。登录逻辑我把关键部分贴出来:

@app.route('/login', methods=['POST']) def login(): username = request.form.get('username') password = request.form.get('password') user = User.query.filter_by(username=username).first() if user and check_password_hash(user.password_hash, password): session['user_id'] = user.id session['username'] = user.username session['is_admin'] = user.is_admin return redirect(url_for('index')) return render_template('login.html', error='账号或密码错误')

逻辑说明:先按用户名查用户,再用check_password_hash校验哈希值。登录成功后把user_id、username、is_admin写进session,后续视图函数判断权限时直接读session。参数说明:check_password_hash第一个参数是库里的哈希,第二个是用户输入的明文,顺序写反会一直登录失败。

这里有个坑:Flask的session默认用密钥签名,但只存在客户端cookie里,如果不设置SECRET_KEY,每次重启服务后session会失效。我建议在config.py里写上:

class Config: SECRET_KEY = 'your-secret-key-here'

4. 核心代码走读:舆情分类、统计接口与用户管理

4.1 舆情正负向判别的规则实现

源码里没有用训练好的模型,而是基于情感词典的规则匹配。词典放在data/emotion_dict.txt,每行一个词,前面是情感极性,后面是词语,中间用空格隔开。分类函数如下:

def classify_sentiment(text): positive_words = ['好', '赞', '优秀', '给力', '支持'] negative_words = ['差', '垃圾', '愤怒', '抵制', '难受'] text = text.strip() pos_count = sum(1 for w in positive_words if w in text) neg_count = sum(1 for w in negative_words if w in text) if pos_count > neg_count: return 'positive' if neg_count > pos_count: return 'negative' return 'neutral'

逻辑说明:这个分类器非常简单,遍历正负向词表,统计命中次数,谁多判谁。相等或都没命中就返回neutral。参数说明:positive_words和negative_words实际会从外部词典文件加载,这里写死是为了演示。你可以改成从数据库读取词典,便于后台动态维护。

这种规则放在课设里足够应付演示,但有一个明显问题:无法处理否定词,比如“不好”里的“好”会被命中成正面。我后面会讲怎么升级。

4.2 饼图数据接口与前端渲染

前端用了ECharts,饼图绘制代码在templates/index.html里:

$.get('/api/stats', function(res) { var chart = echarts.init(document.getElementById('pie')); chart.setOption({ series: [{ type: 'pie', radius: '60%', data: [ { name: res.categories[0], value: res.series[0] }, { name: res.categories[1], value: res.series[1] }, { name: res.categories[2], value: res.series[2] } ] }] }); });

逻辑说明:通过jQuery的$.get请求后端/api/stats接口,拿到JSON后填充到ECharts的data数组里。radius: '60%'控制饼图半径,数字越大图越大。参数说明:res.series的顺序必须和后端返回的顺序对应,因为前端直接按索引取。

如果你要改成柱状图,只需要把type: 'pie'改成type: 'bar',再把data换成xAxis.data和series.data。我一般会加一个点击事件,点击某个扇区后跳到该情感类型的言论列表,这样演示效果会好很多。

4.3 用户管理的权限校验与操作闭环

管理员对用户的增删改,源码做成了三个独立路由。删除用户的逻辑如下:

@app.route('/admin/user/delete/<int:user_id>', methods=['POST']) @admin_required def delete_user(user_id): if user_id == session.get('user_id'): return jsonify({'code': 400, 'msg': '不能删除自己'}) user = User.query.get_or_404(user_id) db.session.delete(user) db.session.commit() return jsonify({'code': 200, 'msg': '删除成功'})

逻辑说明:先调用@admin_required装饰器拦截非管理员请求。然后判断如果删除的是自己,直接拒绝。get_or_404在用户不存在时自动返回404。参数说明:user_id是URL路径参数,前端按钮会构造类似/admin/user/delete/3的请求。

修改用户的功能类似,但要注意密码字段不能通过PUT接口直接明文传,应该单独处理。源码里的做法是:如果提交了新密码,才更新哈希字段;否则保持原密码不变。我复现时发现有人直接修改了原文,导致密码变成明文存储,这是很不安全的习惯。

5. 避坑指南:部署与二次开发最常见的5个问题

5.1 静态文件加载不出来

现象:登录页面打开的没有CSS样式,布局完全错乱,控制台刷了一堆404。

原因:Flask的静态文件路由没有配置对。很多课设代码把static目录放在应用外层,但app = Flask(__name__)默认只认应用同级目录下的static。

解决:在创建Flask实例时强制指定静态目录路径:

app = Flask(__name__, static_folder='../static', template_folder='../templates')

注意:static_folder参数是相对应用文件的位置,我一般写成绝对路径更省事。如果还不行,直接在浏览器访问http://127.0.0.1:5000/static/layui.css试试,能看到CSS内容说明路径对了。

5.2 中文乱码与编码问题

现象:导入CSV后,页面显示言论全是乱码,或者控制台报UnicodeDecodeError。

原因:CSV文件是Excel保存的GBK编码,而Python默认用utf-8解码。

解决:在import_comments函数里改成自动识别编码。我习惯用chardet库检测:

import chardet def detect_encoding(path): with open(path, 'rb') as f: data = f.read(10000) return chardet.detect(data)['encoding']

然后打开文件时encoding=detect_encoding(file_path)。如果不想装额外库,就把异常捕获后用encoding='gbk'重试一次。从那以后我每次做数据导入,第一件事就是检查文件编码,再也不想被乱码折腾。

5.3 饼图不刷新

现象:新增一条言论后,回到首页饼图数量还是旧的,必须刷新整个页面才更新。

原因:前端用$.get只请求了一次数据,后续没有重新触发。

解决:在录入言论成功后,手动调用一次loadStats()函数。我一般把统计加载封装成一个函数:

function loadStats() { $.get('/api/stats', function(res) { /* 更新图表 */ }); }

提交表单成功后loadStats()。如果还不行,检查浏览器缓存,给请求地址加个时间戳:

$.get('/api/stats?_=' + Date.now())

5.4 多用户同时操作时管理员被覆盖

现象:两个普通用户同时登录后,后登录的人把先登录的人的session顶掉了,管理员状态也丢了。

原因:Flask默认的session机制依赖cookie,用户A在浏览器A登录,用户B在浏览器B登录,互相不影响。但如果你用同一个浏览器开两个标签页,session是共享的,后登录的人会覆盖前一个人的状态。

解决:这不是代码bug,是session机制的特性。测试时务必用不同的浏览器或者隐身窗口。如果你希望支持同浏览器多账号切换,建议改成token方案,但课设阶段没必要。我一般会跟同学强调:不要用同一个浏览器试多用户,很容易误判成权限Bug。

5.5 管理员账号被锁死

现象:管理员不小心把自己的账号删了,或者忘了密码,系统里没有第二个管理员,后台进不去。

原因:用户管理接口允许删除任意用户,包括管理员自己。虽然代码里加了“不能删除自己”的判断,但如果管理员用的不是自己账号,或者被另一个管理员删了,就会出问题。

解决:源码里的判断只挡了当前登录用户,没挡其他管理员。我建议改成任何管理员都不能被删除,禁止删除is_admin == 1的用户:

if user.is_admin: return jsonify({'code': 400, 'msg': '管理员不能删除'})

同时,忘了密码时可以直接进数据库重置。SQLite操作命令:

sqlite3 instance/comments.db update user set password_hash='...' where username='admin';

哈希值可以用python -c "from werkzeug.security import generate_password_hash; print(generate_password_hash('newpass'))"生成。这也是我常说的后悔药:数据库直接改是最后的手段,平时还是要做好备份。

6. 进阶玩法:把规则分类升级成轻量情感分析模型

这套系统的规则分类器在实际文本上表现太糙,“不好”会被判成正面,“太棒了”反而可能因为没命中任何词变成中性。我建议把分类模块替换成朴素贝叶斯模型,用sklearn的Pipeline做特征工程。改动不大,但效果提升明显。

from sklearn.feature_extraction.text import CountVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline model = Pipeline([ ('vect', CountVectorizer(token_pattern=r'\w+', ngram_range=(1, 2))), ('clf', MultinomialNB()) ])

逻辑说明:CountVectorizer把文本拆成词和词对,ngram_range=(1,2)让“不好”这种双字词成为一个特征,而不是拆成“不”和“好”。MultinomialNB适合离散特征的文本分类。参数说明:token_pattern按中文常见写法用\w+匹配连续字符,如果你要更精细的分词,可以换成jieba。

训练时需要用带标注的历史数据。你可以把系统里已经人工修正过情感的言论导出成训练集,结构是两列:content,sentiment。然后用pandas读取并训练:

train_data = pd.read_csv('labeled_comments.csv') X = train_data['content'] y = train_data['sentiment'] model.fit(X, y)

训练完保存模型文件,以后分类时直接model.predict([text])[0]。我实际测下来,对于短评文本,朴素贝叶斯比原规则的准确率能提升30%以上,尤其是能正确识别“有点差”“不太满意”这类带有程度词的表达。如果你不想引入模型文件,也可以把训练好的词频放进数据库,在规则分类器上叠加词频权重,这算是折中方案。

替换时要注意:原来的classify_sentiment函数返回的是'positive'/'negative'/'neutral'字符串,新模型正好也返回这三个值,所以接口层不用改,饼图和列表都能正常跑。我就是这样先把模型离线训练好,再在系统里加载,前后端完全不动。

从那以后我每次拿到这类课设源码,都会先翻一下分类模块,只要看到硬编码关键词表,就默认它处理不了否定句,升级成模型前先备份原规则,方便对比效果。这套系统给我的整体印象是骨架完整、边界清晰,适合做二次开发的底子。希望帮到你。

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

RELATED READING

延伸阅读

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