
简介这是一份基于Python Flask框架的天气预报数据可视化分析系统项目源码包面向正在做毕业设计或练习数据分析可视化开发的Python学习者。系统通过接口获取和风天气的全国实时与历史天气数据经清洗后存入MySQL再借助ECharts完成综合天气、各城市天气及历史趋势的可视化展示同时包含用户登录注册与信息管理模块覆盖数据采集、处理、展示到管理的完整链路。压缩包共206个文件约33.13MB主体为Python脚本py与Flask模板pug/html并包含大量JavaScript和CSS样式文件用于前端交互另有SQL数据库脚本可直接初始化表结构整体目录结构清晰便于定位功能模块。目前已有735人学习下载适合用做课程设计参考或二次开发基础能帮助理解Flask项目分层、数据爬取清洗及ECharts可视化的实践方法。1. Python毕设做天气可视化真正的难点不在接口而在分析链路一个基于 Python 的天气预报数据可视化分析系统很多人的第一反应是找一个天气 API调出 JSON 后在前端表格里展示一遍就收工。可答辩时真正会问的是数据怎么存、温度趋势怎么算、接口挂掉怎么办、图表里的数据到底是后端给的还是前端 mock 的。这套课题的真正价值是把“数据获取 → 清洗入库 → Flask 提供接口 → HTML 前端消费 → 图表可视化分析”整条链路串起来而不是做一个会刷新文本的网页。选择 Flask HTML 并不是因为 Flask 最擅长处理大数据而是因为它足够轻后端可以只负责查 SQLite、吐 JSON前端用 ECharts 把 JSON 变成可读的趋势图。这个模式适合 Python 入门阶段的课程设计也能给以后转向 Django、FastAPI 的人留下清晰的近似结构。下面会按照一条可复现的路径来讲先解决数据层再搭 Flask 路由最后用 ECharts 做可视化并在最后一章补上长期运行和调试技巧。2. 天气数据获取与清洗把这个参数表抄进毕设里天气数据看似简单但不同的数据源返回的单位、字段命名和更新频率都不一样。如果一开始就盯着页面效果后面做历史趋势分析时会发现数据缺失、主键冲突、单位不一致返工成本很高。这一章先解决数据源选型再给出完整的抓取和入库代码。2.1 数据源选型无 Key 的 Open-Meteo 和它的字段模型毕设项目里最常见的三种天气数据接口是Open-Meteo、和风天气、心知天气。三者的差异集中在是否需要认证、返回字段结构和请求频率限制。数据源是否需要 API Key单次覆盖范围适合场景Open-Meteo不需要全球支持逐小时和逐日温度、风速、降水概率趋势和风天气需要全球中国区数据较全城市实时天气、分时预报心知天气需要国内为主分钟级天气、生活指数如果你做的是城市历史温度趋势、最高最低温对比这类分析Open-Meteo 是最稳妥的选择。它不需要注册申请也不用在代码里维护 token调用时直接拼参数即可。更重要的是它的逐日数据结构非常适合“日期 最高温 最低温 降水概率 风速”这样的统计表和 SQLite 建模完全对得上。和风天气虽然也有逐日预报但免费版限流较严做班级内分享或答辩演示时容易出现半小时后接口被拒的情况。以北京为例使用 Open-Meteo 的关键位于纬度 39.9042、经度 116.4074。下面这一段请求能取到未来 7 天的逐日最高温、最低温、降水概率和最大风速import requests def fetch_daily_forecast(lat: float, lon: float): url https://api.open-meteo.com/v1/forecast params { latitude: lat, longitude: lon, daily: temperature_2m_max,temperature_2m_min,precipitation_probability_max,windspeed_10m_max, forecast_days: 7, timezone: Asia/Shanghai } resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json()这个函数把参数集中在params里方便后面循环多个城市时直接替换坐标。daily字段里的temperature_2m_max和temperature_2m_min是逐日最高温和最低温precipitation_probability_max是当天最大降水概率windspeed_10m_max是当天最大风速。注意一定要加timezoneAsia/Shanghai否则接口默认返回 UTC 时间存入 SQLite 后和中国时间差 8 小时画趋势图时会多出莫名奇妙的日期偏移。2.2 用 requests 抓取每日天气并把字段规范成可入库结构接口返回的 JSON 是数组形式的列daily.time、daily.temperature_2m_max、daily.temperature_2m_min等。画图时前端希望拿到的是“一行一天”的对象数组所以需要在后端做一次转置。这个转置同时也是清洗过程把字段名改成数据库里的列名并过滤掉空值。def parse_weather_batch(city_name: str, payload: dict): daily payload[daily] rows [] for i, day in enumerate(daily[time]): rows.append({ city: city_name, date: day, temp_max: daily[temperature_2m_max][i], temp_min: daily[temperature_2m_min][i], precip_prob: daily[precipitation_probability_max][i], wind_max: daily[windspeed_10m_max][i], }) return rowsenumerate的作用是把time数组的索引同步到其他字段保证同一天的数据不会被错位。Open-Meteo 的温度默认是摄氏度不需要额外换算如果换用其他接口要特别留意temp_max是否是华氏度或扩大了 10 倍后的整数。清洗阶段最好就把单位统一前端才能直接拿数值无需二次计算。2.3 SQLite 建表与按城市去重入库毕设阶段不建议引入 MySQL因为部署和答辩环境不稳定。SQLite 是 Python 自带的单文件存储拷贝即备份适合这种轻量分析系统。建表时要把(city, date)作为联合主键这样同一个城市同一天只保留一份数据。CREATE TABLE IF NOT EXISTS weather ( city TEXT NOT NULL, date TEXT NOT NULL, temp_max REAL, temp_min REAL, precip_prob INTEGER, wind_max REAL, PRIMARY KEY (city, date) );写表结构时把precip_prob定为大于 0 小于 100 的整型前端作为百分比展示。入库使用ON CONFLICT DO UPDATE保证重复抓取时更新而不是插入新行import sqlite3 def upsert_weather(rows, db_pathweather.db): conn sqlite3.connect(db_path) conn.executemany( INSERT INTO weather(city, date, temp_max, temp_min, precip_prob, wind_max) VALUES (:city, :date, :temp_max, :temp_min, :precip_prob, :wind_max) ON CONFLICT(city, date) DO UPDATE SET temp_maxexcluded.temp_max, temp_minexcluded.temp_min, precip_probexcluded.precip_prob, wind_maxexcluded.wind_max , rows ) conn.commit() conn.close()excluded是 SQLite 在 UPSERT 语法里代表“本次尝试插入的新值”的引用方式。这意味着第二次抓取时不会因主键冲突而报错而是把最新预报覆盖进去。这也是天气可视化系统里最实用的一个细节预报数据每天都可能微调历史表必须支持原地更新而不是粗暴地先删后插。提示SQLite 的 UPSERT 语法需要 SQLite 3.24 以上Python 3.8 自带的版本足够不需要额外安装组件。3. Flask 后端路由设计决定前端图表能不能少写一半数据入库后下一步是用 Flask 把数据库中的数据变成浏览器能消费的内容。很多人会在这里把 SQL 直接写在路由里页面一多代码就会很乱。对于这个毕设规模单文件加一个通用查询函数就够了但路由返回的数据格式必须提前定好否则前端会反复改。3.1 单文件 Flask 应用配 sqlite3 查询函数先在app.py里初始化 Flask并写一个公用的数据库查询函数。sqlite3.Row允许按列名取值再把结果转成字典方便之后jsonify。from flask import Flask, render_template, jsonify, request import sqlite3 app Flask(__name__) DATABASE weather.db def query_db(sql, args()): conn sqlite3.connect(DATABASE) conn.row_factory sqlite3.Row cur conn.execute(sql, args) rows [dict(row) for row in cur.fetchall()] conn.close() return rows这里没有使用 Flask-SQLAlchemy因为当前系统只有一张表用 ORM 反而增加概念负担。query_db接受 SQL 语句和参数返回列表嵌套字典后续无论渲染模板还是返回 JSON 都可以直接用。实际使用时要注意连接关闭时机避免长时间运行的 Flask 开发服务器持有过多文件锁。3.2 三个路由页面、最新天气、历史序列后端路由按照前端的使用方式分成三类首页渲染、最新一天数据、多日历史序列。设计好这三类前端图表就有稳定的数据来源。路由方法参数返回/GET无渲染index.html/api/weather/latestGETcity单个城市最新天气 JSON/api/weather/historyGETcity, days该城市最近 N 天的天气列表 JSON首页路由需要先查出有哪些城市有数据再默认选中最近更新的城市app.route(/) def index(): cities query_db(SELECT city, MAX(date) AS recent_date FROM weather GROUP BY city) default_city cities[0][city] if cities else 北京 latest query_db( SELECT * FROM weather WHERE city? ORDER BY date DESC LIMIT 1, (default_city,) ) return render_template(index.html, latestlatest[0] if latest else {})查询语句里MAX(date)配合GROUP BY city能得到每个城市的最新日期这个结果只用来确定默认城市实际渲染用的是第二次查询。原因是第一次查询即使取了全部字段也只会返回某一城市分组里的非聚集列不一定是最新一行的完整数据直接使用容易埋坑。历史序列接口服务于 ECharts 的异步加载app.route(/api/weather/history) def history(): city request.args.get(city, 北京) days min(int(request.args.get(days, 7)), 30) rows query_db( SELECT date, temp_max, temp_min, precip_prob, wind_max FROM weather WHERE city ? ORDER BY date DESC LIMIT ? , (city, days) ) return jsonify(rows)这里用min(int(...), 30)限制最大返回天数防止前端误传一个超大数值把数据库拖垮。ORDER BY date DESC返回降序前端画折线图时再做一次反转这样在接口层保留“最新在前”的习惯前端只负责展示。3.3 Jinja2 模板注入与 tojson 的使用Flask 默认用 Jinja2 渲染 HTML。首页路由已经把latest传入模板在index.html中可以直接用{{ latest[temp_max] }}展示。但这只适合页面初始状态图表数据应该由 JavaScript 通过接口异步获取。一个常见的错误是直接把整个latest对象塞进script标签。字典中有中文字段名和引号很容易破坏 JS 语法所以 Jinja2 提供了tojson过滤器script const initialCity {{ latest.get(city, 北京) | tojson }}; /scripttojson会把 Python 对象转成合法 JSON并自动转义、、等字符。此外还可以把城市名写在 DOM 元素的>div idapp>!doctype html html langzh-cn head meta charsetutf-8 title天气预报数据可视化分析/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script style .chart { width: 90%; height: 360px; margin: 20px auto; } /style /head body div idcity-title classchart/div div idtempChart classchart/div div idprecipChart classchart/div script src{{ url_for(static, filenamejs/main.js) }}/script /body /html之所以优先选 CDN是因为 ECharts 的完整包接近 1MB放在 Flask 默认的static目录里每次刷新都会占用带宽。CDN 版本利用浏览器缓存本地开发也能加快页面加载。如果学校机房无法访问公网再改成下载 JS 文件放进static/js目录但那时建议只引入核心图表模块而不是全量包。4.2 温度折线图配置平滑、降序反转与极值标注温度最适合用双折线图展示最高温和最低温两条线能直观看出温差。前端通过fetch拉取 Flask 接口在main.js中完成数据解析和图表初始化const tempChart echarts.init(document.getElementById(tempChart)); const initialCity document.querySelector(#app).dataset.city; fetch(/api/weather/history?city encodeURIComponent(initialCity) days7) .then(resp resp.json()) .then(rows { rows.reverse(); const dates rows.map(r r.date); const maxTemps rows.map(r r.temp_max); const minTemps rows.map(r r.temp_min); tempChart.setOption({ title: { text: initialCity 近7日气温趋势, left: center }, tooltip: { trigger: axis }, legend: { data: [最高温, 最低温], bottom: 0 }, xAxis: { type: category, data: dates }, yAxis: { type: value, name: ℃ }, series: [ { name: 最高温, type: line, smooth: true, data: maxTemps }, { name: 最低温, type: line, smooth: true, data: minTemps } ] }); });rows.reverse()是这里的关键一步。后端ORDER BY date DESC返回最新日期在前但 ECharts 的类目轴默认从左到右对应数组索引如果不反转折线图会把第 7 天的数据画在最左边趋势方向完全反掉。smooth: true让折线过渡更自然适合温度这种连续指标风速或降水量这种离散数值应该去掉smooth改为bar或普通折线。4.3 异步 fetch 拉取 Flask 接口避免整页刷新降水量柱状图与温度图共用同一份fetch数据但不建议重复请求接口。可以把数据解析结果保存在一个变量中两个图表分别消费let weatherRows []; function loadWeather(city) { return fetch(/api/weather/history?city encodeURIComponent(city) days7) .then(resp resp.json()) .then(rows { weatherRows rows.reverse(); return weatherRows; }); } function renderPrecipChart() { const dates weatherRows.map(r r.date); const probs weatherRows.map(r r.precip_prob); const precipChart echarts.init(document.getElementById(precipChart)); precipChart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: dates }, yAxis: { type: value, name: %, max: 100 }, series: [{ name: 降水概率, type: bar, data: probs, itemStyle: { color: #5470c6 } }] }); }降水概率的yAxis.max固定为 100否则只有一周降水概率在 20% 到 40% 时柱状图会被拉高视觉上产生“很大概率下雨”的误导。如果需要实现城市切换只要在select的onchange中调用loadWeather(newCity)再分别调用两个图表的setOption即可。此时 Flask 路由不需要改因为 URL 参数已经包含city。5. 让系统可长期运行抓取调度、数据补全与调试技巧数据抓取如果手动执行一次就停止几天后打开页面看到的还是旧的预报可视化分析就会失真。最后一章给出三个能直接改进毕设质量的做法。5.1 用 APScheduler 挂周期抓取不阻塞 Flask在app.py启动时挂一个后台调度器每隔 6 小时抓取一次多个城市的数据from apscheduler.schedulers.background import BackgroundScheduler def run_fetch_all(): cities [{name: 北京, lat: 39.9042, lon: 116.4074}] for c in cities: payload fetch_daily_forecast(c[lat], c[lon]) rows parse_weather_batch(c[name], payload) upsert_weather(rows) scheduler BackgroundScheduler() scheduler.add_job(run_fetch_all, interval, hours6) scheduler.start()注意BackgroundScheduler和 Flask 的app.run在同一个进程不会阻塞请求。6 小时间隔已经足够保证一天最鲜活的天气预报也不会触发数据源的限流。5.2 连续日期缺失SQLite 递归 CTE 补全如果某次抓取失败SQLite 里会出现日期空洞。用递归 CTE 生成该城市所有日期后左连接天气表能快速定位缺失日期WITH RECURSIVE all_dates(date) AS ( SELECT MIN(date) FROM weather WHERE city ? UNION ALL SELECT date(date, 1 day) FROM all_dates WHERE date (SELECT MAX(date) FROM weather WHERE city ?) ) SELECT a.date, w.temp_max, w.temp_min FROM all_dates a LEFT JOIN weather w ON w.city ? AND w.date a.date WHERE w.date IS NULL;这个查询对答辩加分它说明你考虑到了接口不稳定时的数据处理策略而不是假设数据永远完整。补全策略可以是缺失的日期用前后两天均值填充也可以在图表中置为null让 ECharts 的connectNulls自动控制断开。5.3 用 Debug 模式验证接口与静态资源分离开发期建议直接flask --app app run --debug--debug会开启自动重载改完app.py或模板后浏览器刷新即可生效同时请求异常时会在页面直接显示堆栈。答辩演示时把debug关掉改用app.run(host0.0.0.0, port5000)这样同一教室的其他机器也能通过 IP 访问页面方便用手机现场展示。注意static目录只放 CSS、JS 和图片天气数据永远从接口读取这个约定能让系统在更换前端模板时不破坏数据链路。本文还有配套的精品资源点击获取