☰
Python校园舆情管理系统:爬虫、情感分析与可视化实战
2026/10/3 9:58:32 网站建设 项目流程

简介:一份面向Python毕业设计与课程设计的校园舆情管理系统完整项目,前后端代码齐全,适合需要快速搭建Web应用或完成毕设的学生参考。项目采用Python编写后台,配合HTML、CSS、JavaScript构建前端界面,使用MySQL存储数据,整体结构规范、界面简洁,经过严格调试确保可直接运行。压缩包共254个文件,约37.64MB。其中包含29个Python源码文件、35个JavaScript交互脚本、16个CSS样式、12个HTML页面,以及大量GIF演示图和JPG截图,便于查看系统各功能模块的运行效果;另附SQL数据库脚本,可一键导入数据表结构。文件类型覆盖前后端、资源与文档,目录清晰。目前已有91人浏览学习。借助这份资料,读者可获得完整项目源码、数据库脚本、依赖工具说明及部署指引,既能直接运行体验,也能参照代码理解校园舆情管理中的信息采集、分类展示、后台管理等模块的实现思路,对课程设计、毕业设计或PythonWeb开发入门都具有较高参考价值。

1. 从选题到落地:Python校园舆情管理系统到底解决什么问题

毕业设计选“Python校园舆情管理系统”的同学,大多是被 “管理系统” 三个字带偏了:以为就是一个增删改查后台,套个 Django 模板就交差。真正做起来你才会发现,舆情系统的核心工作量在“数据是否来得干净、分析结果是否可信”,界面反而是最不费时间的部分。这个标题解决的是“散落在微博、贴吧、校园论坛里的公开讨论,如何自动汇总、判断正负面并可视化预警”的完整链路,适合想往数据采集与数据分析与可视化方向走的计算机相关专业学生,也适合用 Python 爬虫和 ECharts 做作品集的项目。全文我会从架构、建表、采集、情感分析到避坑,按一套能跑通的最小方案讲明白。

2. 先搭骨架:Django + MySQL + Redis 的舆情数据模型设计

拿到一份带 zip 的毕设项目,先别急着runserver。你需要先回答三个问题:后端用哪个框架、数据存在哪、定时任务怎么调度。这三个决定后面所有代码能不能长在一个稳定的骨架上。

2.1 选型思路:为什么毕业设计里 Django 比 Flask 更省事

舆情管理系统需要的不是一个 API 转发服务,而是后台管理、权限、ORM 建模、定时任务、页面渲染这些“重度功能”的组合。对比下来,Django 自带 admin、auth、ORM,一个命令就能把后台管理界面生成出来,这对答辩演示非常有利。Flask 灵活,但用户登录、分页、表单校验都要自己装第三方扩展,做到一半很容易失控。

推荐组合是 Django 4.x + MySQL + Redis + Celery。Redis 在这里扮演两个角色:一是 Celery 的消息代理,二是后面爬虫去重、热点统计的缓存层。Python 版本建议 3.10,原因在第五章会展开。

一个典型的requirements.txt长这样,注意把容易冲突的包锁到已知能协同工作的版本:

Django>=4.2,<5.0 mysqlclient>=2.2 celery>=5.3,<6.0 redis>=5.0 requests>=2.31 beautifulsoup4>=4.12 jieba>=0.42.1 snownlp>=0.12.3 django-celery-beat>=2.5

逻辑说明:Django 主体保证 4.2 以上但不上 5.0,避免新版本对mysqlclient的兼容问题;Celery 限制在 5.x 是因为 6.x 在 Windows 下的事件循环踩坑更多;django-celery-beat用来在后台动态增删定时任务,比写死crontab更适合答辩时现场演示。安装时建议建独立虚拟环境,不要全局安装。

参数说明:这里没写具体补丁版本号是因为毕业设计不是生产系统,大版本锁住、小版本保持系统默认即可。执行pip install -r requirements.txt后,用python -c "import django; print(django.get_version())"验证安装结果。

2.2 数据库表设计:舆情主表、关键词表和预警表怎么建

舆情系统的业务很集中,核心表就三张:舆情主表、关键词表、预警表。如果项目源码里给了更多表,也是在为这三个实体服务。设计时一定要想清楚一条舆情数据的生命周期:从采集到清洗,从判断正负面到触发预警,最后被人工归档。

