简介一份基于PythonFlask框架实现的新冠疫情可视化项目源码目标用户是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 打底 接口定时刷新」的组合项目里放一份已经清洗好的历史 CSVFlask 启动时先加载它保证页面秒开同时用 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, paramsparams, headersheaders, timeout10) 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标识。timeout10是为了防止数据源卡住时把 Flask 启动流程阻塞住课程设计演示现场最怕的就是页面长时间转圈。返回的data是个 dictkey 和 modules 一一对应字段含义大致如下表。不同时间段的接口字段名略有出入写代码时以实际打印为准返回模块内容常见字段chinaDayList全国每日时间序列date, confirm, dead, heal, nowConfirmchinaDayAddList全国每日新增date, confirm_addnowConfirmStatis现存确诊汇总各类统计数值provinceCompare各省最新快照province, confirm, nowConfirm, dead, heal2.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(dropTrue) 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, indexFalse, encodingutf-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/overviewGET全国累计确诊、治愈、死亡、现存CSV/api/trendGET全国每日时间序列CSV/api/provinceGET各省最新数据用于地图CSV/api/province/nameGET指定省份历史趋势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(code0, msgok, datads.get_overview()) app.route(/api/trend) def trend(): return jsonify(code0, msgok, datads.get_trend()) app.route(/api/province) def province(): return jsonify(code0, msgok, datads.get_province_latest()) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)DataService在模块级实例化意味着所有请求共享同一份内存中的 DataFrame不会每次请求都重新读 CSV。host0.0.0.0是为了答辩时让老师用自己手机连同一个局域网直接访问不用挤在你的电脑前面。debugFalse一定要关Werkzeug 调试器在演示现场刷新一次终端就刷一次屏而且开着 debug 等于暴露了远程执行入口。DataService 里有个关键的坑pandas 的int64类型不能直接被jsonify序列化必须在返回前转成 Python 原生int。import pandas as pd class DataService: 统一封装 CSV 的加载和查询所有路由只调方法不碰文件。 def __init__(self, pathdata/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(orientrecords)iloc[-1]依赖前面清洗时已经按日期排过序to_dict(orientrecords)把 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(timeout120) def province(): return jsonify(code0, msgok, datads.get_province_latest())cache.cached(timeout120)按请求路径做缓存两分钟内重复访问同一个接口直接返回缓存结果不重新读文件和计算。SimpleCache是进程内字典实现单进程跑没有任何问题如果以后拆成多进程或多实例部署就要换成RedisCache并配置REDIS_URL。换上去之后想看缓存有没有生效可以用 redis 可视化管理工具连上去看键列表flask_cache_前缀的键出现就说明命中缓存了。缓存粒度上要注意/api/overview这种汇总数据缓存时间可以设长一点因为一天之内累计值不会频繁变/api/trend设 5 分钟也够。但如果你的项目做了「定时从接口拉取更新」缓存时间必须小于更新周期否则页面刷出来的永远是旧值这个细节答辩时被问到会很扣分。4. ECharts 可视化对接地图注册、折线趋势与可视化大屏适配4.1 图表选型地图、折线、柱状图各放什么数据疫情可视化最忌讳图表堆砌每一类图都要对应一种业务问题。地图回答「哪里严重」折线回答「趋势如何」柱状图回答「谁排前列」饼图回答「结构占比」。选型定下来之后后端接口的设计也跟着定不会出现前端想要的数据后端没给的情况。数据视图图表类型对应接口关键配置全国各省累计地图/api/provincemap visualMap全国每日趋势折线图/api/trendxAxis category smooth省份确诊 Top10柱状图/api/province前端排序横向柱状图治愈/死亡占比环形饼图/api/overviewradius 百分比我一般把地图放在大屏中央最显眼的位置折线图放左侧Top10 柱状图放右侧饼图右下角。布局不用框架CSS Grid 一行就够关键是图表容器要用百分比宽度为后面的适配做准备。4.2 前后端对接fetch 拉 /api 接口再 setOption模板渲染和 AJAX 两种方式都可以课程设计我推荐用fetch拉接口再setOption。模板渲染只有首次打开页面时有一次数据图表做不了刷新和联动AJAX 方式下后端接口可以独立测试前端逻辑也清晰。div idtrendChart stylewidth: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(); /scriptrows.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 打开 ConsoleJS 报错、registerMap 是否在 setOption 之前地图区域空白打印/api/province返回data 的 name 是否为行政区划全名接口转圈超时看 Flask 终端日志数据源接口是否下线确认走离线 CSV 兜底图表只占页面一角检查容器 CSS父容器是否百分比宽高、resize 是否注册jsonify 报 int64 序列化错检查 DataService 返回值是否忘了转 int最后留半小时做一次「断网验收」拔掉网线重启 Flask所有图表应该基于 CSV 兜底数据正常渲染再把 CSV 最后一行的confirm手动改掉刷新页面确认变化生效。这一步同时验证了数据加载链路、缓存失效逻辑和前端渲染三处正确性比答辩前临时改代码可靠得多。本文还有配套的精品资源点击获取