简介:一份基于Python+Flask框架实现的新冠疫情可视化项目源码,目标用户是Python初学者、期末大作业及课程设计人群,覆盖数据抓取、接口封装、ECharts动态图表和前端页面交互的完整实现链路,代码内附详细注释,下载后简单配置即可运行。压缩包共含2022个文件,大小约28.89MB;其中1061个csv数据文件支撑疫情历史数据,780个json文件管理配置与数据接口,110个js文件实现图表交互,11个Python源文件承载Flask后端逻辑,另有CSS样式、字体图标和PNG图片等前端资源,目录结构清晰,便于按模块阅读和二次开发。项目提供全国与省份维度的实时统计、趋势曲线、地图分布等可视化页面,功能完整、界面简洁,能直接作为课程设计或期末大作业的高分参考。目前已有130人学习下载,对于想快速完成同类可视化项目的读者有较高实用价值。
1. 为什么课程设计选 Python + Flask 做新冠疫情可视化,以及"高分"到底高在哪
新冠疫情可视化是课程设计里最典型的「一条链」项目:有真实数据源、有地域维度、有时间维度、有动态变化,可视化出来的效果天然能打。选 Python + Flask 而不是前后端分离,是因为课程设计的验收场景通常是一台学生电脑、一个 Flask 进程同时扛住模板渲染和数据接口,不需要另起前端服务,也不需要上数据库,CSV 足够。所谓「高分项目源码」,我建议把它当成需求清单而不是下载目标——名字里带「源码」「高分」的打包资源,往往下载回来还要花大量时间改成自己讲得清的样子。真正拉开分数差距的只有三件事:数据能否现场拉通、图表能否交互、被追问某个函数时能否讲明白每一行。下面按数据采集、Flask 接口、ECharts 对接、加分排错四段把这套方案讲透,跟着做一天能出活。
2. 疫情数据采集:requests 拉公开接口、pandas 清洗与离线 CSV 兜底
2.1 数据源选型:公开接口、页面爬虫和离线 CSV 怎么搭配
做疫情可视化第一步不是画图,是把数据拿稳。常见的有三条路:写爬虫抓疫情专题页面、调公开 JSON 接口、直接用离线 CSV 文件。页面爬虫最不建议用于课程设计——页面结构改一次你的解析就崩,答辩现场没有任何容错空间,而且不少专题页带了基础的反爬校验,调试时间会远超你的预期。
公开 JSON 接口字段全、有现成的每日时间序列和省份维度,缺点是接口随时可能关闭或改字段,半年前写的解析代码今天跑不通是常态。离线 CSV 最稳,但数据是死的,展示不出「实时拉取」这个答辩加分点。我一般用「离线 CSV 打底 + 接口定时刷新」的组合:项目里放一份已经清洗好的历史 CSV,Flask 启动时先加载它保证页面秒开,同时用 requests 去请求公开接口,成功就覆盖更新 CSV,失败就继续用旧文件并打一条日志。演示时不会因为数据源抖动白屏,被问到更新机制时又能讲清楚增量刷新的设计。
2.2 requests 拉取疫情公开接口的最小代码与响应结构
下面这段代码是接入公开接口的最小实现,核心是requests.get加参数拼接,以及把 json 响应里的ret业务码和 HTTP 状态码分开判断。接口字段以实际返回为准,接入前先打印一次结构再写解析。
import requests def fetch_covid_data(): """从公开疫情接口拉取原始 JSON,返回 dict。 注意:接口字段会随数据源调整,接入前先 print(resp.text) 看一次结构。 """ url = "https://api.inews.qq.com/newsqa/v1/query/inner/publish/modules/list" params = { "modules": "chinaDayList,chinaDayAddList,nowConfirmStatis,provinceCompare" } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" } resp = requests.get(url, params=params, headers=headers, timeout=10) resp.raise_for_status() # HTTP 层:4xx/5xx 直接抛异常 payload = resp.json() if payload.get("ret") != 0: # 业务层:接口自身返回错误码 raise RuntimeError(f"接口返回异常: {payload.get('msg')}") return payload["data"]params里传的modules决定这次请求拿回哪几组数据,chinaDayList是全国每日累计序列,provinceCompare是各省最新快照,这两个是地图和折线图的核心来源。headers里的User-Agent必须有,很多接口会拒绝默认的python-requests标识。timeout=10是为了防止数据源卡住时把 Flask 启动流程阻塞住,课程设计演示现场最怕的就是页面长时间转圈。
返回的data是个 dict,key 和 modules 一一对应,字段含义大致如下表。不同时间段的接口字段名略有出入,写代码时以实际打印为准:
| 返回模块 | 内容 | 常见字段 |
|---|---|---|
| chinaDayList | 全国每日时间序列 | date, confirm, dead, heal, nowConfirm |
| chinaDayAddList | 全国每日新增 | date, confirm_add |
| nowConfirmStatis | 现存确诊汇总 | 各类统计数值 |
| provinceCompare | 各省最新快照 | province, confirm, nowConfirm, dead, heal |
2.3 pandas 清洗:拍平嵌套 JSON 并补算每日新增
接口拿到的数组通常直接用不成图表——日期是字符串、累计值和新增值混在一起、字段名也不是你前端想要的。清洗这一步我固定用 pandas,因为向量化操作比在循环里逐行 diff 快得多,更重要的是代码短,答辩时一句话能讲清。
import pandas as pd def clean_china_daily(raw_list) -> pd.DataFrame: """把接口返回的每日数组清洗成时序表。 raw_list 对应 data['chinaDayList']。 """ df = pd.DataFrame(raw_list) keep = ["date", "confirm", "suspect", "dead", "heal", "nowConfirm"] df = df[keep].copy() df["date"] = pd.to_datetime(df["date"], format="%Y.%m.%d") df = df.sort_values("date").reset_index(drop=True) df["confirm_add"] = df["confirm"].diff().fillna(0).astype(int) df["heal_add"] = df["heal"].diff().fillna(0).astype(int) return df # 清洗后落盘,utf-8-sig 保证 Excel 打开中文不乱码 # clean_china_daily(raw).to_csv("data/covid_history.csv", index=False, encoding="utf-8-sig")接口里的 date 类似"2020.01.13",必须转成datetime类型,否则 pandas 的排序会按字符串字典序排,导致 10 月排在 2 月前面,这个错非常隐蔽。diff()计算相邻两行的差值得到每日新增,fillna(0)处理第一行没有前值的情况,astype(int)把浮点结果转回整数,因为累计字段在接口里可能是字符串或浮点。
这里有个常见的错误做法:有些人拿到chinaDayAddList就直接当新增用,但该模块在不同时期字段变化大,统一「先算累计、再 diff 出新增」的清洗策略最保险,后面前端无论要累计线还是新增柱状图都只需要同一份 DataFrame。清洗完的数据落盘成 CSV,就是整条链路的离线兜底。
3. Flask 后端路由设计:统一响应格式、缓存策略与项目目录
3.1 项目目录结构与路由规划表
Flask 项目最怕写成单文件堆砌,路由、数据逻辑、页面全部挤在 app.py 里。课程设计虽然规模小,但目录划分直接体现工程意识。下面是这套方案我常用的结构:
covid_vis/ ├── app.py # Flask 应用入口,只注册路由 ├── service/ │ └── data_service.py # 数据加载、清洗、查询封装 ├── data/ │ ├── covid_history.csv # 全国历史数据(离线兜底) │ └── province_latest.csv ├── templates/ │ ├── index.html # 可视化大屏主页 │ └── province.html # 省份详情页 └── static/ ├── js/ ├── css/ └── vendor/echarts.min.js路由层和数据层分开之后,接口路径一目了然,也方便答辩时对着路由表讲设计。路由规划我固定用下面这一套:
| 路由 | 方法 | 返回内容 | 数据来源 |
|---|---|---|---|
/ | GET | 渲染 index.html 大屏 | 无 |
/api/overview | GET | 全国累计确诊、治愈、死亡、现存 | CSV |
/api/trend | GET | 全国每日时间序列 | CSV |
/api/province | GET | 各省最新数据,用于地图 | CSV |
/api/province/<name> | GET | 指定省份历史趋势 | CSV |
命名上统一用复数名词,/api/province/广东省这种路径语义清楚,不跟/api/province冲突。所有接口都走/api前缀,页面路由和接口路由分开,后面加鉴权或者换前端框架时不用大改。
3.2 核心接口实现:code/msg/data 响应与 DataService 封装
接口响应格式从前端消费角度定,统一成{code, msg, data}三段式。前端fetch之后只需要判断code === 0,不需要为每个接口单独写异常分支。
from flask import Flask, jsonify, render_template from service.data_service import DataService app = Flask(__name__) ds = DataService() # 启动时加载 CSV,进程内只初始化一次 @app.route("/") def index(): return render_template("index.html") @app.route("/api/overview") def overview(): return jsonify(code=0, msg="ok", data=ds.get_overview()) @app.route("/api/trend") def trend(): return jsonify(code=0, msg="ok", data=ds.get_trend()) @app.route("/api/province") def province(): return jsonify(code=0, msg="ok", data=ds.get_province_latest()) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)DataService在模块级实例化,意味着所有请求共享同一份内存中的 DataFrame,不会每次请求都重新读 CSV。host="0.0.0.0"是为了答辩时让老师用自己手机连同一个局域网直接访问,不用挤在你的电脑前面。debug=False一定要关,Werkzeug 调试器在演示现场刷新一次终端就刷一次屏,而且开着 debug 等于暴露了远程执行入口。
DataService 里有个关键的坑:pandas 的int64类型不能直接被jsonify序列化,必须在返回前转成 Python 原生int。
import pandas as pd class DataService: """统一封装 CSV 的加载和查询,所有路由只调方法不碰文件。""" def __init__(self, path="data/covid_history.csv"): try: self.df = pd.read_csv(path) except FileNotFoundError: self.df = self._fetch_and_clean_from_api() # 离线文件缺失时兜底 self.df["date"] = pd.to_datetime(self.df["date"]) def get_overview(self): row = self.df.iloc[-1] # 最后一行就是最新一天 return { "confirm": int(row["confirm"]), "heal": int(row["heal"]), "dead": int(row["dead"]), "nowConfirm": int(row["nowConfirm"]), } def get_trend(self): sub = self.df[["date", "confirm", "heal", "dead"]] return sub.to_dict(orient="records")iloc[-1]依赖前面清洗时已经按日期排过序,to_dict(orient="records")把 DataFrame 转成[{列名: 值}]的列表结构,正好是前端 ECharts 需要的格式。遇到TypeError: Object of type int64 is not JSON serializable就检查是不是有字段忘了int()转换,这是 pandas 和 Flask 搭配时出现频率最高的报错。
3.3 flask_caching 缓存:避免每次刷新都重新打数据源
数据量小的时候不做缓存也能跑,但地图页面要拖动、要缩放,每次交互都触发后端全量重算,答辩时如果数据源接口不稳定,体验会很难看。用 flask_caching 在接口层做缓存是成本最低的优化。
from flask_caching import Cache cache = Cache(app, config={ "CACHE_TYPE": "SimpleCache", # 进程内缓存,零依赖,够用 "CACHE_DEFAULT_TIMEOUT": 300 }) @app.route("/api/province") @cache.cached(timeout=120) def province(): return jsonify(code=0, msg="ok", data=ds.get_province_latest())@cache.cached(timeout=120)按请求路径做缓存,两分钟内重复访问同一个接口直接返回缓存结果,不重新读文件和计算。SimpleCache是进程内字典实现,单进程跑没有任何问题;如果以后拆成多进程或多实例部署,就要换成RedisCache并配置REDIS_URL。换上去之后想看缓存有没有生效,可以用 redis 可视化管理工具连上去看键列表,flask_cache_前缀的键出现就说明命中缓存了。
缓存粒度上要注意:/api/overview这种汇总数据缓存时间可以设长一点,因为一天之内累计值不会频繁变;/api/trend设 5 分钟也够。但如果你的项目做了「定时从接口拉取更新」,缓存时间必须小于更新周期,否则页面刷出来的永远是旧值,这个细节答辩时被问到会很扣分。
4. ECharts 可视化对接:地图注册、折线趋势与可视化大屏适配
4.1 图表选型:地图、折线、柱状图各放什么数据
疫情可视化最忌讳图表堆砌,每一类图都要对应一种业务问题。地图回答「哪里严重」,折线回答「趋势如何」,柱状图回答「谁排前列」,饼图回答「结构占比」。选型定下来之后,后端接口的设计也跟着定,不会出现前端想要的数据后端没给的情况。
| 数据视图 | 图表类型 | 对应接口 | 关键配置 |
|---|---|---|---|
| 全国各省累计 | 地图 | /api/province | map + visualMap |
| 全国每日趋势 | 折线图 | /api/trend | xAxis category + smooth |
| 省份确诊 Top10 | 柱状图 | /api/province前端排序 | 横向柱状图 |
| 治愈/死亡占比 | 环形饼图 | /api/overview | radius 百分比 |
我一般把地图放在大屏中央最显眼的位置,折线图放左侧,Top10 柱状图放右侧,饼图右下角。布局不用框架,CSS Grid 一行就够,关键是图表容器要用百分比宽度,为后面的适配做准备。
4.2 前后端对接:fetch 拉 /api 接口再 setOption
模板渲染和 AJAX 两种方式都可以,课程设计我推荐用fetch拉接口再setOption。模板渲染只有首次打开页面时有一次数据,图表做不了刷新和联动;AJAX 方式下后端接口可以独立测试,前端逻辑也清晰。
<div id="trendChart" style="width:100%;height:420px;"></div> <script src="/static/vendor/echarts.min.js"></script> <script> const trendChart = echarts.init(document.getElementById('trendChart')); async function renderTrend() { const res = await fetch('/api/trend'); const payload = await res.json(); if (payload.code !== 0) return; // 统一判断业务码 const rows = payload.data; trendChart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: ['累计确诊', '累计治愈'] }, xAxis: { type: 'category', data: rows.map(r => r.date.slice(0, 10)) }, yAxis: { type: 'value' }, series: [ { name: '累计确诊', type: 'line', smooth: true, data: rows.map(r => r.confirm) }, { name: '累计治愈', type: 'line', smooth: true, data: rows.map(r => r.heal) } ] }); } renderTrend(); </script>rows.map(r => r.date.slice(0, 10))是因为 CSV 里落盘的日期带时分秒,图表 x 轴只需要到天。tooltip的trigger: 'axis'保证鼠标悬停时同时显示两条线的数值,而不是单点弹框。smooth: true让折线带弧度,视觉上更像新闻报道里的趋势图。接口路径用相对路径/api/trend,这样不管 Flask 跑在 5000 还是 8080 端口,前端都不用改。
4.3 ECharts 5 中国地图注册与两个适配参数
ECharts 5 不再内置中国地图,必须手动registerMap。最省事的做法是把china.json放到static/vendor/下,页面里用 fetch 加载后注册。不带构建工具时 import 语法用不了,所以走 fetch 注册是课程设计里最不折腾的方案。
let mapChart; fetch('/static/vendor/china.json') .then(r => r.json()) .then(geo => { echarts.registerMap('china', geo); initMap(); }); function initMap() { mapChart = echarts.init(document.getElementById('mapChart')); mapChart.setOption({ visualMap: { min: 0, max: 2000, left: 20, bottom: 20, inRange: { color: ['#e8f4fd', '#d94e5d'] } // 浅到深的连续色带 }, series: [{ type: 'map', map: 'china', roam: true, label: { show: false }, data: [ { name: '湖北省', value: 67800 }, { name: '广东省', value: 2200 } ] }] }); }registerMap必须在setOption之前执行,否则地图区域会空白,这是每次都有人踩的坑。visualMap的min和max决定了颜色映射范围,max 设小了颜色全变深红,设大了看不出差异,我一般先取数据最大值再向上取整到千位。series.data里每一项的name必须是官方行政区划全名,写「湖北」不写「湖北省」就匹配不上地图区域。roam: true开启拖拽缩放,大屏展示时方便聚焦局部省份。
大屏适配两个参数必调:一是图表容器用百分比宽高,二是监听 resize 事件。很多项目图表只显示左上角一块,就是因为容器像素宽高写死,或者忘了注册 resize 回调。
function resizeAllCharts() { if (mapChart) mapChart.resize(); if (trendChart) trendChart.resize(); } window.addEventListener('resize', resizeAllCharts); // 首屏布局稳定后再执行一次,避免容器高度还没撑开就渲染 setTimeout(resizeAllCharts, 200);resize()是 ECharts 实例方法,容器尺寸变了必须重新计算画布。setTimeout延迟 200ms 是因为页面刚加载时 CSS 布局可能还没稳定,提前 resize 会把图表初始尺寸算错。如果觉得延迟不够稳,可以把图表初始化也放到window.onload之后。
5. 三个加分功能与高频报错定位顺序
5.1 时间轴播放与图表导出:答辩现场最能抓住眼球的两个功能
基础图表做完只算及格,两个功能能明显拉开差距。第一个是时间轴播放,让地图按日期动态变化。ECharts 的timeline组件把每一天的完整option放进数组,自动播放时逐帧切换。
mapChart.setOption({ timeline: { data: dates, // 日期数组,如 ['2020-01-20', '2020-01-21', ...] autoPlay: true, playInterval: 800, // 每帧间隔毫秒 bottom: 10 }, options: dates.map(date => ({ series: [{ type: 'map', map: 'china', data: provinceDataByDate[date] // 当天各省数据数组 }] })) });timeline的options数组长度必须和data数组长度一致,每个元素是一个完整的图表配置项,ECharts 会自动把相邻两帧做过渡动画。provinceDataByDate需要后端额外给一个按日期索引的接口,实现就是在 DataService 里按日期筛一遍 DataFrame,成本很低但视觉效果非常抢眼。
第二个加分功能是导出图片。大屏展示时老师想看某个省份细节,直接拖拽缩放再导出,比口头描述直观得多。
function exportMap() { const url = mapChart.getDataURL({ pixelRatio: 2, // 2 倍像素,导出的图在大屏上不糊 backgroundColor: '#fff' }); const a = document.createElement('a'); a.href = url; a.download = '疫情地图.png'; a.click(); }pixelRatio是导出清晰度的关键参数,默认 1 导出的图片在投影上会有锯齿,设成 2 就是双倍分辨率。最后再加一个隐蔽的加分点:把 CSV 缺失时自动去拉接口的兜底逻辑写进 DataService 的__init__,然后在启动日志里打印一行数据源加载方式:离线文件/在线接口,这个细节能直接证明你考虑过数据可用性。
5.2 高频报错定位顺序表
课程设计最常见的报错其实就那几类,按「前端先看控制台、后端先看终端」的顺序排查。
| 现象 | 第一排查动作 | 再往下看什么 |
|---|---|---|
| 启动报 ModuleNotFoundError | 确认虚拟环境已激活 | pip list看 flask、requests、pandas 是否安装 |
| 页面能开但图表空白 | F12 打开 Console | JS 报错、registerMap 是否在 setOption 之前 |
| 地图区域空白 | 打印/api/province返回 | data 的 name 是否为行政区划全名 |
| 接口转圈超时 | 看 Flask 终端日志 | 数据源接口是否下线,确认走离线 CSV 兜底 |
| 图表只占页面一角 | 检查容器 CSS | 父容器是否百分比宽高、resize 是否注册 |
| jsonify 报 int64 序列化错 | 检查 DataService 返回值 | 是否忘了转 int |
最后留半小时做一次「断网验收」:拔掉网线重启 Flask,所有图表应该基于 CSV 兜底数据正常渲染;再把 CSV 最后一行的confirm手动改掉,刷新页面确认变化生效。这一步同时验证了数据加载链路、缓存失效逻辑和前端渲染三处正确性,比答辩前临时改代码可靠得多。
本文还有配套的精品资源,点击获取