舆情主表是所有爬虫数据的最终归宿,字段设计直接影响去重和查询速度:

CREATE TABLE `opinion_post` ( `id` INT NOT NULL AUTO_INCREMENT, `source` VARCHAR(20) NOT NULL DEFAULT 'bbs' COMMENT '来源: bbs/weibo/zhihu/others', `platform_id` VARCHAR(64) NOT NULL COMMENT '平台原始ID, 用于去重', `title` VARCHAR(255) DEFAULT '' COMMENT '标题', `content` LONGTEXT COMMENT '正文内容', `author` VARCHAR(64) DEFAULT '' COMMENT '作者', `publish_time` DATETIME NOT NULL COMMENT '发布时间(清洗后)', `crawl_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '抓取时间', `sentiment` TINYINT DEFAULT 0 COMMENT '-1负面, 0中性, 1正面', `sentiment_score` FLOAT DEFAULT 0 COMMENT '情感置信度0~1', `keyword_id` INT DEFAULT 0 COMMENT '命中的关键词ID', `status` TINYINT DEFAULT 0 COMMENT '0未处理, 1已归档', PRIMARY KEY (`id`), UNIQUE KEY `uk_source_platform` (`source`, `platform_id`), KEY `idx_publish_time` (`publish_time`), KEY `idx_sentiment` (`sentiment`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='舆情主表';

逻辑说明:platform_id存的是微博帖子的 mid、贴吧帖子 tid 这类平台原有编号,它加source组成唯一索引,去重时直接INSERT ... ON DUPLICATE KEY UPDATE或者用 Django 的get_or_create,不需要先查再插,性能和幂等都解决。publish_time走索引是因为所有趋势统计都按时间范围查询,不加索引后期图表接口必然慢。sentiment索引用于负面预警的秒级统计。

参数说明:utf8mb4不是可选项,帖子内容里频繁出现 emoji,utf8存不下会直接写入报错。publish_time建DATETIME而不是VARCHAR,否则第五章会讲到的“刚刚、3小时前”这类相对时间就没法直接参与查询。

预警表单独拆出来的原因:预警记录是“事件快照”,它记录触发时刻的环境,而舆情主表是持续更新的流。两者混在一张表里,后面做历史回溯会非常痛苦:

CREATE TABLE `opinion_alert` ( `id` INT NOT NULL AUTO_INCREMENT, `level` TINYINT DEFAULT 2 COMMENT '1提示, 2警告, 3严重', `content` VARCHAR(500) NOT NULL COMMENT '预警内容', `related_count` INT DEFAULT 0 COMMENT '触发时负面条数', `keyword_id` INT DEFAULT 0 COMMENT '关联关键词', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='舆情预警记录表';

逻辑说明:related_count记录触发当时的负面数量,是为了事后复盘“这个预警是为什么触发的”。很多半路做的项目会漏掉这个字段,上线后面对一条预警完全想不起来当时的舆情规模,这就是典型的返工点。

2.3 目录结构与 settings 配置:拿到 zip 包后第一步看什么

毕设项目的目录通常比生产项目随意,但一个能顺利跑起来的系统,结构应该有明显分层。下面的布局是这类系统最常用的“标准答案”:

campus_opinion/ ├── manage.py ├── requirements.txt ├── config/ # 项目配置: settings/urls/celery │ ├── settings.py │ ├── urls.py │ └── celery.py ├── apps/ │ ├── opinions/ # 舆情业务: models/tasks/crawler │ └── alerts/ # 预警业务 ├── scripts/ │ ├── crawl/ # 爬虫入口 │ ├── analysis/ # 分词/情感/词云脚本 │ └── train_sentiment.py # 情感模型训练脚本 ├── static/ ├── templates/ ├── data/ # 停用词表/自定义词典/标注语料 │ ├── stopwords.txt │ └── campus_words.txt └── logs/ # 采集日志和任务日志

拿到 zip 后的第一件事不是建虚拟环境,而是看requirements.txt和settings.py里的数据库配置。settings.py里至少要有下面这几个配置块,否则即使代码没问题也跑不通:

# config/settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', # ... 其他内置模块 'apps.opinions', 'apps.alerts', 'django_celery_beat', ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'campus_opinion', 'USER': 'root', 'PASSWORD': '123456', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } } CELERY_BROKER_URL = 'redis://127.0.0.1:6379/1' CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/2' CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'

逻辑说明:INSTALLED_APPS里挂上django_celery_beat,是为了能在 Django admin 后台里动态配置采集周期。OPTIONS里的charset必须写,否则即使数据库建库时指定了utf8mb4,Django 连接时也可能按utf8走,热门帖子里带 emoji 的文本会直接抛Incorrect string value。Redis 用 db1 和 db2 分开业务缓存与结果存储,避免 key 互相干扰。

参数说明:数据库账号密码占位123456只是示例,实际部署时改成自己的。检查配置最快要领是在项目根目录执行python manage.py check,它会直接报出实例化错误和配置缺失,比盲跑runserver有效率。

3. 舆情采集与清洗:把微博、贴吧、校园论坛的讨论转成结构化数据

很多人以为舆情系统的难点是爬虫,其实难点是把“长得完全不像表格”的网页文本变成能入库的干净字段。采集和清洗是一对搭档:采集层负责拿原始数据,清洗层负责把原始数据变成可分析的素材。这一章我会给出一套 Celery 定时采集 + 三层清洗的最小实现。

3.1 采集任务:用 Celery 定时把关键词搜索结果写入 MySQL

采集任务在毕业设计里最常见的落法是:在后台维护关键词,由 Celery Beat 定时触发任务去各平台检索,数据入库后再走情感分析。

# apps/opinions/tasks.py from celery import shared_task from apps.opinions.crawler import fetch_keyword_page from apps.opinions.models import OpinionPost @shared_task(name='opinions.crawl_keyword') def crawl_keyword(keyword: str, keyword_id: int): """定时采集任务:抓取关键词页面并去重入库""" rows = fetch_keyword_page(keyword, max_pages=3) for row in rows: defaults = { 'title': row.get('title', ''), 'content': row.get('content', ''), 'author': row.get('author', ''), 'publish_time': row.get('publish_time'), 'keyword_id': keyword_id, } OpinionPost.objects.get_or_create( source=row['source'], platform_id=row['platform_id'], defaults=defaults, )

逻辑说明:get_or_create依据前面建的唯一索引(source, platform_id)做幂等插入,同一帖子重复抓取不会变成两条记录。fetch_keyword_page返回的是已经过基础处理的列表,每个元素至少带source、platform_id、content、publish_time四个字段。

参数说明:max_pages=3是控制单次任务请求量的核心参数,毕设演示时 3 页足够展示趋势,不需要追求全量;调大会拉长任务时间并显著增加触发反爬的概率。任务名name='opinions.crawl_keyword'是给 Celery Beat 定时配置使用的标识,不要随意改动。

采集函数本身不必写得非常复杂,但要留好频率控制和失败退避:

# apps/opinions/crawler.py import time import requests from bs4 import BeautifulSoup def fetch_keyword_page(keyword: str, max_pages: int): """抓取目标站点的搜索结果页, 仅用于教学演示""" session = requests.Session() session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' }) results = [] for page in range(1, max_pages + 1): try: params = {'q': keyword, 'page': page} resp = session.get( 'https://example-campus.edu/search', params=params, timeout=5 ) if resp.status_code != 200: time.sleep(5) continue soup = BeautifulSoup(resp.text, 'html.parser') for item in soup.select('.search-result-item'): results.append(parse_item(item, keyword)) # 均匀间隔, 避免瞬时压力 time.sleep(1.5 + page * 0.3) except requests.RequestException as exc: print(f'[crawler] keyword={keyword} page={page} error: {exc}') time.sleep(3) return results

逻辑说明:这里用requests.Session而不是全局requests.get,是为了让 Cookie 在同一次任务的多个翻页请求之间保持连续,很多站点翻页后回跳登录页就是因为没有复用 Session。select('.search-result-item')是按目标站点结构调整的选择器示例,实际部署时按抓取对象的 HTML 结构修正。异常分支里至少打印日志并 sleep 3 秒,避免某个关键词出错导致整条任务链中断。

参数说明:timeout=5是单次请求最长的等待秒数,超过即抛异常,防止某个慢接口把 Celery worker 占住。time.sleep(1.5 + page * 0.3)是“随页码递增的间隔”,翻到越深越慢,这是对连续翻页最友好的节奏,比固定 2 秒更容易避开频率检测。

提示:毕业设计中的爬虫应使用公开页面、测试账号并在合规范围内控制频率;不要绕过登录鉴权,不要抓取非公开数据。

3.2 文本清洗:去噪、去重、分词和“刚刚 / 3小时前”这类时间怎么处理

抓下来的原始文本不能直接用,里面掺杂着话题标签、@提醒、转发标记、链接和营销号模板。若不清理,情感分析会把“转发微博”这种词也当成有效内容。

# scripts/analysis/clean.py import re import jieba def clean_text(raw: str) -> str: if not raw: return '' text = re.sub(r'#.*?#', '', raw) # 去掉 #话题# text = re.sub(r'@[\w-]+', '', raw) # 去掉 @用户名 text = re.sub(r'https?://\S+', '', text) # 去掉链接 text = re.sub(r'\s+', ' ', text) # 合并空白 return text.strip() STOPWORDS = set(open('data/stopwords.txt', encoding='utf-8').read().split()) def tokenize(text: str): words = jieba.lcut(clean_text(text)) return [w for w in words if w not in STOPWORDS and len(w) > 1]

逻辑说明:四个正则分别对应校园舆情里最常出现的噪声。话题标签一般直接删,但也可以改成记录标签文本作为分类输入,毕业设计阶段先删掉最简单。len(w) > 1是为了筛掉单字,校园讨论里“菜”“贵”这种单字在统计词云时没有合并价值,保留反而让图变得稀疏。

参数说明:data/stopwords.txt是自己维护的停用词表,常见平台词“觉得”“就是”“一个”以及网络噪音“哈哈哈哈”“转发”都放进去。停用词表不用追求大而全,按自己采集到的数据反馈持续补充才是正常心态,几十条也很够用。

时间字段是最容易踩坑的地方:检索结果里的发布时间有“刚刚”“3 分钟前”“昨天 22:10”、绝对时间2024-05-20 08:30三种形态,全部存成字符串会让趋势查询无法按时间聚合。这里写一个最小兼容函数:

# scripts/analysis/time_parse.py from datetime import datetime, timedelta def parse_relative_time(text: str) -> datetime: """把相对时间字符串转换为标准 datetime, 解析失败时给过去时间""" now = datetime.now() text = text.strip() if '刚刚' in text: return now if '分钟前' in text: minutes = int(text.split('分钟前')[0]) return now - timedelta(minutes=minutes) if '小时前' in text: hours = int(text.split('小时前')[0]) return now - timedelta(hours=hours) if '昨天' in text: hour_str = text.replace('昨天 ', '') hour = datetime.strptime(hour_str, '%H:%M') return now - timedelta(days=1, hours=now.hour - hour.hour, minutes=now.minute - hour.minute) try: return datetime.strptime(text, '%Y-%m-%d %H:%M') except ValueError: return now - timedelta(days=3)

逻辑说明:now - timedelta(days=1, hours=now.hour - hour.hour, ...)这段是把“昨天 22:10”换算成昨天的真实时刻,不这样处理,所有昨天帖子的publish_time都会集中在今天零点,趋势曲线会出现每天 0 点过山车式的假象。最后的except ValueError把无法识别的时间兜底成三天前,保证入库不会因为脏时间崩溃,但兜底数据不会污染统计主区间。

参数说明:这个函数的输出精度到分钟,因为舆情趋势分析不需要秒级精度。实际毕设里,可以把调用包一个try/except再包一层,因为个别平台会返回“2024-05-20”这种没有时分的数据。

3.3 关键参数:请求间隔、重试退避和数据量上限的取舍

采集层的参数直接影响两个结果:数据能不能抓全、账号能不能活到答辩那天。这里有一张我常用的参数基准表,适合校园舆情这种中小规模数据量:

参数建议值说明
请求间隔1.5s ~ 3s 随机固定间隔反而更容易被识别
单关键词最大页数3 页足够展示趋势,避免捞历史数据
单任务超时5s防止慢接口占住 worker
重试次数3 次超过后跳过该页,不阻塞任务
退避策略2s 指数增长连续失败时自动拉长间隔
每轮任务间隔30~60 分钟校园舆情不必实时,太频繁会封

参数说明:间隔不要写成固定sleep(2),而是在 1.5 到 3 秒之间取随机值,曲线更接近人工操作。重试超过三次就放弃当前页,毕设场景里数据完整性优先级低于任务稳定性,一页失败不应该拖垮整个关键词任务。任务间隔 30 分钟是平衡点,演示时也能接受在几分钟内看到新数据被采进来。

限制数据量上限在毕设里特别容易被忽略。爬虫跑一天可能抓回几万条无意义转发,入库前在任务入口按max_items_per_run=200截断:

def fetch_keyword_page(keyword: str, max_pages=3, max_items=200): ... if len(results) >= max_items: break return results[:max_items]

逻辑说明:这个截断不是数据完整性上的最佳选择,但毕设阶段能防止本地 MySQL 被几 GB 的“自动转发”文本撑爆,也能保证舆情统计在可控的数据范围内。参数max_items放在函数签名里而不是写死,是为了测试时可以传入更小值快速跑通链路。

4. 情感分析与趋势可视化:从“有数据”到“看得懂数据”

数据落库只是系统完成了一半,另一半是把数据翻译成“最近校园里的负面声音在变多还是变少”。情感分析决定舆情是否触发预警,可视化决定答辩时能否三分钟讲清楚结论。

4.1 情感模型:SnowNLP 还是自定义词典,校园语料的识别策略

情感分析在毕设项目里有两种常见路线:直接用现成的 SnowNLP,或者引入 BERT 一类的深度学习模型。前者开箱即用但默认模型是在电商评论语料上训练的,对“食堂又涨价了”“宿舍热水总是坏”这类校园口语判断经常失灵;后者效果更好,但配置 CUDA、加载预训练权重这一套下来,很多毕设时间就耗进去了。

折中方案是不换库,只换模型:用自己标注的校园语料重训一个 SnowNLP 情感分类器,准确率能提升一截,还保留了轻量部署的优势。

# scripts/train_sentiment.py from snownlp import sentiment # neg.txt 和 pos.txt 每行一条人工标注的校园舆情文本 sentiment.train('data/train_neg.txt', 'data/train_pos.txt') sentiment.save('models/campus_sentiment.marshal')

逻辑说明:train的两个文件分别存放负面和正面语料,行数建议各 2000 条以上。语料可以从之前采集的数据里抽样再人工标记,也可以自己随手写校园场景句子补足。训练完成后save到自定义路径,预测时就不再用默认的电商模型。

# apps/opinions/sentiment.py from snownlp.sentiment import Sentiment classifier = Sentiment() classifier.load('models/campus_sentiment.marshal') def predict_sentiment(text: str) -> int: if not text: return 0 score = classifier.predict(text) if score > 0.6: return 1 if score < 0.4: return -1 return 0

逻辑说明:classifier.predict返回 0 到 1 之间的正情感概率。阈值设成 0.6 和 0.4 是留出 0.4~0.6 的中间地带,避免“其实还行”这类模糊文本被强行切成正或负。实际标注数据时你会发现,校园舆情里中性内容占比很高,强切反而让统计失真。

参数说明:阈值可以按实际预测结果微调。如果负面召回太少就把下阈值升到 0.5;如果正面误报多就把上阈值升到 0.65。这类修改要记在项目说明里,答辩时老师问“阈值为什么取 0.6”你有据可答。

网络用语是另一个重灾区,“排雷”“避雷”“懂的都懂”在默认词典里完全没有。解决方法是给 jieba 加校园词典,再接一层关键词兜底:

# apps/opinions/sentiment.py 追加 import jieba jieba.load_userdict('data/campus_words.txt') STRONG_NEGATIVE_WORDS = ['乱收费', '吃出虫子', '半夜断电', '宿舍漏水', '挂科率'] def predict_sentiment(text: str) -> int: if any(word in text for word in STRONG_NEGATIVE_WORDS): return -1 base_score = 0 # 先走自定义模型, 逻辑与前面相同 ...

逻辑说明:自定义词典文件里每一行是“词汇 词频 词性”,例如乱收费 20 n。它是让 jieba 正确切分新词的基础。强负面词列表是“规则兜底”,当文本里出现明确负面实义词时直接判负,不依赖概率模型。毕业设计里这种规则与模型叠加的做法,比单纯追求算法复杂度更好落地,也更容易向答辩老师解释。

4.2 预警规则:负面舆情 1 小时超过阈值自动推送

预警是舆情系统区别于普通信息管理系统的关键功能。它的规则不复杂,难在阈值怎么取、怎么避免“狼来了”效应。

# apps/alerts/checker.py from datetime import timedelta from django.utils import timezone from apps.opinions.models import OpinionPost from apps.alerts.models import OpinionAlert CHECK_WINDOW_MINUTES = 60 WARN_THRESHOLD = 5 def check_alerts_for_keyword(keyword_id: int): start = timezone.now() - timedelta(minutes=CHECK_WINDOW_MINUTES) recent_negatives = OpinionPost.objects.filter( keyword_id=keyword_id, sentiment=-1, publish_time__gte=start, ).count() if recent_negatives >= WARN_THRESHOLD: OpinionAlert.objects.create( level=2, content=f'关键词 {keyword_id} 近1小时内负面舆情 {recent_negatives} 条', related_count=recent_negatives, keyword_id=keyword_id, )

逻辑说明:预警的本质是“滑动窗口计数”。1 小时内负面数量达到 5 条就产生一条预警记录,窗口信息存在related_count里。这个实现里没做幂等,同一小时重复执行会重复建预警,实际使用时可以在预警表加唯一约束或者用get_or_create按(keyword_id, created_at)去重,毕设阶段可以先不处理。

参数说明:CHECK_WINDOW_MINUTES=60和WARN_THRESHOLD=5是两个可调参数。阈值 5 对校园场景偏敏感,适合演示时快速看到预警效果;真实部署建议改成 20 条/小时。想更精细可以做“负面占比预警”,即 1 小时内负面数除以总舆情数超过 30% 才触发,这个公式对突发“一条大热点转发几百条”的场景更准确,也更好讲。

4.3 可视化面板:用 ECharts 显示趋势、词云和热点排行

可视化是答辩的加分项,也是很多人翻车的地方。ECharts 比 Matplotlib 更适合网页端展示,因为可以直接在 Django 模板里渲染,不用先生成图片文件。趋势图是必选项,它直接展示系统“从数据到结论”的能力:

<!-- templates/opinions/trend.html --> <div id="sentiment-trend" style="height:400px;"></div> <script src="/static/js/echarts.min.js"></script> <script> fetch('/api/opinions/trend?days=30') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('sentiment-trend')); chart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: ['负面', '正面'] }, xAxis: { type: 'category', data: data.days }, yAxis: { type: 'value' }, series: [ { name: '负面', type: 'bar', data: data.neg }, { name: '正面', type: 'line', data: data.pos } ] }); }); </script>

对应的数据接口用 Django 的JsonResponse返回,注意一个关键参数:

# apps/opinions/views.py from django.http import JsonResponse from django.db.models import Count from django.utils import timezone from datetime import timedelta def trend_api(request): days = int(request.GET.get('days', 30)) start = timezone.now() - timedelta(days=days) rows = OpinionPost.objects.filter(publish_time__gte=start) \ .extra(select={'day': "DATE_FORMAT(publish_time, '%%Y-%%m-%%d')"}) \ .values('day', 'sentiment') \ .annotate(count=Count('id')) result = {'days': [], 'neg': [], 'pos': []} day_map = {} for row in rows: day_map.setdefault(row['day'], {1: 0, -1: 0, 0: 0}) day_map[row['day']][row['sentiment']] = row['count'] for day, counts in sorted(day_map.items()): result['days'].append(day) result['neg'].append(counts.get(-1, 0)) result['pos'].append(counts.get(1, 0)) return JsonResponse(result, json_dumps_params={'ensure_ascii': False})

逻辑说明:核心在extra里的DATE_FORMAT,它把publish_time按天截断,让查询结果以天为粒度聚合,返回给前端的数据结构直接对齐 ECharts 的xAxis.data和series.data。sorted(day_map.items())保证日期按字符串升序排列,因为YYYY-MM-DD的字符串排序与时间顺序一致,这里不需要额外转时间对象。

参数说明:json_dumps_params={'ensure_ascii': False}这个参数很重要。Django 默认的JsonResponse会把中文转成\uXXXX转义,前端显示正常,但浏览器调试时满屏反斜杠影响排查,设置成False让接口直接返回可读中文。词云可以走后端生成wordcloud图片再渲染到模板,也可以前端用 echarts-wordcloud 插件实现,毕设里后端生成图片更省资源,接口返回图片 URL 即可。

5. 避坑指南:校园舆情系统跑不起来时,先查这 5 个地方

这部分是血泪经验汇总。以下 5 个问题把毕业设计同学卡住的比例最高,按发生频率从高到低排列,每一条都按“现象 → 原因 → 解决”来写。

5.1 Python 3.12 装不上依赖:用 py -3.10 建虚拟环境

现象:pip install mysqlclient或pip install jieba时,终端报Microsoft Visual C++ 14.0 is required,或者snownlp装完导入报ImportError。

原因:新装的 Python 3.12 里部分第三方包没有预编译的 wheel,会即时尝试本地编译,而 Windows 开发环境通常缺 C++ 构建工具。即使装上了,某些老包对 3.12 的 ABI 也不兼容。

解决:用 Python 3.10 建虚拟环境,多数包都有现成的 wheel:

py -3.10 -m venv venv venv\Scripts\activate python --version pip install -r requirements.txt

逻辑说明:py -3.10是 Windows 上 Python Launcher 的调用方式,系统里装了 3.10 的解释器就能直接用。创建独立venv而不是全局安装,一是避免污染系统 Python,二是requirements.txt的版本依赖只在虚拟环境里生效,测试完可以直接删掉重建。装完依赖后用pip freeze看版本是否与requirements.txt一致,防止 pip 自动升级了子依赖。

5.2 Celery 任务不执行:Windows 下必须加 --pool=solo

现象:celery beat启动了,定时任务也在日志里出现,但采集任务迟迟不跑,worker 终端毫无输出。

原因:Windows 上 Celery 5.x 默认的prefork进程池与 Windows 的进程启动机制不兼容, worker 起不来但进程没退出,导致任务一直积压在队列里不消费。

解决:手动启动 worker 时限制用 solo 线程池,并和包版本一并确认:

celery -A config worker --loglevel=info --pool=solo celery -A config beat --loglevel=info

逻辑说明:--pool=solo让 Celery 在单进程内顺序执行任务,避免 Windows 下多进程事件循环的兼容问题。缺点是并发能力下降,但毕业设计的数据量完全够用。beat 是调度器,它只管把任务按计划发到 Redis,真正执行靠 worker,两个终端都要开,只开 beat 不 worker 是排行榜第一的错误姿势。

5.3 MySQL 中文乱码:建库没加 utf8mb4

现象:后台页面上所有中文内容显示成????,或者Django RuntimeError: Incorrect string value。

原因:建数据库时没有指定字符集,MySQL 默认latin1,存不了中文。

解决:建库时一次性指定,而不是建完再改:

CREATE DATABASE campus_opinion DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

如果库已经建好,可以用一条命令补救:

ALTER DATABASE campus_opinion CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

逻辑说明:建表时DEFAULT CHARSET=utf8mb4也写了,但表依赖的库字符集不对时,表依然可能继承错误的默认值。ALTER DATABASE只改库的默认字符集,已经存在的表还需要逐表ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4,毕设阶段刚建完库直接改库再同步表结构即可。

5.4 爬虫半小时就封:频率、 Cookie 和功能开关

现象:采集任务开始后十几分钟,请求状态码从 200 变成 403,之后所有采集结果为空。

原因:请求间隔固定且太短,站点按 IP 和 User-Agent 的组合判断为脚本行为。另一个常见原因是登录态过期,没有 Cookie 的 Request 被重定向到登录页。

解决:控制频率是第一次修复,把单次抓取间隔调到 2 秒以上,并加上随机抖动。同时,在settings.py里做一个开关,方便演示时快速切断采集任务:

# config/settings.py CRAWLER_ENABLED = True # apps/opinions/tasks.py 中增加 from django.conf import settings if not settings.CRAWLER_ENABLED: return {'skip': 'crawler disabled'}

逻辑说明:这个开关的价值在于,答辩现场如果网络状态不稳定,你可以直接说明“采集服务当前关闭,演示用静态数据”,而不是现场重试十几分钟。Cookie 的问题更隐蔽:登录后的 Cookie 过期时间可能只有一天,建议把 Cookie 导出到配置文件里,并在失败日志里提示“请更新 Cookie”,而不是默默重试。

5.5 情感分析全是中性:词典没更新、训练语料不够

现象:可视化面板里正负面几乎没有波动,折线图贴近零轴,除了内容全是中性的线上系统。

原因:SnowNLP 默认模型是电商语料,校园场景的“涨价”“熄灯”“挂科率”等词汇在它的词典里是低频词;另外标注语料太少,模型没有学到校园讨论的语言习惯。

解决:分两步走:先用 4.1 节的自定义词典让 jieba 正确切词,再扩充训练集。如果在演示前只有一个晚上的时间,优先做“规则兜底”:把明确负面的词汇加进STRONG_NEGATIVE_WORDS。效果立竿见影,而且逻辑可以被答辩评委理解。

# data/campus_words.txt 内容示例 食堂 20 n 宿管 15 n 乱收费 30 n 停水 20 v

逻辑说明:词典格式是“词 频次 词性”,频次是 jieba 切词时的优先级,词性要按真实词性写,n是名词,v是动词。训练语料则是另一个坑,标注 100 条和标注 1000 条的结果天壤之别,建议从采集库里抽取最热门的 500 条人工标一遍,加上规则词就能让输出分布明显分化。

6. 用它完成答辩:验证脚本、演示流程与可选的进阶方向

这章说怎么把系统“变成”你真正能做主的东西。毕设答辩的评价标准不是功能有多新,而是你在现场能复现和解释,所以完整演示链路比花哨功能更值钱。

验收前先写三个最小测试用例,只测最核心的业务逻辑,作为“代码能跑”的证据:

# apps/opinions/tests.py from django.test import TestCase from apps.opinions.sentiment import predict_sentiment from scripts.analysis.clean import clean_text class OpinionTests(TestCase): def test_url_removed(self): self.assertEqual( clean_text('食堂涨价了 https://t.cn/abc'), '食堂涨价了' ) def test_negative_sentiment(self): self.assertEqual(predict_sentiment('半夜断电还不管'), -1) def test_alert_record_created(self): # 构造数据并触发预警, 断言 OpinionAlert 数量+1 ...

逻辑说明:第一个用例验证清洗函数能去掉 URL;第二个用例验证强规则词能触发负面判断;第三个用例验证预警记录能被创建。三个用例覆盖从文本处理到业务动作的最小闭环,跑通python manage.py test就能证明系统核心逻辑成立,答辩时现场跑一遍比任何 PPT 截图都有说服力。

演示流程上,我建议按“造数据—看趋势—看预警”三步走:启动 Redis、worker、beat、runserver后,在后台添加一个演示关键词,手动触发一次采集任务,刷新趋势接口看到曲线变化;再插入几条负面文本触发阈值,让预警记录出现在后台列表里。全程控制在 5 分钟左右,一气呵成,不要在答辩现场踩长命令。

想继续加深度的话,有三个方向性价比最高:第一个是给预警加时序去重,同一关键词在一小时内只创建一条预警,这个改动小且能体现设计思考;第二个是引入 TF-IDF 加 DBSCAN 的简单事件聚类,把零散帖子按主题合并,让“食堂涨价”和“超市打折”分开;第三个是接企业微信机器人 webhook,让预警直接推到手机,这是最直观的展示亮点,也就几行代码。

我每次拿到一套爬虫类毕设源码,第一件事永远是确认 Python 版本与依赖关系,这在校园舆情系统里比任何算法调优都先决定生死。先跑通最小闭环,再谈优化,这套系统的价值就一定能讲清楚。希望帮到你。

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

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

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

立即咨询