ARTICLE DETAIL

资讯详情

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

基于Python的新冠疫情舆情分析系统:从数据采集到情感趋势可视化

基于Python的新冠疫情舆情分析系统:从数据采集到情感趋势可视化 简介这是一份基于Python与Django框架构建的新冠疫情舆情信息分析系统的完整论文文档共31页7166字适合计算机专业学生用于课程设计、毕业设计或相关课题参考。内容涵盖系统需求分析、总体设计、模块功能设计并具体实现了用户注册登录、后台管理、国内及世界疫情数据可视化、新闻舆情正负面评估与数据更新等核心功能开发环境涉及Windows10、PyCharm、Python3.8.5、Django与pyecharts等文档对前端展示、数据处理流程和测试分析均有详细阐述。压缩包内仅含1个docx文件体积约1.17MB轻量便于下载与翻阅。该资源已有299人学习对于需要快速搭建疫情可视化系统或撰写相关Python Web论文的读者可从中获取系统框架设计思路、关键代码组织方式及文档写作范本。1. 为什么需要一个基于Python的新冠疫情舆情信息分析系统假设你负责一个地区的物资调配需要提前判断“居民对防控措施的情绪是配合还是抵触”靠人工翻评论区根本来不及假设你要做毕业设计只打包了一个爬虫脚本加几张饼图答辩时大概率会被追问“情感分析模型在哪、评价指标是什么”而卡住。基于Python的新冠疫情舆情信息分析系统本质是一条“公开文本采集 → 文本清洗 → 情感判定 → 趋势统计 → 报表输出”的流水线它回答的不是“疫情怎么变化”而是“大家怎么看待这件事、情绪在往哪个方向走”。这套方案适合三类人做课程设计或毕业论文的学生想转数据分析岗但缺一个完整项目经验的从业者以及需要给业务侧提供舆情周报的产品和运营人员。2. 用Python搭舆情系统的技术选型生态、库和最短依赖清单2.1 为什么偏偏是Python四库定乾坤做舆情分析可选的语言不少Java、Go、R都能写但Python在这条链路里几乎每个环节都有现成轮子这是它成为默认选项的根本原因。采集层有requests和Scrapy不必自己封装Socket连接文本处理层有jieba分词和Snownlp情感判定中文语境下的词典和预训练模型直接能跑分析层有pandas和scikit-learn从数据透视到分类模型都在同一套API体系里展示层有pyecharts生成HTML报表不需要前端介入。反向看Java的Spring生态固然适合大型系统但做一个日增量几千条的舆情分析任务启动成本和部署成本都偏高R语言在统计建模上很强但爬虫和工程化部署偏弱。用Python搭这套系统我一般遵循一个原则每个层次选一个主力库不追求大而全依赖控制在十四个以内这样换机器重装环境时不容易翻车。2.2 环境搭建从零把依赖装干净这里直接给一套我验证过多次的最小环境安装流程。假设你已经装好了Anaconda或Miniconda打开终端执行conda create -n covid_env python3.9 -y conda activate covid_env pip install requests pandas jieba snownlp scikit-learn pyecharts flask openpyxl逻辑说明第一步创建独立虚拟环境把Python版本锁在3.9这是为了避免系统自带Python被乱装包污染第二步激活环境后续所有操作都在这套隔离环境里跑。第三行一次性安装的八个库分别对应采集requests、表格处理pandas、中文分词jieba、情感基线snownlp、机器学习scikit-learn、可视化pyecharts、报表服务flask和Excel导出openpyxl。参数说明python3.9不是随便选的当前大多数NLP库对3.10以上的兼容性已经没问题但Snownlp这类维护不频繁的库在3.9下表现最省心少踩很多环境坑。如果你只想快速验证不装flask也没关系后面我会说明什么时候才需要它。装完后第一件事是自查环境不要急着写代码pip show snownlp pandas scikit-learn python -c import jieba, snownlp; print(core libs ok)这里pip show会输出每个库的版本和Site路径作用是确认库装进了covid_env而不是系统Python目录。很多初学者的血泪经验是明明pip list里能看到库运行脚本却报ModuleNotFoundError十有八九是IDE解释器没切换到这个虚拟环境。这个问题在VSCode里可以通过CtrlShiftP调出“Python: Select Interpreter”来修正。2.3 数据存储选型先CSV后MongoDB的思路舆情系统的存储方案要跟数据量匹配而不是一步到位上分布式。我的经验是分三个阶段走日增量低于两千条时直接用CSV或JSON Lines文件存储pandas的read_csv一行就能载入分析省去数据库运维负担数据量到几万条且有并发写入需求时换SQLite单文件、零配置支持SQL查询只有当你需要多台机器同时采集并做增量合并时才考虑MongoDB的文档模型因为一条舆情记录本身字段数量不固定有的带地理标签有的带转发数文档型数据库写起来更自然。对应到本项目爬到的原始数据推荐以“每行一个JSON对象”的形式追加写入文件这是我在第三个章节要展开的实现细节。这里先给结论不要一开始就设计复杂的表结构舆情分析最核心的字段只有五个——正文文本、来源、发布时间、抓取时间、内容哈希。其他扩展字段地域、情感值、关联关键词属于分析阶段的产物应当由代码生成后另列存储避免采集脚本越写越重。3. 舆情数据采集一个requests脚本跑通采集-清洗-存储全流程3.1 字段设计与去重逻辑采集层的任务是把公开页面上的文本变成结构化的记录。一个标准的舆情记录包含这些字段字段名类型说明url_idstr对原始URL做MD5哈希取前16位作为唯一标识titlestr标题或摘要文本contentstr正文或评论内容清洗后存储sourcestr来源站点名称如“某新闻门户”或“某论坛”publish_timestr页面展示的发布时间原样保存crawl_timestr抓取时刻由程序生成ISO格式sentimentint情感标注分析阶段写入采集时先置为0这里的核心是url_id。后续每次增量采集前先加载历史url_id集合新抓到的记录如果哈希已存在就直接跳过这能保证断点续跑时不会产生重复数据。写采集脚本前先想清楚这个字段等于给自己留了后悔药。3.2 一个最小可运行的采集脚本下面是一个基于requests和parsel的采集示例。parsel这个库是Scrapy的底层选择器API和Scrapy一致但不需要启动整个爬虫框架适合中小型采集任务import requests import json import time import hashlib from parsel import Selector DATA_SOURCE_URL https://example.com/news/list # 换成你自己的数据源地址 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml, } def clean_text(raw: str) - str: 去掉空白字符和乱码标记保留中文与基础标点 return .join(raw.split()).replace(\u3000, ) def gen_url_id(url: str) - str: 利用MD5截断生成内容去重ID return hashlib.md5(url.encode(utf-8)).hexdigest()[:16] def fetch_page(): resp requests.get(DATA_SOURCE_URL, headersHEADERS, timeout10) resp.raise_for_status() selector Selector(textresp.text) records [] for item in selector.css(div.news-item): title item.css(h2::text).get() url item.css(a::attr(href)).get() ts item.css(span.time::text).get() if not title or not url: continue url url if url.startswith(http) else https://example.com url records.append({ url_id: gen_url_id(url), title: clean_text(title), content: , source: example_news, publish_time: ts, crawl_time: time.strftime(%Y-%m-%d %H:%M:%S), sentiment: 0, }) return records if __name__ __main__: page_records fetch_page() with open(raw_news.jsonl, a, encodingutf-8) as f: for record in page_records: f.write(json.dumps(record, ensure_asciiFalse) \n) print(f本次新增 {len(page_records)} 条累计追加到 raw_news.jsonl)逻辑说明fetch_page函数用requests.get请求列表页用parsel的CSS选择器抽取标题、链接和发布时间字段clean_text负责去掉字符串两端和中间多余的空白gen_url_id生成去重ID主程序把记录逐行追加到JSON Lines文件。之所以用“逐行追加”而不是一次写入整个列表是为了防止脚本中断时丢失已抓取的全部内存数据。参数说明timeout10表示十秒内没响应就抛出异常避免单个卡死请求拖垮整个任务resp.raise_for_status()会在返回状态码非200时直接报错让失败尽快暴露select.css里的“div.news-item”“h2::text”这些选择器必须对准你实际页面的HTML结构不同站点差异很大换数据源时这是第一个要改的地方。3.3 让采集不轻易挂的四个工程习惯很多人在采集阶段翻车不是因为代码写不出来而是没把“反爬应对”当工程问题处理。这里我不建议研究任何绕过机制只讲稳妥做法。第一条请求头必须至少带上User-Agent和Accept很多静态站点对缺失请求头的访问直接返回403。第二条两次请求之间用time.sleep加上随机延时比如sleep(random.uniform(1, 3))这既是对目标站点的基本礼貌也让流量曲线更像人工操作。第三条把已抓取的url_id保存到一个独立文件作为增量游标每处理完一条记录就更新一次重跑脚本时跳过历史数据。第四条凡是批量解析逻辑先用单条数据调试通过再循环跑全量否则一个选择器写错可能把几千条空记录写进文件。这四条习惯配合前面的脚本已经能支撑日增几千条的舆情采集。如果你后续数据源数量翻倍再考虑引入Scrapy框架它自带的调度器、并发控制和去重队列会让任务管理更省心但学习成本也上来了。从“能跑”到“跑得可靠”之间平价方案永远是控制频率而非堆工具。4. 情感分析不是黑匣子从Snownlp基线到Sklearn自训练模型4.1 三条技术路线怎么选情感分析是舆情系统的核心部件但很多人一上来就想着用大模型忽略了成本与数据量的匹配。这里先把三条路线摊开比较路线原理优点缺点适用场景词典法句子分词后匹配正负词词典统计得分无需标注数据速度极快否定句、反讽识别差前期快速验证标签分布机器学习文本转TF-IDF特征训练分类器可控、可解释效果稳定需要几百条人工标注样本中小规模舆情项目的核心方案深度学习用预训练模型如BERT做句子分类准确率最高GPU成本高标注样本需求大数据量大且有标注团队时对绝大多数课程设计和业务试点我推荐第二条路线用Snownlp打基线再用scikit-learn训练一个朴素贝叶斯或逻辑回归分类器两者结合既能快速出结果又能在答辩或汇报时讲清楚模型原理。4.2 用Snownlp跑出第一版情感分数Snownlp是一个纯Python实现的情感分析库默认模型基于电商购物评论训练可以直接拿来给文本打0到1之间的情感分from snownlp import SnowNLP texts [ 疫苗接种点排队秩序很好工作人员很耐心。, 抢不到物资配送速度太慢了。, 无症状感染者数量下降是个好消息。, 核酸排队两小时还没轮到太折腾了。, ] for text in texts: score SnowNLP(text).sentiments print(f{score:.2f} - {text})逻辑说明SnowNLP(text).sentiments对传入句子执行分词、词典匹配和概率计算返回的情感分数越接近1表示越正面越接近0表示越负面。第一版你不需要任何标注数据把全量文本跑一遍就能得到一条情感分布曲线。参数说明Snownlp的默认模型对商品评论的通用情感捕捉还行但面对疫情语料会出现明显偏差比如把“死亡人数上升”这种严重负面表述判成中性甚至正面。遇到这种情况不用慌这个基线只是用来观察数据分布的不是最终答案。4.3 用Sklearn训练自定义分类器当手头积累了两三百条人工标注数据0表示负面1表示正面2表示中性就可以切换到机器学习方案。下面是一个完整的训练与评估流程import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report import joblib # 假设你有标注好的CSV列名是text和label df pd.read_csv(labeled_data.csv) df df[df[label].isin([0, 1, 2])].copy() # 切分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( df[text], df[label], test_size0.2, random_state42, stratifydf[label] ) # 用TF-IDF把文本转成数值特征 vectorizer TfidfVectorizer( max_features5000, ngram_range(1, 2), token_patternr(?u)\b\w\b, sublinear_tfTrue, ) X_train_vec vectorizer.fit_transform(X_train) X_test_vec vectorizer.transform(X_test) # 训练朴素贝叶斯分类器 model MultinomialNB(alpha0.1) model.fit(X_train_vec, y_train) # 输出精确率、召回率、F1 y_pred model.predict(X_test_vec) print(classification_report(y_test, y_pred, target_names[negative, neutral, positive]))逻辑说明整个流程分五步——用pandas读取标注数据并过滤非法标签用train_test_split按类别的原始比例切分数据stratify参数确保三类样本在训练集和测试集中的占比一致避免切分后某一类在测试集中消失用TfidfVectorizer将原始文本转换为TF-IDF数值矩阵用多项式朴素贝叶斯训练模型最后用classification_report输出每个类别的精确率、召回率和F1值。参数说明max_features5000的意思是只保留出现频率最高的5千个特征词这个值可以按语料规模调整语料超过两万条可以放到10000ngram_range(1,2)同时使用单词和相邻双词组合能捕捉“不_满意”这类否定搭配alpha0.1是拉普拉斯平滑系数调小一点可以减少对未出现特征的过度惩罚。训练完成后用joblib.dump(model, sentiment_model.joblib)保存模型再写一个加载函数用于日常识别model joblib.load(sentiment_model.joblib) vectorizer joblib.load(tfidf_vectorizer.joblib) def predict_sentiment(text: str) - int: vec vectorizer.transform([text]) return int(model.predict(vec)[0])这里有一个很多初学者忽视的环节向量化器的训练必须和模型一起保存。否则新文本单独用transform时特征空间和训练时不一致预测结果会完全走样。4.4 从文本到趋势舆情指数怎么算有了每一条文本的情感值接下来要考虑的是怎么让业务方看得懂。趋势分析和可视化这里用pandas和pyecharts来做流程不再是一句一句看评论而是按天聚合出三个指标日均情感得分、负面占比、中性占比。其中负面占比比均值敏感得多某天出现大量负面短文本时均值可能只下降零点零几负面占比却会明显抬头。具体实现是给预测结果设阈值预测值为0视为负面按天分组统计df[sentiment_label] df[text].apply(predict_sentiment) df[date] pd.to_datetime(df[publish_time]).dt.date daily_stats df.groupby(date).apply( lambda g: pd.Series({ total: len(g), negative_ratio: (g[sentiment_label] 0).mean(), positive_ratio: (g[sentiment_label] 2).mean(), avg_score: g[sentiment_label].mean(), }) ) daily_stats daily_stats.reset_index() daily_stats[date_str] daily_stats[date].astype(str)逻辑说明这段代码先把每条文本跑一遍模型得到标签再把publish_time字符串转成日期类型并提取date部分最后按日期分组统计负面占比、正面占比和平均得分。注意(pd.to_datetime会报错时说明publish_time里有非标准格式这就引出了后面避坑章节里要讲的时间解析问题。最后一步生成date_str是为了后续给pyecharts图表当X轴标签。可视化部分最常见的图有三种按日期的情感得分折线图、负面占比面积图、关键词词云。pyecharts画折线图的代码如下from pyecharts.charts import Line from pyecharts import options as opts line ( Line() .add_xaxis(daily_stats[date_str].tolist()) .add_yaxis(负面占比, daily_stats[negative_ratio].round(3).tolist(), is_smoothTrue) .extend_axis(yaxisopts.AxisOpts(name负面占比)) .set_global_opts( title_optsopts.TitleOpts(title舆情负面占比走势), tooltip_optsopts.TooltipOpts(triggeraxis), ) ) line.render(negative_ratio_trend.html)逻辑说明add_xaxis传入日期列表add_yaxis传入负面占比序列is_smoothTrue让折线更平缓extend_axis为右侧添加一个坐标轴用于后续叠加文本量柱状图。render方法输出一个独立HTML文件浏览器直接打开就能看不需要启动任何后台服务。5. 舆情系统高频踩坑实录六个翻车点的现象、原因与解决5.1 分词结果把关键词切碎现象对“无症状感染者”进行词频统计得到的却是“无”“症状”“感染”三个碎片词词云里出现一堆没意义的单字。原因jieba默认词典没有收录“无症状感染者”这个医学复合词按HMM模式切分时把它拆开了。解决准备一个自定义词典文件custom_dict.txt每行写入一个词和词频然后加载import jieba jieba.load_userdict(custom_dict.txt)custom_dict.txt里至少要有无症状感染者、核酸检测、疫苗接种、行程码、隔离管控、物资配送。加载后重新执行分词和词频统计词云质量会明显提升。这条经验也适用于任何垂直领域项目舆情系统的第一步永远是先把领域词典补齐。5.2 Snownlp误判严重负面文本现象一个标注为负面的句子Snownlp给出0.87的高分导致当日负面占比被严重低估。原因Snownlp的默认模型训练语料来自电商评论“质量好”“物流快”是它熟悉的正面表达而疫情语料里的“确诊”“死亡”并未被模型正确映射到负面区间。解决不要依赖Snownlp作为最终模型。把它当特征之一或干脆只用它进行初步数据探索改用4.3节的Sklearn自训练方案。如果暂时没有标注数据至少要做一个负面词增强把“死亡”“确诊”“延误”“抢不到”等词加入自定义负面词典在词典法层面做一次兜底投票。5.3 采集脚本跑了三天开始大量超时现象前三天一切正常第四天开始连续超时日志里出现大量TimeoutError网页直接返回403。原因请求频率虽然不高但长时间保持相同的请求头模式目标站点对异常流量做了限制。解决第一步在请求头里加入随机User-Agent从常用浏览器的UA列表中随机挑选第二步把time.sleep(1)改为time.sleep(random.uniform(1.5, 4.5))打散请求间隔第三步给脚本加入失败重试机制单条请求失败三次后跳过而不是让整个进程卡死。def fetch_with_retry(url, headers, max_retries3): for attempt in range(max_retries): try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() return resp except requests.RequestException: if attempt max_retries - 1: raise time.sleep(2 * (attempt 1))这里的2 * (attempt 1)是指数退避策略第一次失败等2秒第二次等4秒第三次等6秒比固定间隔更有效。5.4 发布时间解析报错ValueError现象pd.to_datetime(df[publish_time])报错提示time data does not match format。原因不同来源的“发布时间”格式差异极大有“2023-01-15 10:00:00”这种标准格式也有“34分钟前”“昨天 15:30”这类模糊表述甚至有空值。解决写一个容错解析函数按已知格式依次尝试import pandas as pd from datetime import datetime, timedelta def parse_time(raw): if raw is None or str(raw) : return pd.NaT raw str(raw).strip() try: return pd.to_datetime(raw) except (ValueError, TypeError): pass if 分钟前 in raw: minutes int(raw.replace(分钟前, )) return datetime.now() - timedelta(minutesminutes) return pd.NaT逻辑说明先尝试标准解析失败再处理“分钟前”“小时前”这类相对时间。无法解析的返回pd.NaT后续需要做缺失值填充。这个函数的通用性很好做任何爬虫项目都能用上。5.5 中断后重新跑数据重复写入现象采集脚本跑了两小时后被人为中断重新启动后数据量翻倍统计结果完全失真。原因脚本没有记录“哪些记录已经入库”重跑时把历史页面重新抓取并追加了一遍。解决在每次追加写入时先加载已有url_id集合进行过滤from pathlib import Path import json seen_ids set() if Path(raw_news.jsonl).exists(): with open(raw_news.jsonl, encodingutf-8) as f: for line in f: try: seen_ids.add(json.loads(line)[url_id]) except json.JSONDecodeError: continue在写入前检查record[url_id]是否已在seen_ids中是则跳过。严格按这个逻辑每次重跑都是增量追加数据不会重复。5.6 训练模型时类别严重不均衡现象标注数据里正面样本占七成中性占两成负面只有一成。模型训练后负面类别的召回率只有0.2几乎所有负面文本都被预测成正面。原因MultinomialNB默认假设类别先验与训练集分布一致样本少的类别被整体压过。解决在模型初始化时指定class_weight参数或对样本做重采样。朴素贝叶斯不支持class_weight需要换LogisticRegressionfrom sklearn.linear_model import LogisticRegression model LogisticRegression( class_weightbalanced, max_iter1000, C0.5, )class_weightbalanced会让模型按样本量的倒数自动加权小样本类别的误分类代价变高。换成逻辑回归还有一个额外好处你可以用model.predict_proba拿到每条文本属于每个类别的概率后续做负面预警时可以按概率阈值筛选而不是只看硬标签。6. 让系统交付前再升一档评估指标、定时任务与轻量部署很多项目能跑通但经不起追问问题就出在缺少评估环节。情感分类器的效果不能只看准确率更要看负面类别的召回率因为舆情系统的核心价值是把负面信息找出来漏报比误报更危险。我的习惯是把分类报告里negative这一行的recall作为验收指标低于0.75就继续补标注数据不急着上线。注意测试集必须来自模型没见过的日期用时间切分而不是随机切分否则训练集和测试集可能包含相同事件的多条相似文本评估结果虚高。部署环节不需要一上来就写Web应用。先用系统计划任务把整条流水线串起来每天凌晨更新数据并重新统计0 2 * * * cd /opt/covid_analysis /opt/anaconda3/envs/covid_env/bin/python run_pipeline.py logs/run.log 21提示cron表达式里的“0 2”表示每天凌晨两点执行路径必须使用虚拟环境里的绝对路径python而不是系统python否则写好的cron任务会因模块缺失而静默失败。run_pipeline.py内部按顺序执行采集、清洗、情感分析、报表导出四个步骤。日志文件要定期轮转用logrotate或写个简单清理任务都行。如果要给同事看效果Flask在这时派上用场。写一个只有三个路由的小服务首页返回最近十四天情感趋势图表格页展示最新负面文本列表导出接口用openpyxl输出当周统计Excel。具体代码不复杂但值得注意的一点是Flask只监听127.0.0.1不要暴露在公网内部演示足够。我踩过最深的坑是负面预警阈值设成固定数值。某天负面自然波动上升系统消息响个不停最后只能关闭告警。后来改用双阈值方案负面占比超过近七天均值的1.5倍且持续两小时才触发预警。这样既保留敏感性又过滤了瞬时抖动。希望这个思路也能帮你在部署时少走一次弯路这套系统做到这一步无论用于答辩汇报还是业务试运行都已经具备完整的说服力。本文还有配套的精品资源点击获取
返回列表