
简介这是一份基于Python与MySQL的网络舆情分析系统毕业论文资料包主要面向计算机专业学生、毕业设计开发者以及需要开展网络言论管理的相关技术人员。论文从计算机技术对信息传播与言论发布的影响切入阐明了舆情分析工作的现实需求并完成了整个系统的设计与实现系统采用Python语言开发以MySQL作为数据存储介质功能覆盖言论分析、言论管理、用户管理等模块同时支持以城市或地区为关键词进行本地相关负面评论的快速检索与查看为网络管理部门提供了高效、可落地的监测思路。资源包共1个文件为docx格式文档整体大小约1.73MB文档中除论文正文外还包含封面、独创性声明、中英文摘要等标准写作模板便于使用者对照格式要求进行修改和提交。目前已有1262人学习下载适合正在撰写舆情分析类毕业设计或希望系统了解PythonMySQL开发流程的读者能够帮助快速搭建论文框架并理解从数据库设计到业务功能实现的关键环节。1. 基于python的网络舆情分析系统一条从采集到预警的数据链路基于python的网络舆情分析系统乍看是课程设计或毕设题目实际是一条从数据采集、清洗入库、文本分析到可视化预警的完整数据链路。它解决的并不是“爬虫能爬到多少数据”而是“拿到数据之后怎么变成可读的舆情结论”某事件在哪个平台发酵、负面占比多少、热度峰值出现在几点、源头是哪篇文章。适合三类人准备做毕设但不想只交 demo 的学生、要给部门搭轻量舆情监控的运维或后端工程师、以及需要给论文补“数据库算法”完整闭环的研究者。我给你的一个反直觉结论是这个系统最耗时间的不是算法调优而是数据采集稳定性和文本清洗情感模型反而能用词典法先跑通。2. 数据层设计与采集实现先把舆情数据存进 MySQL2.1 数据源与采集方案先划定边界再写爬虫做舆情系统前先想清楚数据源边界。常见做法是优先选三种新闻门户的滚动列表、公开评论区和搜索引擎的新闻聚合结果这三类页面结构相对稳定、不需要登录、字段也够用。不要把目标一开始就定成“监控全网”数据源越泛清洗规则越难写后期论文里也没法交代“数据从哪来、覆盖范围是什么”。采集层我首选 requests BeautifulSoup不用一上来就上 Scrapy。原因很简单小规模舆情系统的采集目标是“每天几千到几万条”单机多线程请求足够Scrapy 的中间件、Pipeline 在数据量上去之后才是收益前期只会增加调试成本。下面这个脚本是一个新闻列表页的最小采集实现import requests from bs4 import BeautifulSoup import hashlib, json, time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://news.sina.com.cn/ } def fetch_news_list(url, timeout10): resp requests.get(url, headersHEADERS, timeouttimeout) # 不要把编码写死成 utf-8优先用 apparent_encoding 兜底 resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) items [] for a in soup.select(a): href a.get(href, ).strip() title a.get_text(stripTrue) if not title or len(title) 8: continue if not href.startswith(http): continue # 用 md5(url) 作为去重指纹比直接存 url 做索引快得多 uid hashlib.md5(href.encode(utf-8)).hexdigest() items.append({id: uid, title: title, url: href}) return items if __name__ __main__: base_url https://news.sina.com.cn/roll/#page_{} for page in range(1, 3): data fetch_news_list(base_url.format(page)) with open(fnews_{page}.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) time.sleep(1) # 限速是礼貌也是反封的第一步逻辑说明resp.encoding 用 apparent_encoding 是为了避免某些页面 gbk 编码导致标题乱码md5 指纹是为后面 MySQL 去重做准备url 动辄几百字符直接建索引会浪费空间。time.sleep(1) 是控制请求频率的最低成本手段舆情采集不是抢购不需要高并发。这个脚本只输出 json还没入库先看数据长什么样再决定表结构。2.2 MySQL 表设计舆情主表、热度表、词典表数据库选型上MySQL 是舆情系统最稳的起步选择事务保证写入不丢、SQL 查询方便、千万级数据加索引仍然可查。PostgreSQL 也很好但如果你要给别人复现MySQL 的普及度更高。不要在这个阶段引入 MongoDB舆情数据的核心操作是按时间和来源做聚合关系型数据库的 GROUP BY 比文档数据库更顺手。建表时我推荐拆三张表新闻文章表存原始内容热度统计表存按小时聚合后的数据词典表存自定义分词和情感词。拆表的原因是统计任务不应该反复扫全表聚合结果单独存报表直接查。下面是第一张表的建表语句CREATE DATABASE IF NOT EXISTS yq DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; USE yq; CREATE TABLE news_article ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, uid CHAR(32) NOT NULL COMMENT md5(url) 去重指纹, title VARCHAR(255) NOT NULL, content MEDIUMTEXT, source VARCHAR(64) DEFAULT , publish_time DATETIME NOT NULL, fetch_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, url VARCHAR(512) DEFAULT , comment_cnt INT DEFAULT 0, like_cnt INT DEFAULT 0, UNIQUE KEY uk_uid (uid), KEY idx_publish_time (publish_time), KEY idx_source (source) ) ENGINEInnoDB;参数说明uid 用 CHAR(32) 而不是 VARCHAR(512) 存 URL是索引性能和存储空间的双重考虑publish_time 上建普通索引不建联合索引因为后续查询基本是“某个时间范围 某个来源”两个单索引足够。content 用 MEDIUMTEXT 而不是 TEXT是给长文留余地。这里最容易踩的坑是直接在 url 字段上建 UNIQUE KEYutf8mb4 下 512 字符会超出索引长度限制所以指纹字段必须单独建。2.3 数据入库与幂等去重同一篇新闻重复入库的问题采集脚本跑起来之后会遇到第一个实际麻烦页面刷新后同一篇新闻反复抓到。如果不做去重news_article 表会膨胀得很快后面统计出的“舆情数量”全是虚的。解决思路不是查一遍再插而是用数据库的唯一索引保证幂等。import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databaseyq, charsetutf8mb4, autocommitFalse ) def save_batch(rows): sql INSERT IGNORE INTO news_article (uid, title, content, source, publish_time, url) VALUES (%s, %s, %s, %s, %s, %s) with conn.cursor() as cur: # executemany 批量写入比逐条 insert 快一个量级 cur.executemany(sql, rows) conn.commit()逻辑说明INSERT IGNORE 配合 uk_uid 唯一索引重复的 uid 会被数据库直接丢弃不报错、不影响批量导入。这样遇上去重逻辑时不需要先 SELECT 再 INSERT减少一次网络往返。常见误区是“我代码里先查一遍再插不是更稳吗”但实际上高并发下这种 check-then-insert 存在竞态两条线程同时查到“不存在”然后同时插入还是会重复。把去重交给数据库唯一索引是舆情数据入库最省心的做法。3. 舆情分析核心实现分词、情感判定、热点聚类一条链路3.1 中文分词与关键词提取为什么 jieba 是默认起点舆情分析的第一步是分词。中文分词的难点在于歧义和新词比如“西安交大”可能被切成“西安”和“交大”“yyds”这种网络热词在默认词典里根本不存在。jieba 虽然是老牌库但胜在可控支持自定义词典、支持词性标注、部署成本极低。不要一上来就上 HanLP 或 LTP它们的模型更大、精度更高但舆情分析的前期任务是快速建立基线jieba 足够。import jieba import jieba.analyse # 自定义词必须优先加载否则品牌词会被切开 jieba.load_userdict(custom_dict.txt) text 某手机品牌发布了新款旗舰机但网友对该机型的续航表现吐槽较多 # 精确模式分词适合做关键词提取和情感判定 words jieba.lcut(text) print(words) # 基于 TF-IDF 提取 Top5 关键词用于后续热点聚类 keywords jieba.analyse.extract_tags(text, topK5, withWeightTrue) print(keywords)参数说明lcut 走的是精确模式不会把“手机品牌”继续切成“手机”和“品牌”对情感分析更友好。extract_tags 的 topK 控制每个文本保留几个关键词我一般设 5设太多会把“的”“了”这类停用词带回结果。custom_dict.txt 里每行格式是“词 词频 词性”比如“某手机品牌 100 n”这个词频不是必须准确但给一个较大的数能提高词被合并的概率。如果发现舆情文案里品牌词总被切开优先检查自定义词典而不是换分词库。3.2 情感分析基于情感词典的正面、负面、中性判定情感分析在舆情系统里是核心模块。可以用 Snownlp 直接跑但我更推荐自写一个词典打分器原因是词典法可解释性强论文里能画清楚流程图调阈值、加否定词都直观后期数据量够了想换成深度学习词典法还能作为标注基线。Snownlp 的问题是模型是通用的对“某品牌降价”这种语境会误判为纯负面而实际上消费者可能是在夸性价比。import jieba POS_DICT {好评: 2, 稳定: 1, 流畅: 1, 性价比: 1} NEG_DICT {翻车: -2, 卡顿: -1, 续航崩: -2, 吐槽: -1} DEGREE_DICT {非常: 1.5, 很: 1.2, 有点: 0.8} NEG_WORDS {不, 没, 无} def sentiment_score(text): words jieba.lcut(text) score 0.0 degree 1.0 # 程度副词权重 neg 1.0 # 否定词权重 for w in words: if w in NEG_WORDS: neg -1.0 elif w in DEGREE_DICT: degree DEGREE_DICT[w] elif w in POS_DICT: score degree * neg * POS_DICT[w] degree, neg 1.0, 1.0 elif w in NEG_DICT: score degree * neg * NEG_DICT[w] degree, neg 1.0, 1.0 return score print(sentiment_score(这款手机续航非常稳定)) # 正分 print(sentiment_score(续航一点都不稳定)) # 负分逻辑说明这个打分器的核心是“程度副词 否定词”的窗口机制。读到“非常”把 degree 置为 1.5读到“不”把 neg 置为 -1当下一个情感词出现时把两者乘进去。词打分乘以负权重后整体方向反转这就是为什么“一点都不稳定”能被打成负分。参数说明POS_DICT 和 NEG_DICT 的权重绝对值代表情感强度建议范围 1~3不要给太大否则几条极端词就能淹没整篇文章的判断。这个简易版的问题在于多个否定词叠加如“不是不好”会被误判实际项目中我加了连续否定词窗口读到连续两个否定词就重置为正向。3.3 热点聚类与话题追踪用 TF-IDF 向量化再做聚类舆情系统除了看单篇文章情感还要回答“当前大家在讨论什么”。常见做法是把一段时间内的标题向量化再做聚类。TF-IDF 向量化是为了把文本转成数字KMeans 聚类是为了把相似话题归到同一组。为什么不直接用关键词匹配因为同一个话题的表达方式太多“某品牌手机发布会”和“某某旗舰机发布”在字面上不重叠但向量空间里距离很近。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans import numpy as np titles [ 某品牌发布新款旗舰机售价大幅下调, 旗舰机价格战开打某厂商跟进, 某品牌手机续航测试结果出炉, 发布会现场体验新机手感轻薄, ] # ngram_range(1,2) 保留词组min_df2 过滤只出现一次的词 vectorizer TfidfVectorizer(max_features10000, ngram_range(1, 2), min_df2) X vectorizer.fit_transform(titles) km KMeans(n_clusters2, random_state42, n_init10) labels km.fit_predict(X) # 每个聚类取 TF-IDF 权重最高的词作为话题标签 feature_names vectorizer.get_feature_names_out() for i in range(km.n_clusters): centroid_idx km.cluster_centers_[i].argsort()[::-1][:5] words [feature_names[j] for j in centroid_idx] print(fCluster {i}: {, .join(words)})参数说明n_clusters2 是基于“4 条标题最多两个话题”的人为假设真实场景里需要用轮廓系数或手肘法确定min_df2 是把只出现一次的词过滤掉避免单个文本的独特词影响聚类n_init10 是让 KMeans 多跑几次取最优避免随机初始化带来的结果抖动。热点聚类的时效性要注意舆情系统的聚类窗口我一般设 4 小时或 24 小时窗口太短样本不够太长话题已经变了好几轮。这个方案适合论文里的定性分析如果要实时追踪热点演变就得换增量聚类算法但那是后话。4. 可视化看板与预警规则让舆情结果能看懂、能报警4.1 用 Flask ECharts 搭建最小舆情看板舆情分析的结果最终要给两类人看一类是技术负责人关心数据量和模型准确率另一类是业务或领导只看趋势图和预警消息。给业务看的东西必须可读我的做法是 Flask 提供 JSON 接口前端用 ECharts 渲染前后端分离但放在同一个项目里省去跨域配置的麻烦。from flask import Flask, jsonify import pymysql, json app Flask(__name__) app.route(/api/trend) def trend(): 返回最近 24 小时舆情数量趋势 conn pymysql.connect(host127.0.0.1, userroot, password123456, databaseyq, charsetutf8mb4) with conn.cursor() as cur: cur.execute( SELECT DATE_FORMAT(publish_time, %Y-%m-%d %H:00) AS hour, COUNT(*) AS cnt FROM news_article WHERE publish_time NOW() - INTERVAL 24 HOUR GROUP BY DATE_FORMAT(publish_time, %Y-%m-%d %H:00) ORDER BY hour ) rows cur.fetchall() conn.close() return jsonify([{hour: r[0], cnt: r[1]} for r in rows]) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)逻辑说明这个接口的参数设计值得讲一下——DATE_FORMAT 按小时粒度聚合把时间戳变成整点字符串前端 ECharts 的 x 轴可以直接用用 NOW() - INTERVAL 24 HOUR 而不是在 Python 里算时间是为了让时间判断完全交给数据库避免应用服务器与数据库服务器时区不一致导致“少一小时数据”的诡异问题。这也是舆情系统里很常见的一种“玄学 bug”最后查出来是时区没对齐。接口只返回 JSON图表渲染交给前端ECharts 的折线图配置网上有很多模板重点是数据接口稳定。4.2 舆情预警规则阈值、速率、突发敏感词三种触发有了趋势数据下一步是预警。舆情预警不能只做一个“超过 100 条就报警”的静态阈值那样不是预警是噪音。我一般设计三层规则第一层是数量阈值比如单个时间窗舆情总数超过历史均值 1.5 倍第二层是负面占比比如一小时内负面情感占比超过 30%第三层是敏感词突发比如自定义敏感词在十分钟内出现次数超过日常均值 5 倍。这三层规则从“量变”到“质变”到“具体事件”覆盖了大部分舆情场景。def check_alert(total_now, total_hist, neg_rate_now, neg_th0.3, rate_th1.5): alerts [] # 规则1总量突增 if total_hist 0 and total_now / total_hist rate_th: alerts.append(总量突增) # 规则2负面占比超阈值 if neg_rate_now neg_th: alerts.append(负面占比过高) return alerts参数说明rate_th1.5 意味着当前时间窗舆情数比历史同期多 50% 才报警。这个值不能拍脑袋定死需要观察一周数据再调如果每天都有几个时段误报就调高到 2.0如果该报警没报漏报就调低到 1.3。neg_th0.3 是负面占比阈值这个值受数据源影响很大如果是采集新闻源负面占比天然低0.3 就偏高如果是采集社交公开数据负面情绪本就多0.5 都不一定报警。所以预警规则一定要做成配置项放数据库或配置文件里不要硬编码在代码里。4.3 日报导出把数据库查询结果落成 Excel 文件舆情系统的最终交付物往往是一份日报或周报今天总舆情量、负面量、Top 热点话题、预警事件列表。用 pandas 加 openpyxl 直接导出 Excel 是最稳妥的交付方式业务方可以在 Excel 里二次筛选领导也能直接看。除了 pandas还要装 openpyxl光 pandas 只能写 csvExcel 格式的导出需要这个库。import pandas as pd from sqlalchemy import create_engine engine create_engine(mysqlpymysql://root:123456127.0.0.1:3306/yq?charsetutf8mb4) df pd.read_sql( SELECT DATE(publish_time) AS day, COUNT(*) AS total_cnt, SUM(CASE WHEN sentiment -1 THEN 1 ELSE 0 END) AS neg_cnt FROM news_article WHERE publish_time CURDATE() - INTERVAL 7 DAY GROUP BY DATE(publish_time) , engine) df[neg_ratio] (df[neg_cnt] / df[total_cnt]).round(4) df.to_excel(舆情日报.xlsx, indexFalse, sheet_name7日舆情趋势)逻辑说明这里用的 SQLAlchemy 连接串代替了之前的 pymysql 直连主要是让 pandas 的 read_sql 能直接拿 DataFrame省去逐行转换。CASE WHEN 在 SQL 里做条件计数比先把全表查回来再在 Python 里 groupby 更省内存。注意 to_excel 的 sheet_name 参数如果你不指定默认叫 Sheet1日报交付时最好把 sheet 名字改成业务能看懂的内容。Excel 导出是舆情系统里“做了没人夸、漏了被人骂”的功能但它恰恰是论文里“系统应用”章节最好写的部分。5. 舆情系统落地避坑记录5 条压测才暴露的数据库与调度问题5.1 采集端被限制或返回乱码现象、成因与处理现象采集脚本跑了十几分钟后返回的页面从正常列表变成验证页或者标题全是乱码。第一次遇到时我以为是反爬查了半天才发现是编码问题。成因有两类一是没有带 Referer 或 User-Agent被服务器识别为脚本二是页面本身是 gbk 编码代码里 resp.encoding 固定写成 utf-8导致标题乱码。解决头部信息至少带真实浏览器的 UAReferer 填首页地址编码不要写死用 resp.apparent_encoding。采集频率用限速控制不做并发轰炸大多数新闻站的容忍度比想象中高问题出在频率不稳定时快时慢才容易被限制。5.2 数据库连接池被打满与慢查询另一个隐蔽坑现象多线程采集开起来后程序报 “Too many connections”第二天看数据库统计接口响应时间从 50ms 涨到 3 秒。原因代码里每个线程每次写入都新建 pymysql 连接用完后只 close 了游标没有关连接聚合查询 group by publish_time 时没有索引。解决全局维护一个连接池用 pymysql 的 connections 模块或直接改用 SQLAlchemy 的连接池给 publish_time 和 source 建普通索引。这个问题的隐蔽性在于单线程时一切正常一旦并发上去连接数翻倍MySQL 默认 max_connections 是 151很快就会被耗尽。5.3 情感词典翻车语境迁移时准确率骤降现象情感分析模型在测试集上准确率 78%上线后发现“某品牌续航翻车”被判成中性。原因是词典里没收录“翻车”这个词或者“翻车”被 jieba 切成了“翻”和“车”。解决把业务场景里出现的高频词人工加入词典和情感词表比如数码领域要加“续航、快充、发热、掉电”食品领域要加“卫生、口感、配料”。我后来养成的习惯是每周从数据库里随机抽 50 条新舆情人工标注后对比模型结果把误判的词补进词典。这一步是纯人工活但却是词典法准确率的直接上限。5.4 预警规则太灵敏导致的告警疲劳现象预警功能上线第一天钉钉群被刷了 40 条消息第二天没人看真出事时反而被忽略。原因是阈值设得太低加上没有做同源合并——同一个事件被多个新闻站转载每个站点都触发一次预警。解决预警规则里加静默期同一事件 30 分钟内只推送一次阈值要从历史数据的分位数算而不是拍脑袋定。比如统计过去 30 天每天同时段的舆情量取 95 分位数作为阈值基线远比你拍一个“100 条”靠谱。告警疲劳是舆情系统最常见的“看起来做了很多、实际没人用”的原因。5.5 文本清洗的疏漏HTML 标签和表情符号污染情感分数现象有几天负面率异常偏高抽了几条数据发现内容是“续航真不错”这明显是矛盾表达但情感打分器把“不错”算成正向表情符号又被清洗逻辑删掉。原因表情符号其实携带很强的情感信息不能简单只按文本词典打分需要单独提取表情符号做二次修正。解决清洗时把 HTML 实体和标签去掉但把表情符号单独存一列后续作为情感分析的一个附加特征对“文字正向 表情负向”的组合给一个负向偏移。这类问题不靠算法解决靠的是你在清洗阶段多留一个心眼。6. 评估舆情系统到底准不准从留存语料到冷启动调参6.1 用 200 条标注语料跑一套半小时的留存评估舆情系统上线前我建议先留出 200 条已经人工标注的舆情文本永远不要用这些数据去调情感词典——它们只用来评估。评估指标看三个准确率、召回率、F1尤其要看负向样本的召回率。舆情场景里负面事件漏报的代价远大于误报所以我不只看总体准确率而是单独算“负向 F1”。def evaluate(y_true, y_pred): # y_true/y_pred 都是 list元素为 1 或 01 表示负面 tp sum(1 for t, p in zip(y_true, y_pred) if t 1 and p 1) fp sum(1 for t, p in zip(y_true, y_pred) if t 0 and p 1) fn sum(1 for t, p in zip(y_true, y_pred) if t 1 and p 0) precision tp / (tp fp 1e-9) recall tp / (tp fn 1e-9) f1 2 * precision * recall / (precision recall 1e-9) return precision, recall, f1逻辑说明我加 1e-9 只是防除零实际数据里这个分母几乎不会为零。加这一行的真实原因是之前有一次评估线上舆情数据某个时段全是正向样本tpfp 等于 0直接 ZeroDivisionError日志里留下一堆报错。负向样本在真实舆情里占比通常只有 10% 左右所以只看总体准确率没有意义——模型全预测正向也能有 90% 准确率但舆情系统等于废了。6.2 两小时冷启动调参从词典到阈值的一轮循环冷启动阶段没有标注数据怎么调我的习惯是先用词典法跑通全流程把系统输出的每条结果人工抽检 50 条记录错误类型如果是词典缺词补词典如果是阈值不合理调整阈值如果是“不”“没”这类否定词处理不对检查否定词窗口。这一轮循环下来情感分析的负向 F1 能从 0.3 涨到 0.7 左右已经达到舆情系统可用基线。还有一个重要习惯任何模型改动都必须先跑一遍 6.1 的留存语料对比新旧结果再上生产。这个习惯帮我挡过不少“改了个词典全站预警刷屏”的翻车事故。舆情系统的效果不是靠一个模型撑起来的而是靠数据链路每一环的细节堆出来的一步步把每个环节做扎实结果自然会说服人。希望帮到你。本文还有配套的精品资源点击获取