☰
基于Python+Django的舆情分析系统:DNN与BERT双模型情感分析
2026/10/27 3:04:30 网站建设 项目流程

简介:面向计算机与软件工程专业学生及网络舆情管理方向学习者的毕业设计参考文档,主题为基于Python与MySQL的网络舆情分析系统的设计与实现。内容围绕网络言论分析、言论管理、用户管理等核心模块展开,梳理系统需求、数据库设计、功能实现与开发流程,并讨论网络管理部门在舆情监测中的需求与挑战。资源包共1个docx文件,大小约1.73MB,主体为完整毕业论文文档,包含中英文摘要、系统设计说明与实现论述,适合用于选题参考、论文结构模仿和功能模块梳理。已有1262人学习下载,可为毕业设计写作、系统方案论证及答辩准备提供较具体的案例素材。

1. 从一句评论开始:这套 Python 网络舆情分析系统到底解决什么问题

一个做社区运营的朋友曾经问我:站内一天几千条留言,怎么在半小时内判断出哪批内容值得人工看?他当时的做法是关键词搜索加肉眼翻页,翻到第三页就开始走神。这套基于 Python 和 MySQL 的网络舆情分析系统,本质上就是把这件事工程化:用户提交一段文本或一个地区关键词,系统调用 DNN 与 BERT 两条分析链路给出情感倾向打分,把结果连同原文落进 MySQL,再在管理后台按「未处理 / 已处理」流转,最后用饼图和柱状图把两种算法的结论摆在一起做对比。

它适合三类人:拿它当毕业设计底稿、需要一套能跑通的完整增删改查骨架的同学;想快速搭一个内容审核/口碑监测原型的产品或运营开发;以及想看看 Django 单体项目怎么把机器学习模型塞进 Web 请求链路里的后端。源码包里带论文文档、数据库脚本和完整工程目录,照着改比从零起架子省事得多。下面按「架构 → 库表 → 算法链路 → 排错 → 改造」的顺序拆开讲。

2. 技术栈与整体架构:Django + MySQL 撑起一个 B/S 舆情后台

这套系统的定位很明确:浏览器打开就能用,不装客户端,两个月内一个人写完。所以 B/S 架构不是选择题,是必答题。前端 HTML5 + CSS + JavaScript 负责表单提交和图表渲染,后端 Python 用 Django 承接路由、模板和 ORM,MySQL 存用户、文本、分析结果和管理状态。论文里写的开发机是 i5 十代 + 8G 内存 + 固态盘,这个配置在今天看来很朴素,但也说明一件事——它的模型推理部分必须足够轻,不能指望本地跑一个几十 G 的大模型。

2.1 为什么是 Django 而不是 Flask

毕设场景最怕的不是性能不够,是「写到最后发现登录、后台、表单校验全要自己造」。Django 自带 Admin、自带 ORM、自带用户认证和 CSRF 防护,这三点刚好对上系统的四个核心模块:用户注册登录、文本分析、文本管理、用户管理。用 Flask 当然也能做,但你需要额外引入 Flask-Login、Flask-WTF、SQLAlchemy,还要自己写一套后台管理页面,工作量翻倍。

选 Django 的代价是目录约束比较死,MTV 分层必须守规矩。我一般的做法是把业务按领域切成独立 app,每个 app 管自己的 models、views、urls,主项目只留配置和总路由。这样后面加「定时采集」或者「导出报表」时,新开一个 app 就行,不用动老代码。

提示:论文里提到的编辑器只是个人偏好,实际用什么 IDE 都行,关键是把虚拟环境和依赖版本锁住,requirements.txt 里别只写django三个字母。

2.2 工程目录结构

原始工程的目录不会跟下面完全一致,但按这套系统描述的功能拆分,落到磁盘上最合理的形态就是这样:

