简介:这是一份面向毕业设计的Python网络舆情分析系统完整资料,包含可运行的源码、MySQL数据库文件以及配套说明文档。系统采用浏览器/服务器架构,后端基于Python开发,数据库使用MySQL,实现了用户登录、文本分析、文本管理、对比分析和用户管理等核心功能,覆盖了舆情项目从数据存储到可视化展示的完整链路。压缩包共包含二百九十个文件,其中有四十二个Python源文件、三十四个JavaScript脚本、十二个HTML页面、十五个CSS样式以及SQL数据库文件等,整体大小约九十三兆,目录按源码、数据库、文档分类存放,便于查找和学习。说明文档详细描述了开发环境、需求分析、总体设计、数据库设计、系统功能实现与测试过程,配合代码注释与界面截图,能够帮助读者理解整个项目的开发思路。目前已有一千四百九十二人学习下载,非常适合作为毕业设计参考、课程项目练手或二次开发的基础。
1. 网络舆情分析毕设,真正的验收点是“数据闭环”而不是模型精度
你可能会觉得网络舆情分析是个爬虫项目,真正做完才发现,它更像“数据采集-清洗-存储-分析-可视化”的一条流水线。我之前帮人调毕设时总被问:“老师凭什么相信这个结果?”答案从来不是模型多新,而是每一步数据怎么来、存到哪里、结论怎么生成。这套Python网络舆情分析系统,本质上给了你一条可复制的链路:从公开网页抓取评论,落入MySQL,再通过情感词典和朴素贝叶斯给出倾向,最后用图表和说明文档把整件事讲清楚。它适合有Python基础但没做过完整项目的人,或者想快速把主体跑通、留时间打磨论文的应届生。提前说一句反直觉的结论:后面返工最多的地方,往往不在模型,而在字符集、停用词和接口请求头这些细节上。
2. 舆情数据从哪来:采集目标、存储设计与清洗管线
做数据采集前,先花半天确认“源”。常见坑是网上教程里的目标网站已经改版,CSS选择器失效,反爬突然升级。所以优先选网页结构简单、公开访问的新闻频道或评论接口。这个毕设里我一般建议用 requests + BeautifulSoup 抓静态页面,能不用 Selenium 就不用 Selenium:它太重,内存占用高,答辩演示时还容易弹浏览器窗口添乱。爬虫重点不是说写多漂亮的框架,而是控制好请求头、超时、重试和入库去重。
2.1 抓取评论页:一个带重试的最小爬虫脚本
先给一个最小可跑的抓取函数。这里的关键不是“高并发”,而是让单条请求稳定、失败能自动重来。
import requests from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Referer": "https://target-site.com/", } def fetch_page(url, params=None, timeout=10): resp = requests.get(url, headers=HEADERS, params=params, timeout=timeout) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.textHEADERS 是给服务器看的“浏览器身份”,User-Agent 不能再用老旧的 Python-requests 默认值,Referer 最好从浏览器开发者工具里复制真实跳转来源。timeout 设 10 秒,避免一条坏请求卡死整个采集脚本。raise_for_status() 会在遇到 404/500 时直接抛异常,方便后面接重试逻辑。resp.apparent_encoding会根据页面内容猜编码,否则很多 GBK 页面会变成乱码。
再包一层重试和随机延时:
import time from random import uniform def fetch_with_retry(url, params=None, retries=3): for attempt in range(retries): try: return fetch_page(url, params) except requests.RequestException as exc: print(f"第{attempt + 1}次请求失败: {exc}") if attempt == retries - 1: raise time.sleep(uniform(1, 3))重试次数设 3 就够,sleep 取 1 到 3 秒的随机值,不是玄学,是给服务器一点喘息,防止连续请求被识别为异常流量。页面结构用soup.select("div.comment")这类选择器解析,写完先 print 两行预览再批量抓,能省掉大量返工。
2.2 入库表结构:MySQL 和 SQLite 的选择很影响答辩
毕设环境如果能装 MySQL,就不要用 SQLite。原因不是性能,而是答辩时大概率被问“为什么选这个数据库”。MySQL 可以答事务、并发连接、连接池、水平扩容,SQLite 只能答“单机够用”。论文和说明文档也会更好写,下面这套表结构就是为这个项目准备的。
CREATE TABLE article ( id INT AUTO_INCREMENT PRIMARY KEY, source VARCHAR(50) NOT NULL, title VARCHAR(255), content TEXT, publish_time DATETIME, source_url VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_source_url (source_url) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE comment ( id INT AUTO_INCREMENT PRIMARY KEY, article_id INT, content TEXT, dedup_key VARCHAR(32), sentiment TINYINT DEFAULT 0, publish_time DATETIME, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, UNIQUE KEY uk_dedup (dedup_key), KEY idx_comment_time (publish_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;几个设计点:charset 用 utf8mb4,utf8 存不了 emoji,而网络评论里 emoji 是常态;article 的 source_url 加唯一键,天然去重;comment 的 dedup_key 存清洗后文本的 MD5,避免重复入库;sentiment 用 TINYINT 存 -1/0/1,比字符串省空间,也方便 GROUP BY 统计。外键级联删除让清理实验数据更容易。
连数据库时不要每次新建连接,用连接池。这是数据库增删改查里最值得写进文档的细节之一:
from dbutils.pooled_db import PooledDB import pymysql pool = PooledDB( creator=pymysql, maxconnections=10, mincached=2, maxcached=5, blocking=True, host="localhost", user="root", password="your_password", database="opinion", charset="utf8mb4" ) def get_conn(): return pool.connection()maxconnections 控制连接上限,mincached 预热两个连接,blocking=True 表示连接不够时请求排队而不是直接报错。爬虫批量插入时省去了每次握手建连接的额外开销。依赖装两个包:pip install DBUtils PyMySQL。
2.3 清洗和入库:把“脏评论”变成可分析的语料
网页文本里通常混着 HTML 标签、URL、@用户,还有“ ”之类的实体。清洗的目标是把这些噪音去掉,同时保留中文和表情内容。
import re import hashlib def clean_comment(raw): raw = re.sub(r"<[^>]+>", "", raw) raw = re.sub(r"https?://\S+", "", raw) raw = re.sub(r"@[\w\u4e00-\u9fa5]+", "", raw) raw = raw.replace(" ", " ").strip() return raw def dedup_key(text): return hashlib.md5(text.encode("utf-8")).hexdigest()四个正则分别处理标签、网址、@用户和 HTML 空格实体。注意不要用str.replace去标签,正则更可靠。清洗完再做长度过滤和入库:
def save_comment(article_id, raw_text): clean = clean_comment(raw_text) if len(clean) < 2: return 0 key = dedup_key(clean) conn = get_conn() with conn.cursor() as cur: sql = """ INSERT IGNORE INTO comment (article_id, content, dedup_key, publish_time) VALUES (%s, %s, %s, NOW()) """ n = cur.execute(sql, (article_id, clean, key)) conn.commit() conn.close() return nlen(clean) < 2会把“好”“顶”这种单字评论过滤掉,它们对情感和热点统计都是纯噪声。INSERT IGNORE 的返回值 n 为 1 表示插入成功,0 表示 dedup_key 重复,这样查重和入库一条语句完成。调试期建议先去掉 IGNORE,让 SQL 错误直接暴露,跑通之后再加上。
3. 情感与热点分析:把原始评论变成可答辩指标的模型选型
分析部分最大的风险是“黑匣子”。如果调用第三方情感 API,老师问你“怎么 work”时很难答;硬上 BERT,训练时间长且机器起不来。我一般推荐三层方案:情感词典做基线,朴素贝叶斯做替代模型,TF-IDF 做热词,LDA 做主题扩展。每一层都能输出中间结果,答辩时能逐层讲清楚。
3.1 基于词典的情感基线:先搞清楚评论说什么
情感词典是最好解释的基线。它的逻辑很直白:一句话里正面词多给正分,负面词多给负分,否定词翻转极性。对毕设来说,这个基线能让老师看到你是“理解问题”的,而不是只会调包。
import jieba NEG_WORDS = {"不", "没", "无", "非", "别", "不是"} def dict_sentiment(text, pos_words, neg_words): words = jieba.lcut(text) score = 0 negate = False for w in words: if w in NEG_WORDS: negate = True continue if w in pos_words: score += -1 if negate else 1 elif w in neg_words: score += 1 if negate else -1 negate = False return scorepos_words 和 neg_words 是从词典文件读出来的集合,每行一个词。注意否定词处理,否则“服务不怎么样”会被“怎么样”的正向权重带偏。词典来源可以用知网情感分析用词集或 BosonNLP 情感词典,文件放项目目录,在说明文档数据定义里写清楚来源。如果想继续扩展“不太”“不够”这类双重否定,边界很难控制,毕设做到这一步就够。
3.2 TF-IDF 热点词:别把词频当热度
很多初学者直接用Counter数词频,结果“今天”“手机”“质量”全是高频词,根本看不出舆论差异。正确做法是 TF-IDF,它会给全语料里反复出现、区分度小的词降低权重。
from sklearn.feature_extraction.text import TfidfVectorizer import jieba def cut_for_tfidf(text): return " ".join(jieba.cut(text)) corpus = [cut_for_tfidf(text) for text in comment_texts] vectorizer = TfidfVectorizer( max_features=500, stop_words=["的", "了", "是", "在", "我", "你", "他", "就", "都"] ) X = vectorizer.fit_transform(corpus) keywords = vectorizer.get_feature_names_out() avg_scores = X.toarray().mean(axis=0) top10 = sorted(zip(keywords, avg_scores), key=lambda x: x[1], reverse=True)[:10]参数说明:max_features 限制特征数,毕设语料几千条时 500 足够,再大会拖慢训练;stop_words 要边跑边补,第一轮跑完看结果,把“真的”“觉得”“哈哈”这类网络噪音词加进去;分词后必须用空格拼起来再喂 TfidfVectorizer,否则它把整个句子当成一个 token,结果完全没意义。
如果只想对单批文本提取热词,直接用 jieba 自带实现更快:
import jieba.analyse top = jieba.analyse.extract_tags(text, topK=20, withWeight=True)它和 TfidfVectorizer 是同一套思想,适合进程内快速看效果;整库统计用 TfidfVectorizer 更均匀。
3.3 朴素贝叶斯:数据标注和训练代码
情感词典覆盖不了网络新词,所以还需要一个分类器兜底。毕设里朴素贝叶斯比 SVM 更稳,原因是对小样本不敏感,解释起来也简单:先算先验概率,再算每个词的条件下归属某类别的概率。
from sklearn.feature_extraction.text import CountVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline model = Pipeline([ ("vec", CountVectorizer(max_features=2000, token_pattern=r"[\w\u4e00-\u9fa5]+")), ("clf", MultinomialNB(alpha=1.0)) ]) model.fit(X_train, y_train)X_train 是分词后用空格拼接的句子,y_train 是 0/1/2 三个值,分别代表负向、中性、正向。标注工作量至少 500 条,太少会过拟合。token_pattern 这里同时匹配中文和英文,避免把标点当成词。alpha=1.0 是拉普拉斯平滑,避免某个词在训练集没出现导致概率直接变 0。
训练完不要只看准确率。这里有个所有人都会踩的坑:随机切分会让模型“作弊”。舆情数据天生带时间相关性,同一事件爆发的评论会同时出现在训练集和测试集,模型背下事件名词就能拿高分,但遇到新事件就崩。这和做量化交易策略代码时的回测逻辑一样,必须前一段训练、后一段验证:
split_time = "2024-06-01 00:00:00" train_df = comments[comments["publish_time"] < split_time] test_df = comments[comments["publish_time"] >= split_time]这样评估出来的 F1 没那么好看,但才是真实水平。答辩时主动讲出“我用时间切分而不是随机切分”,比晒一个虚高的准确率有用得多。
4. 从图表到说明文档:把分析结果包装成完整成果
很多同学代码跑通后,直接截图放论文里,丑且没说服力。舆情分析的项目展示至少要包含三类可视化:情感占比、热词排行、时间趋势。用 HTML 输出而不是静态图片,好处是交互缩放,答辩时也能让老师自己点,像个产品。
4.1 用 pyecharts 生成词云和情感分布图
pyecharts 是毕设里最稳的可视化方案,输出 HTML,不用配前端环境。词云输入一个“词-权重”二元组列表,权重可以来自上一章的 TF-IDF 均值。
from pyecharts.charts import WordCloud from pyecharts import options as opts pairs = [("故障", 38), ("价格", 29), ("服务", 24)] wc = WordCloud() wc.add("", pairs, shape="circle", word_size_range=[12, 60]) wc.set_global_opts(title_opts=opts.TitleOpts(title="热点词云")) wc.render("output/wordcloud.html")shape 用 circle 最保守,不要上来就心形。word_size_range 控制字号范围,太大会互相遮挡,太小看不出层级。注意 pyecharts 版本不同,参数名可能从word_size_range变成老版本的word_size_range,如果报错先看版本,不必硬记。
情感分布用饼图:
from pyecharts.charts import Pie pie = Pie() pie.add( "情感占比", [("正面", 321), ("中性", 438), ("负面", 126)], radius=["40%", "70%"] ) pie.set_global_opts(title_opts=opts.TitleOpts(title="评论情感分布")) pie.render("output/emotion.html")radius 设成 ["40%", "70%"] 会生成环形图,比实心饼图看着专业。数据直接从数据库统计出来,别在代码里写死。
4.2 Flask 接口:把数据库结果喂给前端
简单做法不是用 Flask 渲染大模板,而是写几个 JSON 接口,前端拉数据画图。后端代码更短,也好讲解。
from flask import Flask, jsonify from db import get_conn app = Flask(__name__) @app.route("/api/emotion") def emotion(): conn = get_conn() counter = {1: 0, 0: 0, -1: 0} with conn.cursor() as cur: cur.execute("SELECT sentiment, COUNT(*) FROM comment GROUP BY sentiment") for senti, cnt in cur.fetchall(): counter[int(senti)] = cnt conn.close() return jsonify([ {"name": "正面", "value": counter[1]}, {"name": "中性", "value": counter[0]}, {"name": "负面", "value": counter[-1]}, ]) if __name__ == "__main__": app.run(port=5000, debug=False)GROUP BY 的查询把 sentiment 从数据库拉出来,映射成标签。注意 sentiment 字段在 MySQL 里可能返回 Decimal,要 int() 转一下,否则 JSON 序列化会报错。前端用一个静态 HTML 就能消费这个接口:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> </head> <body> <div id="chart" style="width:600px;height:400px;"></div> <script> fetch('/api/emotion') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('chart')); chart.setOption({ series: [{ type: 'pie', radius: ['40%', '70%'], data: data }] }); }); </script> </body> </html>这段 HTML 放 Flask 的 templates 目录,启动后访问 http://localhost:5000 就能看到图。注意答辩环境如果没网,CDN 的 echarts.js 会加载失败,提前把文件下载到 static 目录更稳妥。
4.3 说明文档:让每个图表都有“它证明了什么”的句子
说明文档不是论文,但老师会翻。写的时候不要大段贴代码,而是讲清楚“为什么这样设计”。比如情感分析选了朴素贝叶斯而不是 BERT,可以写“受标注数据量和运行环境影响,采用可解释性强的浅层模型,后续可替换为预训练模型”——这句话能堵住一半追问。
| 文档章节 | 核心内容 | 与代码的对应关系 |
|---|---|---|
| 需求分析 | 用户场景、功能列表 | 不要贴代码,用用例描述 |
| 系统设计 | 架构图、模块划分 | 采集/清洗/分析/可视化四模块 |
| 数据库设计 | ER 图、表结构说明 | 对应第 2 章建表 SQL |
| 核心算法 | 情感词典、朴素贝叶斯、TF-IDF | 对应第 3 章训练代码 |
| 系统测试 | F1、爬取成功率、入库率 | 用时间切分实验数据 |
| 总结与展望 | 局限和改进方向 | 可写实时流、预训练模型微调 |
每个图表下面至少写一句“它说明了什么”。情感饼图旁写“负面占比 12%,主要集中在物流投诉”,比光放图有效得多。数据字典也要给:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
| sentiment | TINYINT | -1 负面,0 中性,1 正面 | 1 |
| dedup_key | VARCHAR(32) | MD5 去重键 | 9f86d081884c7d65... |
这些内容凑起来,说明文档自然就有厚度,也不会被“源码缺文档”扣分。
5. 毕设实战避坑清单:5 个我见过最多的翻车现场
这部分记录的是我从数据采集到答辩演示遇到或见过最多的坑。每条按现象、原因、解决展开,动手前先看一遍。
5.1 爬虫一跑就被拦截,浏览器打开却正常
现象:同一个 URL 用浏览器能看,代码一跑返回 302 跳转或验证码页。
原因:服务器检测到“非浏览器请求”。最常见是请求头缺 Referer 或 Cookie,User-Agent 又是老旧的默认值;也可能因为两次请求间隔太短。
解决:从浏览器开发者工具复制当前页面的完整请求头,至少包含 User-Agent、Referer、Cookie。再加随机延时:
import time from random import uniform for page in pages: fetch_page(page) time.sleep(uniform(0.5, 1.2))延时设 0.5 到 1.2 秒之间比较稳,太短被封,太长采集太慢。如果目标站点必须登录,手动登录后复制 Cookie 放 headers 里,不要在毕设里写模拟登录逻辑,那个坑更深。
5.2 中文入库变问号,带 emoji 直接报错
现象:控制台 print 中文正常,插入 MySQL 后全变“???”,插入 emoji 时报Incorrect string value: '\xF0...'。
原因:MySQL 连接或表结构用了 utf8。utf8 只支持 3 字节,emoji 是 4 字节,数据库只能报错。除此之外,页面编码和连接参数不一致也会出乱码。
解决:统一用 utf8mb4。建库时执行:
CREATE DATABASE opinion CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接池参数里写charset="utf8mb4",requests 拿到响应后先指定resp.encoding = "utf-8"。这三处保持一致,乱码问题基本绝迹。这个问题修起来快,但最好在设计阶段就写对,否则清洗后的语料全要重跑。
5.3 jieba 把品牌名切成单字,热点词没法看
现象:热词榜出现“华”“为”“苹”“果”,而不是“华为”“苹果”。
原因:jieba 默认词典没有收录这些专有名词,分词器会按单字切分。舆情文本里品牌名和地名特别多,这个坑很普遍。
解决:准备一个用户词典,每行一个词,可选带词频和词性:
import jieba jieba.load_userdict("userdict.txt")userdict.txt 内容示例:
华为 100000 nz 鸿蒙 100000 nz 青白江 100000 ns词频给高一点,避免被词典里其它词拉低;词性标注 nz(其他专名)或 ns(地名)按情况填。自定义词典直接影响情感和热词结果,尤其是词典匹配类的情感基线,分词错了,后面全错。
5.4 训练准确率 95%,新数据一到手就崩
现象:分类器在测试集上 F1 很高,把新爬的评论丢进去,情感结果明显不对。
原因:随机切分造成“信息泄露”。舆情评论高度聚集在具体事件里,随机切分会把同一事件的重复讨论同时放进训练和测试,模型记住事件词汇,没学会情感规律。
解决:按时间切分,前一段训练,后一段验证。代码上只是一行条件:
train_df = comments[comments["publish_time"] < split_time] test_df = comments[comments["publish_time"] >= split_time]改成时间切分后,训练集上的覆盖率会下降,但泛化能力才是答辩真正要展示的。老师看到 95% 准确率,第一反应大概率是问“你的测试集怎么切的”,到时候圆不回来。
5.5 词云被“今天”“真的”“一个”淹没
现象:词云里全是虚词和口语词,看不出舆论主题。
原因:jieba 自带停用词表只有几十个,网络文本里的高频噪音词“真的”“哈哈”“感觉”都没过滤。
解决:建一个项目专用 stopwords.txt,每行一个词,统计前过滤:
from collections import Counter import jieba stopwords = set(open("stopwords.txt", encoding="utf-8").read().split()) def top_words(texts, topk=30): counter = Counter() for text in texts: words = [w for w in jieba.cut(text) if w.strip() and w not in stopwords and len(w) > 1] counter.update(words) return counter.most_common(topk)len(w) > 1 会过滤大部分虚词,但也要小心“沪”“京”这种单字地名简称,如果研究对象正好是地域讨论,就得把这条件拆开。每次跑完看一眼结果,把新出现的噪音词补进 stopwords.txt,迭代三轮词云基本就干净了。
6. 从能跑到能答辩:用可复现验证收尾
临近答辩,我通常不再改模型,而是固定三个数字:爬取条数、入库条数、有效语料条数。写一个独立验证脚本,跑完整链路后输出这几个数,再和真实页面数量对比。如果爬了 300 条但入库只有 150 条,去清洗逻辑里找问题;如果入库 300 条但有效语料只有 100 条,多半是停用词或长度过滤太狠。这三个数字比任何算法的输出都更能说明系统可靠性。
下面这张表是我常用的一种成绩记录方式,也建议写进说明文档的测试章节:
| 指标 | 数值 | 检查点 |
|---|---|---|
| 爬取条数 | 867 | 与页面记录核对 |
| 入库条数 | 804 | 去重与字符集 |
| 有效语料 | 731 | 清洗过滤阈值 |
| 情感分类 F1 | 0.83 | 按时间切分 |
给老师展示时,不要只放一张饼图,要把模型评估表贴进说明文档,注明训练集数量、切分方式、F1 和召回率。很多同学只写“准确率 95%”,老师经验丰富,一眼就知道那个数字是怎么来的。我当时吃过这个亏,后来养成一个习惯:任何指标都要写清“在什么数据上、用什么切分”算出来的。这也让调试不那么像玄学——从随机切分改成时间切分后,模型 F1 从 0.9 降到 0.82,但新数据上的表现反而稳了。这套流程我重做过三次,每次踩坑位置都差不多。希望这份清单能在答辩前帮你少熬几天夜,也在老师追问“为什么这样设计”时,让你答得比我当时更顺。
本文还有配套的精品资源,点击获取