简介:一套基于Python与MySQL数据库开发的网络舆情分析系统毕业论文资源,面向高校计算机、网络管理相关专业学生,尤其适合正在筹备毕业设计或课题研究的读者。系统围绕网络舆情管理的实际需求,使用Python语言完成业务逻辑,采用MySQL数据库存储数据,设计并实现了言论分析、言论管理、用户管理等多种功能,能够辅助网络管理部门快速定位本地相关舆情或负面评论,平衡网民隐私权与监管效率。压缩包内共1份docx文档,大小约1.73MB,文档包含独创性声明、授权书、中英文摘要、目录、正文及参考文献等完整论文组成部分;正文部分从引言与背景介绍入手,详细阐述了系统设计思路、开发环境搭建和功能模块实现过程。目前已有1262人浏览学习,这份资料可以帮助读者把握舆情分析系统的设计框架与开发流程,学习Python+MySQL的项目落地方式,也可直接参考其章节结构和排版规范,为撰写同类毕业论文节省大量时间。
1. 基于 Python 的网络舆情分析系统:从文本录入到数据看板的完整后台闭环
“基于 Python 的网络舆情分析系统”这个名字,容易让人以为它是个爬虫项目,或者只是一个情感分类算法 Demo。把这份包含源码工程和论文文档的资料完整跑一遍之后,你会发现它真正的分量是一套完整的 Web 后台:用户登录注册、文本输入与历史记录、DNN 与 BERT 双模型推理、负面预警、文本管理、用户权限、柱状图与饼图对比分析,全部串在一条业务链上。它面向高校或属地舆情管理工作人员,用 MySQL 落库、Python 做业务调度,从“某个人输入一句话”到“管理员看到分析结果并标记处理状态”闭环完成。正在学 Django + MySQL 管理系统的开发者,值得照着拆一遍。
2. 技术选型与整体架构:Django + MySQL 的 B/S 骨架怎么撑起文本分析全流程
拿到这份资料,我建议你先读论文正文里的技术选型和需求分析,再动手碰代码。论文的 2.1 到 2.6 节把架构、语言、前端、数据库、算法和开发环境全讲清楚了,照着这些描述去反推工程落地方式,比直接翻代码高效得多。这一章我把技术栈的来龙去脉和数据库设计拆开讲,搞清楚这部分,后面看核心代码就不会迷路。
2.1 技术栈判断:从开发工具与功能描述反推框架组合
论文里提到的开发工具是 Sublime Text + Navicat for MySQL,前端用 HTML5 + CSS + JavaScript,后端语言是 Python,数据库是 MySQL,架构是 B/S。这套组合在校园项目里几乎有一个默认映射:后端框架用 Django。理由很直接,Django 自带 ORM、模板渲染、表单验证和后台管理,恰好覆盖论文 3.2 节里列出的“用户注册登录”“文本数据增删改查”“文本分析”“用户管理”这些需求。原文 2.6 节也明确提到了“进行 Python 和 Django 编程”,所以这个判断基本没有悬念。
B/S 架构的选择本身也合理。传统的 C/S 架构需要安装客户端、升级要重新分发;B/S 只要浏览器能打开,服务器部署一次,所有用户访问同一个地址就行。对于舆情管理这类需要多人同时使用的后台系统,浏览器端零安装是最大的优势。前端虽然用了 HTML5,但整个系统不是静态页面,所有页面数据都来自 Django 视图渲染后的模板变量或接口返回,HTML 只负责结构和布局,CSS 负责样式,JavaScript 负责表单交互和图表绘制。
一个常见的最小依赖清单长这样,如果你要在本地复现环境,按这个方向装就行:
# requirements.txt(按本系统所需能力推断的常见组合) Django mysqlclient numpy scikit-learn bert-tensorflow这里不写死版本号,是因为毕业设计项目的依赖版本往往以“当时能装上”为准。装依赖时如果遇到 mysqlclient 编译失败,在 Windows 上可以换成PyMySQL,然后在 Django 的__init__.py里加一行pymysql.install_as_MySQLdb(),这是 Python + MySQL 项目里非常经典的替代方案。
2.2 总体功能结构与工程目录:从首页到用户管理的请求落点
论文 4.1 节的系统结构图把功能划分得很清楚:用户登录、个人信息管理、文本分析、文本管理、对比分析和用户管理。这里的用户分两类,普通用户负责文本分析和文本管理,管理员额外拥有用户管理权限。普通用户可以有很多个,管理员只有一个,这个权限模型在后端实现时非常像 Django 自带的is_staff/is_superuser字段。
按 Django 的常规工程布局,这套系统落地后大概率是这样一个目录结构:
opinion_system/ ├── manage.py ├── opinion_system/ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── app/ │ ├── models.py │ ├── views.py │ ├── urls.py │ ├── forms.py │ ├── algorithms/ │ │ ├── dnn_predict.py │ │ └── bert_predict.py │ └── migrations/ ├── templates/ └── static/重点在app/algorithms/目录。把 DNN 和 BERT 的预测逻辑独立成模块,而不是混在视图函数里,是这套源码值得借鉴的一个设计。视图层只负责拿文本、调算法、存结果,算法层只负责接收文本、返回标签和分数,两边通过一个简单的函数约定通信。这样以后想换算法、加算法,改动范围被限制在algorithms/内部,不会把业务代码搅乱。
templates/下通常是index.html、login.html、register.html、analyze.html、text_manage.html、user_manage.html这些页面。Django 的模板继承在这里很实用,一个base.html把导航栏和底部定义好,其他页面只需要重写 content 块,前后端联调时期能省不少重复劳动。
2.3 数据库设计:五张表如何支撑舆情分析与言论管理
论文 4.2 节列出了系统需要的表:用户信息表、文本分类及文本信息表、文本管理信息表、站内新闻表。这里有个细节值得注意——论文特意把“文本分类”和“文本信息”拆成两张表,目的是用分类表管理言论种类,用文本信息表存具体的文本内容,避免把分类名称重复存在每一条文本记录里。这是很典型的表设计思考:分类是字典数据,文本是业务数据,两者分开维护,后续加新分类不需要动文本表结构。
按常规做法,这五张表的字段设计大概长这样:
| 表名 | 职责 | 关键字段 |
|---|---|---|
| user_info | 用户注册信息与登录状态 | id、username、password、phone、register_time、last_login_time |
| text_category | 文本分类字典 | id、category_name |
| text_info | 爬取或手动输入的文本内容 | id、user_id、category_id、content、sentiment_score、analysis_result、create_time |
| text_manage_info | 文本管理状态流转 | id、text_id、manage_status、manager_id、handle_time、remark |
| news_info | 站内公告新闻 | id、title、content、publish_time |
SQL 落地时,我一般会这样建表:
-- 用户信息表 CREATE TABLE user_info ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL, phone VARCHAR(20), register_time DATETIME DEFAULT CURRENT_TIMESTAMP, last_login_time DATETIME NULL ); -- 文本分类表 CREATE TABLE text_category ( id INT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(50) NOT NULL ); -- 文本信息表 CREATE TABLE text_info ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, category_id INT, content TEXT NOT NULL, sentiment_score FLOAT, analysis_result VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user_info(id), FOREIGN KEY (category_id) REFERENCES text_category(id) ); -- 文本管理信息表 CREATE TABLE text_manage_info ( id INT AUTO_INCREMENT PRIMARY KEY, text_id INT NOT NULL, manage_status TINYINT DEFAULT 0, manager_id INT, handle_time DATETIME, remark VARCHAR(255) ); -- 站内新闻表 CREATE TABLE news_info ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, content TEXT, publish_time DATETIME DEFAULT CURRENT_TIMESTAMP );password字段用 VARCHAR(128) 是因为 Django 的make_password生成的哈希串就是这个长度,如果你打算直接用 Django 的auth模块做认证,字段长度必须留够。text_info的sentiment_score我用 FLOAT 类型,因为模型输出的概率分数是个 0 到 1 之间的小数。manage_status用 TINYINT,0 表示未处理,1 表示已处理,2 表示忽略,比直接用 VARCHAR 存中文状态更节省空间,也方便写查询条件。
这套表结构覆盖了系统所有核心流程:用户在user_info里认证,输入的文本进text_info,分类挂在text_category,管理动作记录在text_manage_info,首页公告读news_info。五张表没有一张是多余的,也没有明显的设计缺陷。
3. 核心模块拆解:一段文本从输入到返回的完整分析链路
这一章是整套源码最值得看的部分。论文第 5 章虽然讲到了文本分析界面和对比分析界面,但没展示具体代码,所以我按照项目场景下合格开发的惯用做法,把登录、文本分析、对比分析三条链路的关键代码补全并拆开讲。你带着“一个请求从进入到返回”这条主线去看源码,会比按文件顺序读清晰得多。
3.1 登录注册与权限判定:谁可以发起分析
系统不是打开就能用的,未注册用户只能看首页公告和站内新闻,注册并登录后才能进入文本分析和文本管理页面。这个限制在 Django 里实现得比较简单,登录视图用内置的authenticate和login,页面跳转用login_required装饰器拦截未登录请求。
核心登录逻辑大致是这样:
# app/views.py from django.contrib.auth import authenticate, login from django.shortcuts import render, redirect def user_login(request): if request.method == 'POST': username = request.POST.get('username', '') password = request.POST.get('password', '') user = authenticate(username=username, password=password) if user is not None: login(request, user) return redirect('index') return render(request, 'login.html', {'error': '用户名或密码错误'}) return render(request, 'login.html')代码逻辑其实只有三件事:从 POST 请求里取出用户名和密码,调用authenticate校验,校验通过后用login写入会话。redirect('index')里的index是 URL 配置里给首页路由起的 name,这样写的好处是以后路由地址变了,视图代码不用改。
authenticate是 Django 内置的认证方法,它会去user_info表对应的模型里比对用户名和密码哈希,比对成功返回用户对象,失败返回 None。这里需要注意,密码在入库前必须用make_password做哈希处理,绝不能存明文。注册视图里常见的写法是UserInfo.objects.create_user(username=..., password=...),create_user会自动完成哈希,比手动create安全得多。
权限判定方面,管理员判断用is_superuser,普通用户的管辖范围判断用request.user和模型里的外键关联。比如“用户只能看到自己提交的文本”这个需求,查询时加一个filter(user=request.user)就能实现,这也是后面文本管理模块的基本原则。
3.2 文本分析主流程:DNN 与 BERT 双模型如何协同
文本分析是本系统的核心功能,论文 5.3 节说得很清楚:用户进入页面后可以查看历史输入记录,也可以在文本框中输入新内容,系统会对内容做数据分析并给出负面信息预警。我一般会把视图拆成两个部分,一部分处理 POST 请求执行分析,另一部分处理 GET 请求展示历史记录。
# app/views.py from .models import TextInfo from .algorithms.dnn_predict import dnn_predict from .algorithms.bert_predict import bert_predict def text_analyze(request): if request.method == 'POST': text = request.POST.get('content', '').strip() if not text: return render(request, 'analyze.html', {'error': '输入内容不能为空'}) # 调用双模型分析 label, score = double_model_predict(text) TextInfo.objects.create( user=request.user, content=text, sentiment_score=score, analysis_result=label, ) return render(request, 'analyze.html', {'result': label, 'score': score}) history = TextInfo.objects.filter(user=request.user).order_by('-create_time')[:20] return render(request, 'analyze.html', {'history': history})其中double_model_predict是算法的统一入口,DNN 和 BERT 的结果在这里汇合:
# app/algorithms/__init__.py(或视图内联函数) def text_clean(text): # 去除首尾空白、统一全半角 return ' '.join(text.split()) def double_model_predict(text): text = text_clean(text) # DNN 模型推理,速度快但精度一般 dnn_label, dnn_prob = dnn_predict(text) # BERT 模型推理,速度慢但更准 bert_label, bert_prob = bert_predict(text) # 冲突时优先采用 BERT 结果,分数取两个模型的均值 label = bert_label if bert_label else dnn_label score = round((dnn_prob + bert_prob) / 2.0, 4) return label, score逻辑说明:DNN 和 BERT 分别对同一段文本输出一个情感标签(比如“负面/正面/中性”)和一个概率分数。两个模型结论不一致时,代码里默认信任 BERT,因为 BERT 在 2018 年提出后就在多项自然语言处理任务上刷新了成绩,论文原文也强调“使用两种算法目的就是将舆情分析做到最准”,所以让更准的模型仲裁是合理选择。分数取均值则是为了让结果更平滑,避免单模型的一次偶发失误把分拉偏。
参数说明:dnn_predict(text)和bert_predict(text)的返回值约定是(label, probability)元组,probability是一个 0 到 1 之间的浮点数。以“负面概率”口径为例,分数越高代表越可能为负面,这套系统里把 0.6 以上视为负面内容并给出预警提示,页面标红展示;0.4 到 0.6 之间是中性,0.4 以下为正面。阈值可以在settings.py里定义成常量NEGATIVE_THRESHOLD = 0.6,这样以后调优阈值时不用去视图里找数字。
历史记录取最近 20 条,是因为页面空间有限,且舆情管理关注的是最近一段时间的言论,太老的数据意义不大。如果你希望支持时间范围筛选,把order_by换成filter(create_time__range=(start, end))即可。
3.3 负面预警与对比分析:统计结果怎么变成柱状图和饼图
对比分析模块是论文 5.6 节提到的功能,用户可以通过柱状图和饼图对比 DNN 与 BERT 两种算法的分析结果。这里的核心逻辑不是图表本身,而是“按算法口径分别聚合统计数据”。
一个典型的聚合查询是这样:
# app/views.py from django.db.models import Count def compare_view(request): # 按文本类别统计数量,用于饼图 category_stats = TextInfo.objects.values('category__category_name') \ .annotate(total=Count('id')).order_by('-total') # 按文本结果(负面/正面/中性)统计,用于柱状图 result_stats = TextInfo.objects.values('analysis_result') \ .annotate(total=Count('id')).order_by('analysis_result') return render(request, 'compare.html', { 'category_stats': list(category_stats), 'result_stats': list(result_stats), })values('category__category_name')里的双下划线是 Django ORM 跨表查询的写法,意思是“取关联分类表的 category_name 字段”,最终生成的 SQL 是LEFT JOIN text_category ON ... GROUP BY category_name。annotate(total=Count('id'))等价于 SQL 里的COUNT(id),结果集会多出一个 total 字段。两个查询返回的都是字典列表,可以直接 JSON 序列化丢给前端图表库。
如果要做“双模型对比”,你就得在text_info表里多存一个字段来标记这条结果来自哪个模型,比如algorithm = 'dnn' / 'bert',然后查询时按algorithm字段分组,就能分别统计两个模型的正负面数量。论文里对比分析展示的是两种算法的效果差异,所以在实际实现中大概率采用了这个方案。
前端图表建议用 ECharts 或 Highcharts 渲染,饼图的 data 值直接取category_stats列表,柱状图的 X 轴取result_stats里的 result 值,Y 轴取 total 值。图表数据来自数据库聚合结果,这就保证了任何一次新的文本分析完成后,刷新页面图表就会自动更新。
4. 功能实现与数据流:首页、文本管理和用户管理的具体落法
核心分析链路讲完之后,剩下的功能模块更偏业务。论文 5.1 到 5.7 节分别描述了首页展示、登录注册、文本分析、文本管理、个人信息查看、对比分析和用户管理,这一章我把非分析类功能的数据流理一遍,让你知道每个页面的数据是从哪些表里查出来的。
4.1 首页信息聚合:公告、统计卡与近期舆情怎么拼出来
首页采用上中下结构,头部是导航栏,中部左侧显示当前用户信息和日期天气,右侧显示系统总发言数、总用户数等统计数据,同时展示站内公告。这个页面的数据来源很分散——用户信息从request.user拿,统计数字从text_info和user_info表聚合,公告从news_info表取,所以视图里要一次性把所有上下文变量准备好。
# app/views.py from datetime import date from django.shortcuts import render from .models import TextInfo, UserInfo, NewsInfo def index(request): context = { 'total_text_count': TextInfo.objects.count(), 'total_user_count': UserInfo.objects.count(), 'today_text_count': TextInfo.objects.filter(create_time__date=date.today()).count(), 'news_list': NewsInfo.objects.order_by('-publish_time')[:5], 'current_user': request.user, } return render(request, 'index.html', context)total_text_count是全库文本总数,对应首页右侧的“总发言数”;today_text_count是当天新增文本数,这个字段比总数更能反映舆情活跃度。news_list取最近 5 条公告,order_by('-publish_time')是倒序排列,[:5]在 Django ORM 里会生成LIMIT 5,不会全表加载。当前用户直接放到request.user,模板里通过{% if user.is_authenticated %}判断是否显示登录栏。
模板侧渲染统计卡片的写法很简单:
<!-- templates/base.html 或 index.html 片段 --> <div class="stat-card"> <span class="stat-label">总发言数</span> <strong>{{ total_text_count }}</strong> </div> <ul class="news-list"> {% for item in news_list %} <li>{{ item.title }} - {{ item.publish_time|date:"Y-m-d" }}</li> {% endfor %} </ul>带|date:"Y-m-d"的管道符号是 Django 模板过滤器,用来格式化时间戳输出。日期天气这类信息通常由 JavaScript 在浏览器端获取,不经过后端数据库,所以论文里说左侧显示日期和天气,实际实现大概率是在模板里引一段 JS 代码完成的。
4.2 文本管理:关键词检索与管理状态的流转
文本管理是本系统最核心的业务功能之一,用户之前输入的关键词和最终分析结果都在这里展示。页面需要一个关键词搜索框,还要支持管理员对文本做“未处理/已处理”的状态标记。搜索用 Django ORM 的icontains做模糊匹配,状态流转用update_or_create保证幂等。
# app/views.py from django.db.models import Q from .models import TextInfo, TextManageInfo def text_manage(request): keyword = request.GET.get('keyword', '').strip() texts = TextInfo.objects.select_related('user').all().order_by('-create_time') if keyword: texts = texts.filter(Q(content__icontains=keyword) | Q(analysis_result__icontains=keyword)) return render(request, 'text_manage.html', {'texts': texts})select_related('user')会在查询时直接 JOIN 用户表,避免在模板里循环访问text.user.username时产生 N+1 次查询。这个优化在数据量大时效果明显,属于 Django 开发者应该形成肌肉记忆的操作。Q(content__icontains=keyword) | Q(analysis_result__icontains=keyword)生成的是WHERE content LIKE %keyword% OR analysis_result LIKE %keyword%,用 Q 对象是为了把两个字段的模糊匹配拼到同一个 WHERE 条件里。
状态标记的操作通常是管理员点击列表右侧的按钮,触发一个 POST 请求:
def update_manage_status(request, text_id): status = request.POST.get('status', '0') TextManageInfo.objects.update_or_create( text_id=text_id, defaults={ 'manage_status': int(status), 'manager_id': request.user.id, 'handle_time': now(), } ) return redirect('text_manage')update_or_create的第一个参数是查询条件,defaults是更新时要写入的字段。第一次标记时如果text_manage_info里没有这条记录,就会自动创建;之后再次点击则是更新状态,不会生成重复记录。这比先filter().exists()再决定 insert 还是 update 要简洁得多,也是我在这个场景下的首选。状态字段manage_status用 0/1/2 区分未处理/已处理/忽略,前端模板里用{% if item.manage_status == 0 %}判断按钮样式即可。
4.3 注册用户管理:创建、搜索、权限设置与删除操作
用户管理模块只有管理员能进入。论文 5.7 节描述了两种能力:一是创建新用户,输入姓名、密码和手机号即可添加;二是通过列表上方的搜索栏查找用户,修改信息或删除违规用户。这里的权限校验必须在服务端做,不能只靠前端隐藏入口。
# app/views.py from django.contrib.auth.decorators import login_required from django.contrib.auth.hashers import make_password from .models import UserInfo @login_required def user_manage(request): if not request.user.is_superuser: return redirect('index') keyword = request.GET.get('keyword', '').strip() users = UserInfo.objects.annotate(text_count=Count('textinfo')).order_by('-register_time') if keyword: users = users.filter(username__icontains=keyword) return render(request, 'user_manage.html', {'users': users})annotate(text_count=Count('textinfo'))统计每个用户名下的文本数量,这个字段能在列表页直观展示哪个用户发言最活跃。新用户创建时,密码必须经过make_password哈希再入库:
def user_create(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') phone = request.POST.get('phone', '') UserInfo.objects.create( username=username, password=make_password(password), phone=phone, ) return redirect('user_manage') return render(request, 'user_create.html')删除用户和修改权限的操作也要放在 POST 请求里,并且每次都要重新校验is_superuser。管理员只能删除普通用户,不能删除自己或者另一个管理员,这个校验最好在视图里写死:
def user_delete(request, user_id): if not request.user.is_superuser: return redirect('index') target = UserInfo.objects.get(id=user_id) if target.is_superuser: return redirect('user_manage') # 拒绝删除管理员 target.delete() return redirect('user_manage')权限设置同理,管理员可以通过一个复选框控制用户是否有文本管理权限。这里的权限字段可以是UserInfo模型上的一个布尔字段can_manage_text,视图里target.can_manage_text = request.POST.get('can_manage') == 'true'然后save()即可。
5. 踩坑与排查:跑这套项目前后要确认的五个细节
这套系统技术栈不算新,但 Django + MySQL + 预训练模型这个组合在本地复现时踩坑点不少。我按常见的故障现场整理五条,每一条都是真实会发生的“玄学”问题,其实背后原因都很具体。你照着查,能省下不少排查时间。
5.1 现象:数据库连接报 2003 错误,页面打不开
启动 Django 后访问页面,浏览器提示数据库连接失败,终端打印OperationalError: (2003, "Can't connect to MySQL server...")。这个报错的意思很直白:Django 压根没连上 MySQL 服务。
原因基本只有两个:一是 MySQL 服务没启动,二是settings.py里的HOST或PORT写错了。很多人习惯了用 Navicat 连数据库,Navicat 能连不代表 Django 能连,因为 Navicat 可能是在本机起的服务,而 Django 配置里写的是别的地址。
解决方法是先确认服务状态,再检查配置。Windows 下看任务管理器里有没有mysqld.exe,或者在命令行执行mysql -u root -p手动登录。排除了服务问题后再去settings.py里核对HOST是不是127.0.0.1或localhost,PORT是不是3306。推荐做法是先用 Navicat 用同一套账号密码测试连接,Navicat 能连而 Django 不能连,那就是 Django 配置的问题了。
5.2 现象:BERT 模型加载要十几秒,首次请求直接超时
文本分析页面第一次点击分析按钮,浏览器转圈很久才出结果,甚至直接请求超时。这是因为 BERT 预训练模型体积大,首次加载要读入几百兆参数文件,在只有 CPU 的笔记本上初始化特别慢。
问题不在代码逻辑,而在加载时机。如果模型是在视图函数里每次请求都load_pretrained(),那每次分析都要重新加载一次,慢上加慢。常见做法是项目启动时一次性加载模型到内存,或者用模块级全局变量做缓存。
解决方法是把模型加载放到algorithms/bert_predict.py的模块顶层:
# app/algorithms/bert_predict.py _model = None def bert_predict(text): global _model if _model is None: _model = load_bert_model() # 首次加载,后续请求直接复用 return _model.predict(text)另外,输入文本长度最好限制在 128 到 256 个字符内,BERT 的 Transformer 结构对超长文本的处理是截断和补 padding,输入越长推理越慢,舆情评论类文本通常都很短,截断到 128 基本不影响准确率。
5.3 现象:中文入库后变成问号,文本分析结果乱码
输入中文文本进行分析,页面显示正常,但打开数据库表一看,content字段存的全是?????。这是 MySQL 字符集没配置成utf8mb4的典型症状。
原因有两个层面:数据库表本身的字符集不是utf8mb4,或者 Django 连接时用的字符集不对。Navicat 建表时如果没手动指定字符集,默认可能是latin1,中文自然存不进去。
解决方法是双管齐下。建表时指定字符集:
CREATE TABLE text_info ( ... ) DEFAULT CHARSET=utf8mb4;同时在settings.py的数据库配置里加上OPTIONS:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'opinion_system', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } }还有个容易忽略的点:如果表已经建好了,ALTER TABLE text_info CONVERT TO CHARACTER SET utf8mb4可以把现有表改过来,但已经变成问号的数据是找不回来的。所以这个坑最好在建表阶段就规避,这也是为什么论文里强调数据库设计要在编码之前完成。
5.4 现象:页面样式全丢,只剩光秃秃的 HTML
本地开发时页面样式正常,部署到服务器或者把DEBUG改成False之后,所有 CSS、JS 全部失效,页面变成了纯文本列表。
原因很简单:Django 的静态文件服务只在DEBUG=True时由开发服务器自动提供,DEBUG=False后静态文件需要由 Web 服务器单独处理。很多初学者不知道这一点,直接把DEBUG关了,静态文件就 404 了。
如果你只是在本地跑源码,解决方法是保持DEBUG=True,同时确认settings.py里配了STATIC_URL = '/static/'和STATICFILES_DIRS,并且模板里用{% load static %}和{% static 'css/style.css' %}引用静态资源,而不是写死/static/css/style.css路径。如果确实需要关闭 DEBUG,记着执行python manage.py collectstatic收集所有静态文件到STATIC_ROOT,再交给 Nginx 之类的服务托管。
5.5 现象:两个模型的结果对不上,对比图的饼状图和柱状图数据互相矛盾
对比分析页面里,DNN 统计出 30 条负面,BERT 统计出 22 条负面,两份图表的同一类数据对不上。管理员看着图产生了困惑。
这是双模型系统的必然现象,但具体原因往往是预处理不一致。DNN 的输入可能是jieba分词后用词向量编码,BER T的输入是tokenizer分词后做 padding,两边对同一条文本的“负面”判定阈值也可能不同。结果就是文本被两个模型分到了不同类别,比例自然不一致。
解决方法是统一预处理入口。把清洗、分词、截断的逻辑收敛到一个公共函数里,在text_clean()之后才分别喂给两个模型。当初我踩这个坑时,就是因为在dnn_predict里先做了去停用词,而bert_predict没做,导致同一条文本在 DNN 那边被判定为中性,在 BERT 这边因为多了几个停用词被判成负面。统一预处理之后,两边的输入口径一致,结果差异就只来自模型本身了。如果两个模型仍然对同一条文本结论不一致,就在业务层面约定一个仲裁规则,比如论文里强调的“更信任 BERT”,把规则写进代码,对比分析页面就不会出现数据打架的问题。
6. 二次开发:把人工输入的文本分析升级成定时采集与可插拔算法
这套系统当下的交互模式是“用户手动输入文本,模型即时分析”。放在真实舆情管理场景里,这个模式太被动——每天谁能盯着页面一遍遍刷新输入?更现实的方案是接入数据采集任务,定时拉取指定关键词下的新增言论,自动入库,自动分析,管理员只需要每天登录系统看新增的负面舆情和处理状态。这一章讲两条改造路径,都是我在原有骨架之上会优先做的事。
6.1 改造思路:给分析引擎加一个抽象层
现在的double_model_predict是硬编码调用 DNN 和 BERT,如果将来想接入第三套模型,比如 XLNet 或者 ChatGLM,改函数内部的逻辑就行,但这会让主流程越来越臃肿。更好的做法是定义分析器基类,让每个模型各自实现predict方法:
# app/algorithms/base.py class BaseAnalyzer: def predict(self, text): raise NotImplementedError # app/algorithms/dnn_predict.py class DNNAnalyzer(BaseAnalyzer): def predict(self, text): # 返回 (label, prob) return dnn_predict(text) # app/algorithms/bert_predict.py class BERTAnalyzer(BaseAnalyzer): def predict(self, text): return bert_predict(text)业务代码里只需要维护一个分析器列表,按顺序逐个调用,再按仲裁规则选结果。这样新增算法时,不需要动views.py,只需要新增一个继承BaseAnalyzer的类,并把它注册到配置列表里。
6.2 改造示例:批量任务与按日趋势图
采集到的新文本往往是一批一批的,比如每分钟拉一次接口,每次拿到几十条新评论。可以写一个 Django 管理命令,用定时任务周期执行:
# app/management/commands/auto_analyze.py from django.core.management.base import BaseCommand from app.models import TextInfo class Command(BaseCommand): def handle(self, *args, **options): for content in fetch_latest_comments(): label, score = double_model_predict(content) TextInfo.objects.create( user=get_system_user(), content=content, sentiment_score=score, analysis_result=label, ) self.stdout.write('auto analyze finished')fetch_latest_comments()可以替换成任意数据源,比如爬虫入库的结果、API 推送的评论列表。配合系统 crontab 或 Windows 任务计划定期执行python manage.py auto_analyze,系统就从“被动输入”变成了“主动监控”。
趋势图改造更简单,把 3.3 节的聚合查询改成按日期分组:
from django.db.models.functions import TruncDate daily_stats = TextInfo.objects \ .filter(create_time__gte=start_date) \ .annotate(day=TruncDate('create_time')) \ .values('day', 'analysis_result') \ .annotate(total=Count('id')) \ .order_by('day')这段代码生成的结果是[{day: 2025-03-01, analysis_result: 'negative', total: 12}, ...],前端拿到后按日期和分类做双维度柱状图,就能看到负面言论在时间轴上的变化曲线。改造完成后,这套系统就从“文本分析工具”真正变成了“舆情数据看板”,而底层表结构完全不用动。
说到这个改造,我有点犯嘀咕:当初我自己做类似系统时,只顾着把模型调准,忽略了数据采集和定时任务的交付,结果系统上线后每天没人喂数据,成了摆设。后来去补调度任务,又因为预处理不一致翻了车,跟第 5 章说的情况几乎一模一样。从那以后,每次接这种分析类项目,我都会在动手写模型前,先把“数据从哪来、分析完存到哪、每天什么时候自动跑”这三件事敲定,再回来写代码。希望你如果打算在这套源码上继续扩展,也能先想到这个层面,真的能帮到你少走弯路。
本文还有配套的精品资源,点击获取