opinion_system/ ├── manage.py ├── requirements.txt ├── opinion_system/ # 项目配置层 │ ├── settings.py # 数据库、静态文件、模板路径 │ ├── urls.py # 总路由,include 各 app │ └── wsgi.py ├── apps/ │ ├── accounts/ # 注册登录、个人信息、用户管理 │ │ ├── models.py # SysUser │ │ ├── views.py │ │ └── urls.py │ ├── analysis/ # 文本分析入口、双模型调用 │ │ ├── views.py │ │ ├── services.py # 业务编排,不写进 views │ │ └── urls.py │ ├── contents/ # 文本管理、站内新闻 │ └── dashboard/ # 首页统计、对比分析图表 ├── ml/ │ ├── dnn_model.py # TF-IDF + MLP 情感分类 │ ├── bert_model.py # BERT 情感分类封装 │ └── weights/ # 模型权重与词表 ├── templates/ # 全站模板 ├── static/ # css / js / echarts └── media/

关键点在apps/analysis/services.py这一层。很多人图省事把模型加载、推理、入库、拼装返回结果全塞进views.py,前期跑得通,后期换模型或者加异步就变成一团乱麻。把「编排逻辑」抽到 services,视图只负责取参数、调服务、渲染,这是单体项目里最便宜的一次架构升级。

2.3 一次请求从浏览器到数据库的完整链路

以「文本分析」这个动作举例,链路是这样的:用户在页面文本框输入一段话,点提交,浏览器 POST 到/analysis/submit/;Django 的 URLconf 匹配到视图函数,中间件先过一遍 CSRF 校验和会话校验;视图拿到content和keyword两个参数,做非空和长度校验;然后调用 services 里的分析函数,函数内部分别走 DNN 分支和 BERT 分支拿到两个情感分值;接着把原始文本、关键词、两个分值、模型标识一起写进text_info表,同时往text_manage表插一条状态为「未处理」的记录;最后重定向回列表页,列表页用 ORM 分页查出最近 20 条,模板渲染时把分值映射成颜色标签。

# apps/analysis/views.py from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods from .services import analyze_and_save @login_required # 未登录直接跳登录页,省掉手写判断 @require_http_methods(["POST"]) # 只接受 POST,避免 GET 刷数据 def submit_text(request): content = (request.POST.get("content") or "").strip() keyword = (request.POST.get("keyword") or "").strip() if not content: return render(request, "analysis/form.html", {"error": "文本内容不能为空"}) if len(content) > 2000: # 与 text_info.content 字段长度保持一致 return render(request, "analysis/form.html", {"error": "单条文本不超过 2000 字"}) record = analyze_and_save(content=content, keyword=keyword, user=request.user) return redirect("contents:text_detail", pk=record.id)

@login_required负责登录态拦截,@require_http_methods限制请求方法,两个装饰器加起来就把最常见的越权和误提交堵住了。content的长度上限 2000 不是随手写的,它必须和数据库字段长度、前端maxlength三处对齐,任何一处不一致都会在边界值上出问题——这条血泪经验后面第 5 章还会再提一次。

3. 数据库设计:五张表串起用户、文本与分析结果

毕设论文里最容易糊弄的就是数据库设计,一张大表塞所有字段也能跑。但这套系统要支持「文本分类」「管理状态流转」「双算法结果对比」三件事,硬塞一张表会出现大量冗余,改一次分类名要更新几千行。按论文描述的功能点拆,五张表是比较合理的落点。

3.1 表结构与字段职责

表名职责关键字段与其他表的关系
sys_user注册用户与管理员username、password、role、phone被text_manage、site_news引用
text_category文本分类字典name、sort被text_info引用
text_info原始文本与分析结果content、keyword、dnn_score、bert_score多对一指向分类
text_manage处置流转记录text_id、handler_id、status一条文本对应一条管理记录
site_news站内公告title、content、is_top指向发布人

role字段我一般用 0/1 表示普通用户和管理员,而不是建一张角色表。这套系统的权限粒度只有两级——普通用户能做分析和查看自己的记录,管理员能管所有人——上 RBAC 属于过度设计,评审老师问起来还得解释半天。

