ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python网络舆情分析系统:从数据采集到情感识别的完整实践

Python网络舆情分析系统:从数据采集到情感识别的完整实践 简介这是一份面向毕业设计的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, paramsNone, timeout10): resp requests.get(url, headersHEADERS, paramsparams, timeouttimeout) 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, paramsNone, retries3): 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个设计点charset 用 utf8mb4utf8 存不了 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( creatorpymysql, maxconnections10, mincached2, maxcached5, blockingTrue, hostlocalhost, userroot, passwordyour_password, databaseopinion, charsetutf8mb4 ) def get_conn(): return pool.connection()maxconnections 控制连接上限mincached 预热两个连接blockingTrue 表示连接不够时请求排队而不是直接报错。爬虫批量插入时省去了每次握手建连接的额外开销。依赖装两个包pip install DBUtils PyMySQL。2.3 清洗和入库把“脏评论”变成可分析的语料网页文本里通常混着 HTML 标签、URL、用户还有“ ”之类的实体。清洗的目标是把这些噪音去掉同时保留中文和表情内容。import re import hashlib def clean_comment(raw): raw re.sub(r[^], , raw) raw re.sub(rhttps?://\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 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_features500, stop_words[的, 了, 是, 在, 我, 你, 他, 就, 都] ) X vectorizer.fit_transform(corpus) keywords vectorizer.get_feature_names_out() avg_scores X.toarray().mean(axis0) top10 sorted(zip(keywords, avg_scores), keylambda x: x[1], reverseTrue)[:10]参数说明max_features 限制特征数毕设语料几千条时 500 足够再大会拖慢训练stop_words 要边跑边补第一轮跑完看结果把“真的”“觉得”“哈哈”这类网络噪音词加进去分词后必须用空格拼起来再喂 TfidfVectorizer否则它把整个句子当成一个 token结果完全没意义。如果只想对单批文本提取热词直接用 jieba 自带实现更快import jieba.analyse top jieba.analyse.extract_tags(text, topK20, withWeightTrue)它和 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_features2000, token_patternr[\w\u4e00-\u9fa5])), (clf, MultinomialNB(alpha1.0)) ]) model.fit(X_train, y_train)X_train 是分词后用空格拼接的句子y_train 是 0/1/2 三个值分别代表负向、中性、正向。标注工作量至少 500 条太少会过拟合。token_pattern 这里同时匹配中文和英文避免把标点当成词。alpha1.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, shapecircle, word_size_range[12, 60]) wc.set_global_opts(title_optsopts.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_optsopts.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(port5000, debugFalse)GROUP BY 的查询把 sentiment 从数据库拉出来映射成标签。注意 sentiment 字段在 MySQL 里可能返回 Decimal要 int() 转一下否则 JSON 序列化会报错。前端用一个静态 HTML 就能消费这个接口!DOCTYPE html html head meta charsetutf-8 script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script /head body div idchart stylewidth: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 正面1dedup_keyVARCHAR(32)MD5 去重键9f86d081884c7d65...这些内容凑起来说明文档自然就有厚度也不会被“源码缺文档”扣分。5. 毕设实战避坑清单5 个我见过最多的翻车现场这部分记录的是我从数据采集到答辩演示遇到或见过最多的坑。每条按现象、原因、解决展开动手前先看一遍。5.1 爬虫一跑就被拦截浏览器打开却正常现象同一个 URL 用浏览器能看代码一跑返回 302 跳转或验证码页。原因服务器检测到“非浏览器请求”。最常见是请求头缺 Referer 或 CookieUser-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;连接池参数里写charsetutf8mb4requests 拿到响应后先指定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, encodingutf-8).read().split()) def top_words(texts, topk30): 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但新数据上的表现反而稳了。这套流程我重做过三次每次踩坑位置都差不多。希望这份清单能在答辩前帮你少熬几天夜也在老师追问“为什么这样设计”时让你答得比我当时更顺。本文还有配套的精品资源点击获取
返回列表