简介:这是一套面向高校学生的Python网络舆情分析系统完整项目,可作为Python毕业设计或课程设计使用,适合具备Python基础、需要完成综合实践项目的学习者。项目采用Python 3.7或3.8开发,配合PyCharm与Navicat,数据库为MySQL,前后端代码齐全,功能完善、界面美观、操作简单,具有较高的实际应用价值。压缩包共287个文件,约75.63MB,包含42个py源码文件、35个pyc编译文件、34个js脚本、15个css样式、12个html页面,以及gif、jpg、png等界面素材和sql数据库脚本、csv数据文件、pb模型文件等,覆盖前端页面、后端逻辑与数据存储各环节。项目经过严格调试,可正常运行,部署时用PyCharm导入、pip安装依赖、配置数据库密码后即可启动。目前已有124人学习下载,适合需要完整项目参考、快速搭建舆情分析系统并完成毕业设计的学生。
1. 从一份毕业设计压缩包说起:网络舆情分析系统到底在分析什么
打开一份名为「基于python的网络舆情分析系统」的毕业设计压缩包,多数人第一反应是找requirements.txt和数据库脚本,然后卡在「数据从哪来」这一步。这个标题真正要解决的问题不是「做一个网站」,而是把散落在社交平台、新闻评论区的非结构化文本,变成能按时间、情感、关键词三个维度下钻的结构化结论。它适合两类人:一类是正在做毕业设计、需要一套能跑通、能答辩、能写进论文的系统骨架;另一类是刚接触 Python 数据分析、想拿一个完整项目练手的入门者。核心链路只有四段——采集、清洗、分析、可视化,缺任何一段,系统就退化成一张静态图表。下面按我实际搭过几套类似系统的顺序,把每一段拆开讲。
2. 技术选型与数据采集:为什么用 Python 而不是 Java 或 PHP
2.1 选型对比:Flask + SQLite 与 Django + MySQL 的取舍
毕业设计场景下,选型的第一约束不是性能,是「能不能在一台普通笔记本上跑起来,并且让答辩老师看懂」。常见做法是 Flask 做后端、SQLite 做存储、ECharts 做前端图表,这套组合的依赖最少,pip install三条命令就能起步。如果学校要求必须用 MySQL,那就换成 Flask + MySQL,代码改动量在数据库连接层,不超过 20 行。
| 方案 | 依赖复杂度 | 适合场景 | 主要代价 |
|---|---|---|---|
| Flask + SQLite | 低 | 单机演示、数据量 10 万条以内 | 并发写入弱,不适合多用户 |
| Flask + MySQL | 中 | 需要展示数据库设计、数据量较大 | 要装服务、配账号密码 |
| Django + MySQL | 高 | 要求完整后台管理、用户权限 | 学习曲线陡,答辩讲不清 |
我一般会先问一句:你的数据量级和答辩重点是什么?如果重点是「分析算法」,选 SQLite 把精力留给情感分析;如果重点是「系统架构」,选 MySQL 把 ER 图做扎实。
2.2 采集层:用 requests + BeautifulSoup 抓取评论的最小实现
采集是第一个翻车高发区。很多同学直接对着页面requests.get,拿回来一堆空数据,原因是目标页面是前端渲染的。稳妥做法是先确认数据是否在 HTML 源码里,在的话用 BeautifulSoup 解析;不在的话找接口返回的 JSON。
import requests from bs4 import BeautifulSoup import time import random HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" } def fetch_comments(url, max_page=5): """抓取评论列表,返回 [(用户名, 评论内容, 时间), ...]""" results = [] for page in range(1, max_page + 1): # 分页参数名各站不同,常见是 page / p / offset resp = requests.get(url, params={"page": page}, headers=HEADERS, timeout=10) if resp.status_code != 200: print(f"第 {page} 页返回 {resp.status_code},跳过") continue soup = BeautifulSoup(resp.text, "html.parser") # 选择器要按实际页面结构调整,这里用 class 示例 for item in soup.select(".comment-item"): user = item.select_one(".user-name") content = item.select_one(".comment-text") ts = item.select_one(".time") if content: results.append(( user.get_text(strip=True) if user else "匿名", content.get_text(strip=True), ts.get_text(strip=True) if ts else "" )) # 随机间隔,降低被限流的概率 time.sleep(random.uniform(1.5, 3.5)) return results逻辑说明:HEADERS里带 User-Agent 是基本礼貌,很多站点没有它会直接返回 403。params传分页参数,比手动拼 URL 更清晰。time.sleep的随机区间是关键,固定间隔容易被识别为脚本行为。参数调整上,max_page控制采集深度,毕业设计演示 5 到 10 页足够;timeout设 10 秒,避免单页卡死拖垮整个流程。
提示:采集前先确认目标站点的 robots.txt 和使用条款,只采集公开、允许抓取的数据,不要碰需要登录才能看的内容。
2.3 数据落库:SQLite 建表与批量插入
采集回来的数据要落库,否则程序一关就没了。SQLite 不需要额外服务,一个文件就是数据库,适合毕业设计。
import sqlite3 def init_db(db_path="opinion.db"): conn = sqlite3.connect(db_path) cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, content TEXT NOT NULL, publish_time TEXT, source TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) # 内容字段建索引,后面按关键词查询会快很多 cur.execute("CREATE INDEX IF NOT EXISTS idx_content ON comments(content)") conn.commit() return conn def save_comments(conn, rows, source="demo"): cur = conn.cursor() cur.executemany( "INSERT INTO comments (username, content, publish_time, source) VALUES (?, ?, ?, ?)", [(u, c, t, source) for u, c, t in rows] ) conn.commit() print(f"写入 {len(rows)} 条")逻辑说明:executemany比循环execute快一个数量级,几千条数据差距明显。idx_content索引是为后面的关键词统计服务,没有索引时LIKE '%关键词%'在万级数据上会明显变慢。参数上,source字段用来区分不同来源,方便后续按平台做对比分析。
3. 文本清洗与情感分析:把评论变成可计算的分数
3.1 中文分词与停用词过滤
原始评论里全是表情、@、链接和「哈哈哈哈」,不洗一遍,情感分析结果会严重失真。常见做法是 jieba 分词加一份停用词表。
import jieba import re STOPWORDS = set() with open("stopwords.txt", encoding="utf-8") as f: for line in f: STOPWORDS.add(line.strip()) def clean_text(text): """去掉 URL、@、表情符号和多余空白""" text = re.sub(r"http\S+", "", text) text = re.sub(r"@[\w\u4e00-\u9fa5]+", "", text) text = re.sub(r"\[.*?\]", "", text) # 表情占位符 text = re.sub(r"\s+", " ", text) return text.strip() def tokenize(text): words = jieba.lcut(clean_text(text)) return [w for w in words if w not in STOPWORDS and len(w) > 1]逻辑说明:clean_text里四条正则分别处理链接、@用户、表情占位符和空白。len(w) > 1过滤掉单字,因为中文单字噪声大。停用词表建议用哈工大停用词表,再手动补上「转发」「点赞」这类平台词。
3.2 基于 SnowNLP 的情感打分与阈值校准
情感分析是这类系统的核心卖点。SnowNLP 开箱即用,但它的默认阈值不一定适合你的数据,必须校准。
from snownlp import SnowNLP def sentiment_score(text): """返回 0~1 的情感分,越接近 1 越正面""" cleaned = clean_text(text) if not cleaned: return None return SnowNLP(cleaned).sentiments def label(score, pos=0.6, neg=0.4): """按阈值打标签,阈值需要根据实际数据调整""" if score is None: return "无效" if score >= pos: return "正面" if score <= neg: return "负面" return "中性"逻辑说明:SnowNLP 返回的是 0 到 1 的连续值,直接按 0.5 切会得到大量「中性」,区分度差。我一般会先跑 200 条人工标注样本,看正面和负面各集中在哪个区间,再定pos和neg。参数上,pos=0.6、neg=0.4是常见起点,如果数据里负面评论多,可以把neg提到 0.45。
注意:SnowNLP 在电商评论上表现尚可,在新闻评论或论坛语境下准确率会下降,答辩时如果被问到,可以坦诚说明并给出人工抽检的准确率数据,比硬撑更可信。
3.3 关键词提取与词云数据准备
除了情感,舆情系统还要回答「大家在讨论什么」。TF-IDF 是性价比最高的方案。
from sklearn.feature_extraction.text import TfidfVectorizer def extract_keywords(texts, top_k=30): """从评论文本列表里提取 TF-IDF 关键词""" tokenized = [" ".join(tokenize(t)) for t in texts] vectorizer = TfidfVectorizer(max_features=1000) matrix = vectorizer.fit_transform(tokenized) scores = matrix.sum(axis=0).A1 words = vectorizer.get_feature_names_out() ranked = sorted(zip(words, scores), key=lambda x: x[1], reverse=True) return ranked[:top_k]逻辑说明:tokenize复用前面的分词函数,保证清洗逻辑一致。max_features=1000限制词表大小,避免内存爆掉。matrix.sum(axis=0)把每篇文档的 TF-IDF 值按词累加,得到全局权重。返回的ranked直接喂给前端词云组件即可。
4. 可视化与 Web 展示:让答辩老师三秒看懂结果
4.1 Flask 路由设计与接口划分
后端接口不用多,四个就够:总览统计、情感分布、关键词排行、时间趋势。
from flask import Flask, jsonify, render_template import sqlite3 app = Flask(__name__) def query(sql, args=()): conn = sqlite3.connect("opinion.db") conn.row_factory = sqlite3.Row rows = conn.execute(sql, args).fetchall() conn.close() return [dict(r) for r in rows] @app.route("/") def index(): return render_template("index.html") @app.route("/api/sentiment") def api_sentiment(): rows = query("SELECT content FROM comments") dist = {"正面": 0, "中性": 0, "负面": 0} for r in rows: s = sentiment_score(r["content"]) dist[label(s)] += 1 return jsonify(dist) @app.route("/api/keywords") def api_keywords(): rows = query("SELECT content FROM comments") texts = [r["content"] for r in rows] return jsonify(extract_keywords(texts, top_k=30))逻辑说明:query封装了连接和关闭,避免每个接口重复写。row_factory = sqlite3.Row让结果能直接转字典。情感接口每次请求都重新计算,数据量大时应该加缓存,毕业设计数据量下可以接受。
4.2 ECharts 前端接入与数据绑定
前端用 ECharts 的饼图和词云,数据从上面的接口拿。
// 情感分布饼图 fetch('/api/sentiment') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('sentiment')); chart.setOption({ series: [{ type: 'pie', data: Object.entries(data).map(([name, value]) => ({ name, value })), // 正面绿、负面红,符合直觉 color: ['#52c41a', '#faad14', '#f5222d'] }] }); });逻辑说明:Object.entries把后端返回的{正面: 120, 中性: 80, 负面: 45}转成 ECharts 需要的数组格式。颜色顺序要和data的键顺序对应,否则会出现「正面显示红色」这种低级错误。词云图需要额外引入echarts-wordcloud插件,数据格式是[{name, value}],直接把/api/keywords的返回结果传进去。
4.3 时间趋势折线图的 SQL 聚合
时间趋势要按天聚合,SQL 里用substr截取日期。
SELECT substr(publish_time, 1, 10) AS day, COUNT(*) AS total, SUM(CASE WHEN sentiment = '负面' THEN 1 ELSE 0 END) AS negative FROM comments WHERE publish_time IS NOT NULL GROUP BY day ORDER BY day;逻辑说明:substr(publish_time, 1, 10)假设时间格式是2024-01-15 10:30:00,截前 10 位得到日期。如果采集时时间格式不统一,先在清洗阶段标准化。SUM(CASE WHEN ...)是条件计数的标准写法,比跑两次查询高效。
5. 避坑与排查:那些让系统跑不起来的细节
5.1 采集返回空列表
现象:fetch_comments返回[],但浏览器里能看到评论。原因:页面是 JavaScript 渲染的,requests拿到的 HTML 里没有评论节点。解决:打开浏览器开发者工具的 Network 面板,找返回 JSON 的接口,直接请求接口;或者用 Selenium 渲染后再解析,但毕业设计里优先找接口,更轻量。
5.2 中文乱码
现象:数据库里存进去的中文变成????或乱码。原因:SQLite 默认编码没问题,问题通常出在requests的resp.text上,当响应头没声明编码时,requests会猜错。解决:显式指定resp.encoding = resp.apparent_encoding,再取resp.text。
5.3 SnowNLP 首次调用报错
现象:SnowNLP(text).sentiments抛异常或返回 0.5。原因:SnowNLP 依赖内置语料,首次调用会加载模型,如果安装不完整或文本为空会出问题。解决:先判断cleaned是否为空,空文本直接返回None;确认snownlp安装完整,必要时重装。
5.4 词云图不显示
现象:前端词云区域空白,控制台报series.wordCloud is not a function。原因:只引入了echarts,没引入echarts-wordcloud插件。解决:在 ECharts 之后引入echarts-wordcloud.min.js,并确认series.type写的是wordCloud,大小写敏感。
5.5 数据库锁死
现象:程序运行中报database is locked。原因:SQLite 同一时刻只允许一个写操作,采集线程和 Web 请求同时写会冲突。解决:采集和展示分开跑,采集完再启动 Web;或者把连接超时设长,sqlite3.connect("opinion.db", timeout=15)。
6. 进阶技巧:把情感分析准确率从 70% 提到 85% 的验证方法
SnowNLP 的默认模型在通用语料上训练,直接用在你的数据上,准确率大概七成。想提上去,最有效的不是换模型,是校准阈值加人工抽检。具体做法:从数据库随机抽 200 条,人工标注正面、中性、负面,然后跑一遍你的label函数,算混淆矩阵。
from sklearn.metrics import classification_report # y_true 是人工标注,y_pred 是系统输出 y_true = [...] # 200 条人工标签 y_pred = [label(sentiment_score(t)) for t in texts] print(classification_report(y_true, y_pred, labels=["正面", "中性", "负面"]))看classification_report的输出,重点看负面类的recall。如果负面召回低,说明阈值neg定高了,把负面评论误判成了中性,调低neg再跑一遍。这个过程迭代两三轮,准确率通常能到 85% 左右。如果学校允许,可以在论文里把这套校准流程写成一个小节,比单纯说「用了 SnowNLP」有说服力得多。
另一个技巧是分来源校准。不同平台的评论风格差异大,微博的负面表达和新闻评论区的负面表达不一样。按source字段分组,每组单独定阈值,比全局一个阈值更准。代价是要维护多套参数,毕业设计里如果数据来源单一,可以跳过。
最后说个我自己的习惯:每次改完阈值或清洗规则,都把当次的classification_report存一份到logs/目录,文件名带日期。答辩前翻一遍,能清楚说出「我调了哪几次、每次为什么调、效果变化多少」。这套记录比任何 PPT 都管用,老师问细节时直接翻日志,比临场编靠谱。希望帮到你。
本文还有配套的精品资源,点击获取