text_info里同时放dnn_score和bert_score,是为了支撑「对比分析」模块。两个分值都落库,对比页面就不需要重新跑一遍推理,直接聚合查询即可。这是用存储换响应速度的典型取舍,在数据量不大(几千到几万条)的场景下非常划算。

3.2 建表 SQL 与索引取舍

-- 用户表:密码必须是哈希,字段长度留够 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL COMMENT 'PBKDF2 哈希,不是明文', real_name VARCHAR(32) DEFAULT '', phone VARCHAR(20) DEFAULT '', role TINYINT NOT NULL DEFAULT 0 COMMENT '0普通 1管理员', last_login DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -- 文本信息表:分析结果和原文放一起 CREATE TABLE text_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL DEFAULT 1, content VARCHAR(2000) NOT NULL, keyword VARCHAR(64) DEFAULT '' COMMENT '分析时输入的地区/主题词', source VARCHAR(64) DEFAULT '' COMMENT '来源平台标识', dnn_score DECIMAL(5,4) DEFAULT NULL COMMENT '0~1 负面概率', bert_score DECIMAL(5,4) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_keyword_time (keyword, created_at), KEY idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -- 管理流转表:一条文本一行状态 CREATE TABLE text_manage ( id BIGINT PRIMARY KEY AUTO_INCREMENT, text_id BIGINT NOT NULL, handler_id BIGINT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0未处理 1已处理 2已忽略', remark VARCHAR(255) DEFAULT '', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_text (text_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

三处细节值得说。第一,所有表统一utf8mb4,不是utf8。中文在 MySQL 的utf8里是「三字节 UTF-8」,存不了 emoji 和部分生僻字,用户从社交平台复制的一段话里带个表情符号,插入直接报错或者静默截断,这是新手最常翻车的地方。第二,text_manage的text_id加了唯一索引,保证一条文本只有一条流转记录,避免重复点击产生脏数据。第三,idx_keyword_time复合索引是为了支撑「按地区词 + 时间倒序」的列表查询,这也是首页和文本管理页最高频的查询模式。

注意:dnn_score用DECIMAL(5,4)而不是FLOAT。浮点精度问题在比对「两条记录分值是否相等」时会出玄学 bug,定点数稳妥。

3.3 用 ORM 而不是裸 SQL

论文里只讲了表的逻辑结构,实际编码时我更倾向全走 Django ORM。原因不是 ORM 更快,是它把「字段改动后要同步改 SQL」这件事省掉了。

# apps/contents/models.py from django.db import models class TextInfo(models.Model): category_id = models.BigIntegerField(default=1) content = models.CharField(max_length=2000) keyword = models.CharField(max_length=64, blank=True, default="") source = models.CharField(max_length=64, blank=True, default="") dnn_score = models.DecimalField(max_digits=5, decimal_places=4, null=True) bert_score = models.DecimalField(max_digits=5, decimal_places=4, null=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "text_info" indexes = [models.Index(fields=["keyword", "created_at"])] class TextManage(models.Model): text = models.OneToOneField(TextInfo, on_delete=models.CASCADE, db_column="text_id") handler_id = models.BigIntegerField(null=True) status = models.SmallIntegerField(default=0) remark = models.CharField(max_length=255, blank=True, default="") updated_at = models.DateTimeField(auto_now=True)

OneToOneField对应数据库里的唯一索引,on_delete=models.CASCADE保证删文本时管理记录一起清掉,不会留下孤儿数据。列表页查询的时候记得带select_related("text"),否则 N 条记录会额外触发 N 次查询,页面上看不出问题,日志里一片 SQL 刷屏。

4. 文本分析链路:DNN 与 BERT 双模型怎么接进 Django

这是整套系统最有技术含量、也最容易写崩的部分。论文里说用了 DNN 和 BERT 两种算法做对比分析,落到工程上,问题不是「模型准不准」,而是「一个 Web 请求里怎么把模型跑起来还不超时」。

4.1 模型层的封装方式

两个模型各自封成一个类,对外只暴露predict(text) -> float一个方法,返回值是 0 到 1 之间的负面倾向概率。上层 services 不关心内部是 sklearn 还是 transformers。

# ml/dnn_model.py import jieba import joblib from pathlib import Path class DNNClassifier: """TF-IDF 向量化 + 多层感知机,轻量、启动快,适合当基线模型""" def __init__(self, model_dir="ml/weights"): base = Path(model_dir) self.vectorizer = joblib.load(base / "tfidf.pkl") # 训练时保存的向量器 self.clf = joblib.load(base / "mlp.pkl") # 训练好的 MLP @staticmethod def _cut(text: str) -> str: # 中文必须分词,空格分隔后交给 TF-IDF return " ".join(jieba.cut(text)) def predict(self, text: str) -> float: vec = self.vectorizer.transform([self._cut(text)]) # predict_proba 返回 [[负, 正]],取第 0 列作为负面概率 return float(self.clf.predict_proba(vec)[0][0])
# ml/bert_model.py import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification class BertClassifier: """加载本地微调好的中文情感分类模型,只在进程内实例化一次""" _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self, model_path="ml/weights/bert-sentiment"): if getattr(self, "_ready", False): return self.tokenizer = AutoTokenizer.from_pretrained(model_path) self.model = AutoModelForSequenceClassification.from_pretrained(model_path) self.model.eval() # 关掉 dropout,推理必须 self._ready = True @torch.no_grad() # 不建计算图,省内存 def predict(self, text: str) -> float: batch = self.tokenizer(text, truncation=True, max_length=256, return_tensors="pt") logits = self.model(**batch).logits prob = torch.softmax(logits, dim=-1)[0] return float(prob[0]) # 标签 0 定义为负面

DNN 分支走jieba分词加 TF-IDF,好处是加载快、单条推理毫秒级,缺点是对新词和反讽无能为力;BERT 分支用的是预训练加微调的路子,max_length=256是性能和覆盖度的平衡点,超过这个长度直接截断,因为舆情文本绝大多数是短句。

BertClassifier用了单例模式,这一点非常关键。模型文件动辄几百兆,如果每次请求都from_pretrained一次,第一个用户点分析要等十几秒,第二个用户来了内存直接翻倍。用__new__加_ready标记保证进程内只加载一次,是这套系统能跑起来的前提。

提示:model.eval()和@torch.no_grad()别漏。前者影响 BatchNorm 和 Dropout 的行为,后者能砍掉一半以上的显存占用,在 8G 内存的机器上是生死线。

4.2 业务编排与结果落库

视图只调一个函数,真正的活都在 services 里干。

# apps/analysis/services.py import logging from decimal import Decimal from ml.dnn_model import DNNClassifier from ml.bert_model import BertClassifier from apps.contents.models import TextInfo, TextManage logger = logging.getLogger(__name__) _dnn = DNNClassifier() _bert = BertClassifier() # 首次导入即加载,配合单例不会重复 def analyze_and_save(content: str, keyword: str, user) -> TextInfo: dnn_score = bert_score = None try: dnn_score = _dnn.predict(content) except Exception as e: # 单模型失败不阻断整条链路,标记为空后续补跑 logger.exception("DNN 推理失败: %s", e) try: bert_score = _bert.predict(content) except Exception as e: logger.exception("BERT 推理失败: %s", e) record = TextInfo.objects.create( content=content, keyword=keyword, dnn_score=Decimal(f"{dnn_score:.4f}") if dnn_score is not None else None, bert_score=Decimal(f"{bert_score:.4f}") if bert_score is not None else None, ) TextManage.objects.create(text=record, status=0) return record

这里的编排思路是「降级不中断」:两个模型任意一个抛异常,另一个的结果照样入库,页面照样能展示。真实场景下 transformers 版本的兼容问题、权重文件路径配错,都会导致 BERT 分支挂掉,如果直接raise,用户看到的就是 500 页面,体验非常差。

Decimal(f"{x:.4f}")这个写法不是啰嗦。float直接塞进DecimalField会带上二进制浮点的尾巴,比如0.7312000000000001,超出decimal_places=4就会被 Django 在严格模式下拒绝,或者被 MySQL 四舍五入成你不想要的值。先格式化成字符串再转,是稳的做法。

4.3 对比分析模块的数据聚合

对比页面要展示「同一批文本,两个模型判定差异有多大」。最直观的做法是按分值区间分桶,统计各桶的记录数。

# apps/dashboard/views.py from django.db.models import Count, Case, When, IntegerField from django.shortcuts import render from apps.contents.models import TextInfo def compare_view(request): qs = TextInfo.objects.exclude(dnn_score=None).exclude(bert_score=None) # 按 0.5 为界切成"偏负面/偏正面"两档,统计两模型的判定分布 stat = qs.aggregate( dnn_neg=Count(Case(When(dnn_score__gte=0.5, then=1), output_field=IntegerField())), bert_neg=Count(Case(When(bert_score__gte=0.5, then=1), output_field=IntegerField())), total=Count("id"), ) # 判定不一致的样本,是最值得人工复核的部分 diff = qs.filter(dnn_score__gte=0.5, bert_score__lt=0.5).count() \ + qs.filter(dnn_score__lt=0.5, bert_score__gte=0.5).count() return render(request, "dashboard/compare.html", { "stat": stat, "diff": diff, "diff_rate": round(diff / stat["total"] * 100, 2) if stat["total"] else 0, })

Count(Case(When(...)))是 Django 里做条件计数的标准写法,一次查询拿到所有分桶数据,比循环查 N 次快一个数量级。diff这个指标比单纯的两根柱子更有价值——两个模型分歧大的样本,往往就是反讽、缩写、方言这类难例,把它挑出来给人工看,才是「双模型」真正的意义,而不是为了论文里多写一段。

5. 常见问题排查:跑起来之后最容易翻车的 5 个地方

代码能跑通和系统能交付是两回事。这一节列的都是我在类似项目里真实踩过的,按「现象 → 原因 → 解决」写,遇到对应症状可以直接对号入座。

5.1 中文和表情符号入库报错或变成问号

现象:提交一段带 emoji 的文本,控制台抛Incorrect string value: '\xF0\x9F...',或者数据进库了但中文全是???。

原因:数据库、表、连接三处的字符集不一致。MySQL 的utf8实际是三字节,存不了四字节的 emoji;Django 的DATABASES配置里如果没指定charset,驱动会用默认值。

解决:建库时写死CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,已有的表用ALTER TABLE text_info CONVERT TO CHARACTER SET utf8mb4;迁移,Django 配置补上"OPTIONS": {"charset": "utf8mb4"}。三处齐了才不会复发。

5.2 第一次点「文本分析」转圈十几秒然后 502

现象:服务刚启动,第一个分析请求特别慢,有时候直接超时;之后再点就正常了。

原因:BERT 权重是在请求线程里懒加载的,第一次推理把加载时间也算进去了。生产环境的反向代理通常有 30 秒超时,模型加载加首次推理刚好卡在边界上。

解决:把模型加载提前到应用启动阶段,在apps.py的ready()里做一次空文本预热推理,或者在部署脚本里跑一条管理命令触发加载。预热完再放流量进来,首个请求就是毫秒级。

5.3 表单提交一直 403 Forbidden

现象:本地跑得好好的,部署到服务器上,所有 POST 请求全部 403,GET 正常。

原因:Django 的 CSRF 中间件在模板里找不到{% csrf_token %},或者跨域部署时CSRF_TRUSTED_ORIGINS没配。

解决:检查表单模板里是否写了{% csrf_token %},这是最常见的原因;如果是前后端分离或者加了域名,在settings.py里配CSRF_TRUSTED_ORIGINS = ["https://your-domain"]。千万别图省事直接@csrf_exempt,那等于把整个表单敞开。

5.4 关掉 DEBUG 之后页面样式全丢、图片 404

现象:DEBUG = True时一切正常,改成False之后 CSS 不生效、头像裂开、后台进不去。

原因:Django 在DEBUG=False时不再自动托管静态文件,STATIC_URL和STATIC_ROOT也没配,collectstatic更没跑过。

解决:配好STATIC_ROOT,执行python manage.py collectstatic,然后由 Nginx 或 WhiteNoise 托管。本地调试阶段可以先装 WhiteNoise 中间件顶一下,别在开发期就去折腾 Nginx 配置。

5.5 文本列表越翻越慢,数据库连接数打满

现象:数据量到几千条以后,文本管理页打开明显变卡,日志里同一时刻刷出上百条 SELECT。

原因:模板里循环取item.textmanage.status,每条记录触发一次关联查询,标准的 N+1 问题;再加上分页没做,一次查全表。

解决:查询加select_related("textmanage")或prefetch_related,分页用Paginator每页 20 条。另外确认text_manage.text_id上的唯一索引存在,缺索引时关联查询会退化成全表扫描。

6. 二次开发:把单机毕设改成能持续供数的分析平台

原系统是「人工输入文本 → 出结果」的手动模式,实际用起来最大的短板是数据源。真正要跑起来,第一步是加采集端。我一般会写一个 Django 管理命令,配合系统定时任务,把抓取到的内容批量灌进text_info:

# apps/analysis/management/commands/ingest.py from django.core.management.base import BaseCommand from apps.analysis.services import analyze_and_save class Command(BaseCommand): help = "批量导入外部文本并触发分析" def add_arguments(self, parser): parser.add_argument("--file", required=True) # 每行一条文本 parser.add_argument("--keyword", default="") # 归属的地区/主题词 def handle(self, *args, **opts): ok = fail = 0 with open(opts["file"], encoding="utf-8") as f: for line in f: text = line.strip() if not text: continue try: analyze_and_save(content=text[:2000], keyword=opts["keyword"], user=None) ok += 1 except Exception as e: fail += 1 self.stderr.write(f"跳过:{e}") self.stdout.write(self.style.SUCCESS(f"成功 {ok} 条,失败 {fail} 条"))

批量导入时有三件事必须提前想好。一是单条失败要吞掉异常继续跑,不能因为第 37 条有问题就整批回滚;二是content[:2000]的截断必须和字段长度一致,外部数据什么长度都可能有;三是量大的时候加个--batch参数分批提交,别让一个事务挂着几万条。

第二个改造方向是模型热更新。ml/weights/目录下的模型文件换掉之后,进程不会自动重载。稳妥的做法是在BertClassifier里加一个reload_if_changed()方法,比对文件 mtime,变了就重新加载,挂在一个管理命令或者后台按钮上。别用文件监听那一套,在容器环境里经常失效。

第三个方向是权限细化。现在的两级角色够用,但如果要加「只读审计员」这类角色,改动点集中在三处:

改造点位置改法
角色定义sys_user.role用常量类替代魔法数字 0/1
视图控制apps/*/views.py抽一个@role_required("auditor")装饰器
模板渲染templates/base.html按角色隐藏按钮,但后端必须再校验一次

最后一条尤其要命:模板里隐藏按钮只是视觉上的,任何人改一下请求参数照样能调接口,所以权限判断必须在视图层再落一次。

这套系统真正值得抄的不是那几个页面,是「模型封装成单例 + 业务编排抽到 services + 降级不中断」这三件事的组合。我最早做类似功能时图快,把from_pretrained直接写在视图函数里,结果压测一跑,内存曲线跟爬山一样,重启三次才定位到问题。从那以后我每次接模型进 Web 项目,都强制先问一句:这个对象什么时候加载、加载几次、加载失败怎么办。这三个问题答不上来,先别写视图。希望帮到你。

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

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

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

立即咨询