简介:本资源是一套完整的Python数据工程实践项目,面向计算机、数学、电子信息等专业的本科生及初学者,聚焦疫情数据与社交媒体舆情的采集、治理与分析全流程。项目涵盖疫情实时数据爬取、微博关键词定向抓取(含MySQL数据库存储)、结构化数据清洗与特征构建、多维度可视化呈现(含疫情趋势图与词云),以及基于情感词典的微博文本细粒度情感分析,适合作为课程设计、期末大作业或毕业设计参考。压缩包共15个文件,含7个核心Python脚本(爬虫、预处理、可视化、情感分析模块)、3个CSV数据集(含标注样本与情感关键词库)、2个文本词典(正/负向词表)、1个JSON微博原始数据、1张结果示意图及1份Markdown项目说明文档,整体大小23.87MB。已有728人学习下载,提供开箱即用的完整代码链路、清晰的模块划分与可复现的数据分析流程,便于理解数据从采集到价值输出的闭环逻辑。
1. 项目概述:一个数据驱动的疫情社会观察系统
最近在整理硬盘,翻出来一个前两年做的老项目,当时主要是想看看社交媒体上的公众情绪和官方发布的疫情数据之间有没有什么有趣的关联。这个项目麻雀虽小,五脏俱全,从数据抓取、清洗、存储到分析和可视化,走完了一个完整的数据分析闭环。它不是简单的“爬虫+可视化”的堆砌,核心在于将结构化的疫情统计数据与非结构化的社交媒体文本进行关联分析,试图从数据层面理解特定时期的社会心态。
简单来说,这个系统干了三件事:第一,从权威数据源定时抓取结构化的疫情数据(如新增确诊、累计治愈等);第二,同步从微博抓取与疫情相关的关键词下的实时讨论内容;第三,将这两类数据清洗后存入数据库,并进行情感倾向分析,最后通过可视化图表呈现数据趋势与公众情绪的对比。这非常适合想通过一个实战项目,系统学习Python数据获取、处理、分析和呈现全流程的朋友,尤其是对社会计算或舆情分析感兴趣的同学。
2. 项目整体架构与技术选型解析
2.1 核心模块拆解与设计思路
整个项目可以清晰地划分为四个核心模块,它们之间通过数据库进行数据流转,形成一个松耦合但逻辑紧密的管道。
数据采集层:这是项目的“触手”。我将其设计为两个独立的爬虫,但共享同一套请求管理和异常处理机制。
- 疫情数据爬虫:目标是各级卫健委官网或聚合数据平台。这类数据通常以JSON接口或规整的HTML表格形式存在,变化频率低(每日更新),但要求数据准确、稳定。因此,这个爬虫的核心是稳健性,需要处理好反爬策略(如请求头、频率限制),并设计重试和验证逻辑,确保抓取到的每个数字都是可靠的。
- 微博关键词爬虫:目标是微博的搜索页面或移动端接口。微博数据是非结构化的文本、时间、用户信息混合体,且反爬严密(动态参数、登录态、滑块验证)。这个爬虫的核心是突破性与效率。我选择模拟移动端请求,并利用关键词轮询,持续抓取最新发布的微博。这里的关键在于如何稳定地获取数据,而不是一味求快。
注意:微博爬虫的合规性是红线。本项目代码仅用于学习网络请求解析、动态页面处理和数据抽取技术。实际应用中,必须严格遵守
robots.txt协议,控制请求频率,避免对目标服务器造成压力,且所有数据仅应用于个人学习与研究分析。
数据存储层:选用关系型数据库MySQL作为数据仓库。为什么不用更简单的CSV或NoSQL?因为我们需要处理两类差异很大的数据,并且后续需要频繁地进行关联查询和聚合分析。
- 表结构设计:
epidemic_data表:存储日期、地区、新增确诊、新增疑似、累计确诊、治愈、死亡等字段。以(date, region)作为复合主键。weibo_data表:存储微博ID、发布时间、用户昵称、微博正文、关键词、抓取时间等。最重要的是,我增加了一个cleaned_text字段,用于存放预处理后的纯文本,为情感分析做准备。- 这种设计便于我们执行诸如“查询某日全国新增确诊数,并关联分析当天‘隔离’关键词下的微博情感倾向”这样的操作。
数据处理与分析层:这是项目的“大脑”。
- 数据预处理:这是最繁琐但决定分析质量的一步。对于疫情数据,主要是处理缺失值、异常值(如某日数据为负)。对于微博文本,则复杂得多:去除广告、表情符号、URL链接;进行中文分词;去除停用词(如“的”、“了”);文本规范化。
- 情感分析:我采用了基于预训练模型(如
SnowNLP或百度AI开放平台的情感倾向分析API)的方法。SnowNLP适合离线、大批量处理,虽然精度不是最高,但一致性较好。将微博cleaned_text字段的内容送入模型,得到情感极性分值(如0-1,越接近1越积极),存入数据库的新字段sentiment_score中。
数据可视化层:使用Matplotlib+Seaborn库,目标是生成能清晰对比趋势的图表。
- 疫情数据通常用折线图展示新增/累计趋势,用堆叠面积图展示确诊、治愈、死亡病例的构成变化。
- 微博情感数据可以用每日情感平均分折线图来观察情绪波动,用热力图展示不同关键词在不同时间段的情感强度。
- 最关键的,是将两条折线(例如“当日新增确诊数”和“当日‘疫情’关键词微博平均情感分”)放在同一个坐标系中,观察它们的走势关联,这是整个项目视觉上的亮点。
2.2 关键技术栈与工具选型理由
- Python 3.8+: 生态丰富,是数据科学和爬虫领域的事实标准。
- Requests & BeautifulSoup4: 用于疫情数据爬取和静态页面解析。
Requests简洁高效,BeautifulSoup对新手友好,足以应对大多数政府公开数据页面。 - Selenium (可选): 作为微博爬虫的备选方案。当接口逆向工程困难时,可以用
Selenium模拟浏览器行为直接获取渲染后的数据,缺点是速度慢、资源消耗大。本项目核心是学习,因此我同时提供了Requests+逆向和Selenium两种思路的代码注释。 - PyMySQL / SQLAlchemy: 用于连接和操作MySQL数据库。
PyMySQL更直接,SQLAlchemy的ORM模式能让代码更优雅,适合中型项目。 - Pandas & NumPy: 数据预处理的绝对核心。
Pandas的DataFrame是处理表格数据的利器,清洗、转换、聚合操作都非常方便。 - Jieba: 最好的中文分词Python库之一,用于对微博正文进行分词。
- SnowNLP / TextBlob: 轻量级的情感分析库。
SnowNLP针对中文优化,开箱即用,适合学习入门。 - Matplotlib & Seaborn:
Matplotlib是基础绘图库,功能强大但配置繁琐;Seaborn基于前者,提供了更美观的统计图表样式和高级接口,两者结合使用效率最高。 - Schedule / APScheduler: 用于实现爬虫的定时任务,让数据采集自动化运行。
3. 核心模块实现细节与实操要点
3.1 双爬虫的稳健实现与防封禁策略
疫情数据爬虫的实现: 关键在于找到稳定、结构化的数据源。我以某数据平台的公开API为例。
import requests import pandas as pd import time def fetch_epidemic_data(date_str): """ 获取指定日期的疫情数据 """ url = f"https://api.example.com/epidemic/data" # 示例URL,实际需替换 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...', 'Accept': 'application/json' } params = {'date': date_str} try: # 添加延迟,避免请求过快 time.sleep(1) resp = requests.get(url, headers=headers, params=params, timeout=10) resp.raise_for_status() # 检查HTTP错误 data = resp.json() # 解析JSON,转换为规整的列表 records = [] for item in data['results']: record = { 'date': item['date'], 'region': item['region']['name'], 'confirmed_new': item['confirmed']['new'], 'confirmed_total': item['confirmed']['total'], 'cured_new': item['cured']['new'], # ... 其他字段 } records.append(record) return pd.DataFrame(records) except requests.exceptions.RequestException as e: print(f"请求失败: {e}") return pd.DataFrame() # 返回空DataFrame,后续统一处理实操心得:对于公开API,务必仔细阅读其文档,了解调用频率限制。将请求头
User-Agent设置为常见的浏览器标识,并合理使用time.sleep()是基本的礼貌。所有网络请求都必须包裹在try-except块中,并记录日志,这是保证爬虫长期稳定运行的基础。
微博关键词爬虫的实现: 这里以通过解析移动端接口为例,难度较高。
import requests import re import json def fetch_weibo_by_keyword(keyword, max_page=5): """ 通过关键词搜索微博(模拟移动端接口) """ base_url = "https://m.weibo.cn/api/container/getIndex" headers = { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15...', 'Referer': f'https://m.weibo.cn/search?keyword={keyword}' } weibo_list = [] for page in range(1, max_page+1): params = { 'containerid': f'100103type=1&q={keyword}', 'page_type': 'searchall', 'page': page } try: resp = requests.get(base_url, headers=headers, params=params, timeout=15) data = resp.json() # 解析cards中的微博信息 if data.get('ok') == 1: cards = data.get('data', {}).get('cards', []) for card in cards: mblog = card.get('mblog') if mblog: weibo_info = { 'id': mblog.get('id'), 'text': mblog.get('text'), 'created_at': mblog.get('created_at'), 'user_name': mblog.get('user', {}).get('screen_name'), 'keyword': keyword } # 简单清洗HTML标签 weibo_info['text'] = re.sub(r'<[^>]+>', '', weibo_info['text']) weibo_list.append(weibo_info) time.sleep(2) # 非常重要!页间延迟 except Exception as e: print(f"抓取关键词'{keyword}'第{page}页失败: {e}") break return weibo_list核心技巧:微博的反爬策略会经常变动。
containerid等参数可能需要通过浏览器开发者工具抓包分析获取。User-Agent必须设置为移动端。页间延迟(time.sleep)是避免IP被临时封禁的最有效手段之一,不要贪快。此外,可以考虑使用IP代理池来应对更严格的限制,但这超出了基础学习的范围。
3.2 数据库设计与高效数据入库
我使用SQLAlchemy来定义数据模型,这样代码更清晰,也便于维护。
from sqlalchemy import create_engine, Column, String, Integer, Float, Date, Text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker Base = declarative_base() class EpidemicData(Base): __tablename__ = 'epidemic_data' id = Column(Integer, primary_key=True, autoincrement=True) date = Column(Date, nullable=False) region = Column(String(50), nullable=False) confirmed_new = Column(Integer) confirmed_total = Column(Integer) cured_new = Column(Integer) # ... 其他字段 # 设置复合唯一约束,防止重复插入同一天同一地区的数据 __table_args__ = (UniqueConstraint('date', 'region', name='uix_date_region'),) class WeiboData(Base): __tablename__ = 'weibo_data' id = Column(Integer, primary_key=True, autoincrement=True) weibo_id = Column(String(30), unique=True, nullable=False) # 微博唯一ID created_at = Column(String(30)) user_name = Column(String(100)) raw_text = Column(Text) # 原始文本 cleaned_text = Column(Text) # 清洗后文本 keyword = Column(String(50)) sentiment_score = Column(Float) # 情感分析得分 crawl_time = Column(Date) # 抓取时间 # 数据库连接与会话 engine = create_engine('mysql+pymysql://user:password@localhost:3306/epidemic_analysis?charset=utf8mb4') Base.metadata.create_all(engine) # 创建表 Session = sessionmaker(bind=engine)注意事项:
- 字符集:微博文本包含大量Emoji和特殊符号,MySQL的
utf8mb4字符集是必须的,标准的utf8无法存储四字节的Emoji。- 唯一约束:为微博ID设置
UNIQUE约束,可以避免在多次抓取中插入重复数据。疫情数据表则通过(date, region)的复合唯一约束来去重。- 批量插入:无论是
Pandas的to_sql方法(设置if_exists='append'),还是使用SQLAlchemy的session.bulk_save_objects(),都比逐条插入session.add()快一个数量级。处理爬虫数据时,务必采用批量操作。
3.3 数据预处理的实战流程
数据预处理在单独的脚本中完成,它从数据库读取原始数据,处理后再写回或存入新表。
疫情数据清洗:
import pandas as pd from sqlalchemy import create_engine engine = create_engine('mysql+pymysql://...') df_epidemic = pd.read_sql('SELECT * FROM epidemic_data WHERE date > "2022-01-01"', engine) # 1. 处理缺失值:对于数值字段,用前向填充或插值法 df_epidemic['confirmed_new'].fillna(method='ffill', inplace=True) # 2. 处理异常值:假设新增确诊不应为负,将其置为0或标记 df_epidemic.loc[df_epidemic['confirmed_new'] < 0, 'confirmed_new'] = 0 # 3. 确保日期格式 df_epidemic['date'] = pd.to_datetime(df_epidemic['date']) # 4. 去重(基于数据库约束,这里作为二次保险) df_epidemic.drop_duplicates(subset=['date', 'region'], keep='first', inplace=True)微博文本清洗与情感分析: 这是更核心的部分。
import re import jieba from snownlp import SnowNLP import numpy as np def clean_weibo_text(text): """清洗单条微博文本""" if not isinstance(text, str): return '' # 去除网页标签、URL、@用户、话题符号 text = re.sub(r'<[^>]+>', '', text) text = re.sub(r'http[s]?://\S+', '', text) text = re.sub(r'@[\w\u4e00-\u9fa5]+', '', text) text = re.sub(r'#([^#]+)#', r'\1', text) # 保留话题内容,去掉#号 # 去除Emoji和特殊符号(简单版) text = re.sub(r'[^\w\u4e00-\u9fa5,。!?、;:“”‘’()《》【】\s]', '', text) # 分词并用空格连接(SnowNLP可直接处理字符串,但分词后效果更好) words = jieba.lcut(text) cleaned = ' '.join([w for w in words if w.strip()]) return cleaned def analyze_sentiment_batch(text_list): """批量情感分析,返回分数列表""" scores = [] for text in text_list: if text and len(text.strip()) > 5: # 过滤过短文本 try: s = SnowNLP(text) scores.append(s.sentiments) # 情感积极度,0-1 except: scores.append(0.5) # 分析失败,取中性值 else: scores.append(0.5) # 无意义文本,取中性值 return scores # 从数据库读取原始微博数据 df_weibo = pd.read_sql('SELECT id, raw_text FROM weibo_data WHERE cleaned_text IS NULL', engine) # 应用清洗函数 df_weibo['cleaned_text'] = df_weibo['raw_text'].apply(clean_weibo_text) # 批量情感分析 texts_for_analysis = df_weibo['cleaned_text'].tolist() sentiment_scores = analyze_sentiment_batch(texts_for_analysis) df_weibo['sentiment_score'] = sentiment_scores # 将清洗后的数据和情感分写回数据库 # ... (使用df_weibo.to_sql或ORM更新对应记录)经验之谈:文本清洗没有“标准答案”。你需要根据分析目标调整清洗强度。例如,如果关心用户情绪,保留表情符号可能更有价值(需要专门的表情分析)。
SnowNLP的情感分析对于长文本和正式文本效果尚可,但对于微博短文本、网络流行语、反讽句的识别能力有限。这是所有情感分析工具的共性局限,在解读结果时必须心中有数。
4. 数据可视化:让趋势与关联一目了然
可视化不是为了画图而画图,而是为了回答问题。我们的核心问题是:疫情数据的变化与公众情绪波动有关联吗?
4.1 疫情趋势可视化
首先,我们绘制疫情本身的发展趋势。
import matplotlib.pyplot as plt import seaborn as sns sns.set_style("whitegrid") # 设置Seaborn样式 plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文显示问题 plt.rcParams['axes.unicode_minus'] = False # 假设我们已经有一个按日期聚合的疫情DataFrame: df_epidemic_daily fig, axes = plt.subplots(2, 1, figsize=(14, 10)) # 子图1:新增确诊与新增治愈的对比(折线图) axes[0].plot(df_epidemic_daily['date'], df_epidemic_daily['confirmed_new'], label='新增确诊', color='coral', linewidth=2) axes[0].plot(df_epidemic_daily['date'], df_epidemic_daily['cured_new'], label='新增治愈', color='lightgreen', linewidth=2) axes[0].set_title('每日新增确诊与新增治愈趋势对比') axes[0].set_xlabel('日期') axes[0].set_ylabel('人数') axes[0].legend() axes[0].fill_between(df_epidemic_daily['date'], 0, df_epidemic_daily['confirmed_new'], color='coral', alpha=0.3) # 子图2:累计确诊、治愈、死亡的堆叠面积图 axes[1].stackplot(df_epidemic_daily['date'], df_epidemic_daily['confirmed_total'], df_epidemic_daily['cured_total'], df_epidemic_daily['dead_total'], labels=['累计确诊', '累计治愈', '累计死亡'], colors=['lightcoral', 'lightgreen', 'gray']) axes[1].set_title('累计病例构成变化') axes[1].set_xlabel('日期') axes[1].set_ylabel('累计人数') axes[1].legend(loc='upper left') plt.tight_layout() plt.savefig('epidemic_trend.png', dpi=300, bbox_inches='tight') plt.show()4.2 微博情感趋势与关联分析
接下来,分析微博情绪,并尝试与疫情数据关联。
# 从数据库读取按日期和关键词聚合的情感平均分 df_sentiment_daily = pd.read_sql(""" SELECT DATE(created_at) as date, keyword, AVG(sentiment_score) as avg_sentiment FROM weibo_data WHERE sentiment_score IS NOT NULL GROUP BY DATE(created_at), keyword ORDER BY date """, engine) # 假设我们关注“疫情”这个关键词 df_keyword = df_sentiment_daily[df_sentiment_daily['keyword'] == '疫情'].copy() # 合并疫情数据与情感数据(按日期连接) df_merged = pd.merge(df_epidemic_daily, df_keyword, on='date', how='inner') fig, ax1 = plt.subplots(figsize=(15, 7)) color = 'tab:red' ax1.set_xlabel('日期') ax1.set_ylabel('新增确诊数', color=color) line1 = ax1.plot(df_merged['date'], df_merged['confirmed_new'], color=color, label='新增确诊', linewidth=2) ax1.tick_params(axis='y', labelcolor=color) # 创建第二个Y轴,共享同一个X轴 ax2 = ax1.twinx() color = 'tab:blue' ax2.set_ylabel('微博情感平均分', color=color) line2 = ax2.plot(df_merged['date'], df_merged['avg_sentiment'], color=color, label='情感分(疫情关键词)', linestyle='--') ax2.tick_params(axis='y', labelcolor=color) # 情感分范围是0-1,可以固定Y轴范围以便观察波动 ax2.set_ylim(0, 1) # 添加图例 lines = line1 + line2 labels = [l.get_label() for l in lines] ax1.legend(lines, labels, loc='upper left') plt.title('新增确诊趋势与“疫情”关键词微博情感趋势对比') fig.tight_layout() plt.savefig('correlation_analysis.png', dpi=300) plt.show()通过这张双Y轴图,我们可以直观地观察两条曲线的走势。例如,可能发现在新增确诊数大幅攀升后的几天,微博情感平均分会出现一个明显的低谷(情绪转向消极)。这只是一个粗略的观察,严谨的相关性分析还需要计算统计指标(如皮尔逊相关系数)。
5. 项目部署、优化与常见问题排查
5.1 工程化与自动化部署
一个学习项目不能只停留在Jupyter Notebook里。我将其模块化,并编写了主调度脚本。
spider_epidemic.py: 疫情数据爬虫模块。spider_weibo.py: 微博爬虫模块。data_processor.py: 数据清洗与情感分析模块。visualizer.py: 可视化绘图模块。config.py: 配置文件,存放数据库连接信息、API密钥(如有)、关键词列表等。main_scheduler.py: 主调度脚本,使用schedule库定时运行任务。
# main_scheduler.py 示例 import schedule import time from spider_epidemic import main as run_epidemic_spider from spider_weibo import main as run_weibo_spider from data_processor import main as run_data_processor def job(): print("开始每日数据任务...") run_epidemic_spider() # 抓取昨日疫情数据 run_weibo_spider() # 抓取最新微博 run_data_processor() # 处理新数据并分析 print("每日数据任务完成。") # 每天上午9点执行 schedule.every().day.at("09:00").do(job) while True: schedule.run_pending() time.sleep(60)可以将这个脚本放在服务器上,用nohup或systemd在后台运行,实现全自动化数据流水线。
5.2 性能优化与扩展思路
- 异步爬虫:对于微博这类需要大量I/O等待的爬虫,可以使用
aiohttp+asyncio实现异步并发,极大提升抓取效率。但要注意目标网站的压力,控制并发数。 - 增量爬取:微博爬虫不应每次都从头翻页。可以记录上次抓取到的最新微博ID,下次请求时只抓取比这个ID更新的数据。
- 情感分析升级:
SnowNLP是基线方案。可以尝试使用更强大的预训练模型,如BERT,在自己的标注数据上微调,以获得更精准的情感分类(积极、消极、中性)甚至情绪识别(愤怒、恐惧、悲伤等)。 - 前端展示:用
Flask或Django搭建一个简单的Web应用,使用ECharts或Plotly库生成交互式图表,让分析结果可以通过网页动态查看。
5.3 常见问题与排查实录
在开发和运行过程中,我遇到了不少坑,这里记录下最典型的几个:
问题1:疫情数据爬虫突然返回空数据或403错误。
- 排查:首先检查目标网站是否改版,API地址或参数是否变化。其次,检查请求头
User-Agent和Referer是否被屏蔽,尝试更换。最后,检查IP是否被临时封禁。 - 解决:更新请求头信息;在代码中加入更长的随机延迟;考虑使用付费的代理IP服务(针对高频率抓取);如果网站提供,优先使用其官方数据接口或开放数据集。
问题2:微博爬虫运行一段时间后,返回的数据全是“加载中”或需要登录。
- 排查:这是最典型的问题。微博的未登录态访问有严格限制和频率限制。通过浏览器开发者工具,检查网络请求中是否包含
cookies或authorization等认证信息。 - 解决:
- 方案A(简单但不稳定):用
Selenium模拟登录,获取cookies后供Requests使用。但登录可能触发滑块验证。 - 方案B(推荐学习):研究微博移动端或PC端的登录接口,逆向加密过程,实现程序化登录获取令牌(
access_token)。这涉及复杂的逆向工程,是爬虫技术进阶的必经之路。 - 忠告:对于学习项目,可以手动获取一次
cookies放入代码中短期使用。长期运行必须考虑自动化更新认证信息的机制。
- 方案A(简单但不稳定):用
问题3:情感分析结果不准确,很多反讽句被判断为积极。
- 排查:这是自然语言处理,特别是中文短文本情感分析的普遍难题。
SnowNLP的训练语料比较通用,对网络用语和反讽识别能力弱。 - 解决:
- 降低期望:理解工具的局限性,将分析结果视为一种“情绪热度”的粗略指标,而非精确的情感判断。
- 后处理规则:建立一份“负面词”词典,如果文本中包含“无语”、“离谱”等词,即使模型打分高,也手动调整为负面或中性。
- 升级模型:如5.2所述,使用更先进的模型并在特定领域的语料上微调。
问题4:可视化图表中文显示为方框。
- 排查:Matplotlib默认字体不包含中文。
- 解决:代码中已给出方案。确保系统存在中文字体(如
SimHei黑体),并通过plt.rcParams['font.sans-serif']进行设置。在Linux服务器上部署时,可能需要手动安装中文字体。
问题5:数据库写入速度慢。
- 排查:是否在循环中逐条执行
session.add()和session.commit()? - 解决:务必使用批量操作。对于Pandas,用
df.to_sql(..., if_exists='append', index=False, chunksize=500)。对于SQLAlchemy ORM,使用session.bulk_save_objects(list_of_objects),最后再统一session.commit()。
这个项目从构思到实现,几乎踩遍了数据采集和分析各个阶段的常见坑。它最大的价值不在于得出了某个惊天动地的结论,而在于完整地实践了一套数据驱动的解决方案。当你亲手让散落各处的数据,经过自己的代码管道,最终变成一幅能讲述故事的图表时,那种感觉远比单纯调用一个分析API要深刻得多。对于初学者,我建议不要一开始就追求大而全,可以先实现疫情数据爬取和可视化,再逐步加入微博爬虫和情感分析,像搭积木一样把这个系统构建起来,每一步都吃透其中的原理和问题,这样收获才是实实在在的。
本文还有配套的精品资源,点击获取