☰
Python网络舆情分析系统:从数据采集到情感识别的完整实践
2026/10/1 3:31:15 网站建设 项目流程

简介:这是一份面向毕业设计的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.text

HEADERS 是给服务器看的“浏览器身份”,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("&nbsp;", " ").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 n

len(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 score

pos_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%,主要集中在物流投诉”,比光放图有效得多。数据字典也要给:

字段类型说明示例
sentimentTINYINT-1 负面,0 中性,1 正面1
dedup_keyVARCHAR(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清洗过滤阈值
情感分类 F10.83按时间切分

给老师展示时,不要只放一张饼图,要把模型评估表贴进说明文档,注明训练集数量、切分方式、F1 和召回率。很多同学只写“准确率 95%”,老师经验丰富,一眼就知道那个数字是怎么来的。我当时吃过这个亏,后来养成一个习惯:任何指标都要写清“在什么数据上、用什么切分”算出来的。这也让调试不那么像玄学——从随机切分改成时间切分后,模型 F1 从 0.9 降到 0.82,但新数据上的表现反而稳了。这套流程我重做过三次,每次踩坑位置都差不多。希望这份清单能在答辩前帮你少熬几天夜,也在老师追问“为什么这样设计”时,让你答得比我当时更顺。

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

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

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

立即咨询