简介这是一份基于Python的天气预报数据可视化分析系统完整项目适合数据分析方向的学生、Python开发者以及需要展示气象数据的项目团队使用。系统覆盖实时天气查询、天气分析、历史数据查询三大模块后端采用Flask框架提供Web服务利用爬虫从气象源抓取实时数据配合NumPy、Pandas完成统计分析与趋势预测前端使用PyEcharts生成柱状图、折线图、饼图等交互式图表。资源包共231个文件约39.2MB主要包含18个Python源码文件、59个JS脚本、12个CSS样式、17个Pug模板、7个SQL数据库脚本以及HTML页面和项目文档目录结构完整便于定位后端逻辑、前端界面与数据库设计。目前已有177人学习下载。获取后可获得一套可运行的完整系统代码既能直接部署演示也可参考其技术栈用于课程设计、毕业设计或数据分析实战练习。1. 一个基于 Python 天气预报数据可视化分析系统到底在解决什么问题如果你已经看腻了天气 App 当天的曲线想回答「过去三个月哪座城市温差最大」「今年夏天比去年热多少」这类问题那基于 Python 天气预报数据可视化分析系统就是一条能落地的路径。它做的事情并不复杂用 Python 爬虫定时抓取天气数据把实况和预报落进 SQLite用 pandas 清洗聚合最后用 Flask ECharts 把统计结果展示成折线图、柱状图和地图热力图。这类系统最适合课程设计、个人数据分析练手和团队内部小工具不追求毫秒级实时但要能把数据存下来、能重复分析。很多人做出来只能用来演示真正拉开差距的是数据源选型、字段设计和参数校准这也是后面几章要讲的重点。2. 天气数据源选型与采集入库先解决数据从哪来做这种系统最先卡住的往往不是代码而是「数据到底从哪来」。天气数据不像电商商品可以随便抓很多接口需要 Key网页爬虫又容易反爬。我曾经见过有人花了三天写爬虫结果第四天页面改版解析逻辑全部作废。所以这一章先把数据源讲清楚再给一套可以直接复制的采集入库流程。2.1 数据源怎么选免费 API、网页爬虫与公开数据集常见的可选方案有三类免费天气 API、网页爬虫、离线公开数据集。我一般建议优先用 API而不是爬网页。原因有两个一是 API 返回的是结构化 JSON字段名、时间格式、数值单位都已经对齐省去大量解析工作二是 API 的请求频率和限流策略相对明确按文档控制即可不需要跟反爬机制纠缠。方案优点主要坑适合场景免费天气 API高德、和风等返回结构化 JSON字段稳定城市覆盖广需要申请 Key部分接口有每日配额长期增量采集网页爬虫中国天气网等免 Key数据量大页面结构会变解析易碎有反爬短期练手、临时补数离线公开数据集拿到就能用格式稳定时效差覆盖城市固定算法演示、离线分析选型时还要考虑一个实际问题你的系统是要跑一个月还是跑一年。如果只是想交一份课设爬虫也能用如果想让系统持续运行、每天自动更新API 的稳定性远比「免费」重要。拿我自己常用的高德天气接口来说虽然要 Key 和配额但返回的forecasts里直接带未来几天的逐日预报字段足够干净省去不少清洗工作。给城市配 adcode 行政区划编码采集逻辑可以完全复用。2.2 用 requests 拉取城市天气并落库 SQLite数据源定了之后第一步就是把请求和入库跑通。下面这段代码是采集部分的最小闭环请求天气接口、解析返回 JSON、把每日预报写入 SQLite。import requests import sqlite3 # adcode 是行政区划编码可以在开放平台的城市列表里查到 CITY_CODE { 北京: 110000, 上海: 310000, 广州: 440100, 深圳: 440300, } def fetch_weather(adcode): url https://restapi.amap.com/v3/weather/weatherInfo params { key: 你的高德key, city: adcode, extensions: all, # all 返回未来几天预报base 只返回实况 } resp requests.get(url, paramsparams, timeout5) resp.raise_for_status() return resp.json() def save_daily(data): conn sqlite3.connect(weather.db) cur conn.cursor() cur.execute( CREATE TABLE IF NOT EXISTS daily_weather ( city TEXT, date TEXT, day_temp INTEGER, night_temp INTEGER, day_weather TEXT, day_wind TEXT, PRIMARY KEY (city, date) ) ) for forecast in data.get(forecasts, []): city forecast[city] for cast in forecast.get(casts, []): # INSERT OR REPLACE同一城市同一天重复请求不会产生脏数据 cur.execute( INSERT OR REPLACE INTO daily_weather VALUES (?, ?, ?, ?, ?, ?), (city, cast[date], int(cast[daytemp]), int(cast[nighttemp]), cast[dayweather], cast[daywind]) ) conn.commit() conn.close() # 先手动跑一次确认数据落库 data fetch_weather(CITY_CODE[北京]) save_daily(data)代码里有两个参数值得关注extensionsall和timeout5。extensionsall决定返回的是预报数据而不是只有当前实况这决定了后面能不能做长期趋势分析timeout5是防止网络异常时程序一直挂在那里等待本身也是一种资源浪费。resp.raise_for_status()会在返回 4xx/5xx 时直接抛异常避免把错误页面当成 JSON 解析。落库用 SQLite 而不是 CSV是因为后续清洗和查询更稳而且单文件可以随身拷贝。PRIMARY KEY (city, date)在表结构层面就去重INSERT OR REPLACE则相当于给重复采集留了后悔药同一城市同一天重复跑不会产生两行数据。注意forecasts是一个列表即使只查一个城市也要遍历因为 API 返回的结构是按城市封装的后续扩展多城市时这段逻辑不用改。2.3 字段设计与采集调度参数采集字段不要只存温度。day_weather、day_wind、date这些字段看似占空间但在后续聚合分析里非常有用。比如你想统计「这个月下了几天雨」「哪种风向最容易降温」没有原始天气现象和风力字段一切无从谈起。日期字段建议统一存成YYYY-MM-DD不做任何格式化后面用 pandas 解析最省事。真正长期运行时手动跑脚本没有意义。我一般用 APScheduler 做定时采集调度参数直接写在代码里方便放到服务器上跑。from apscheduler.schedulers.blocking import BlockingScheduler import datetime def collect_job(): for city, adcode in CITY_CODE.items(): data fetch_weather(adcode) save_daily(data) print(f{datetime.datetime.now().isoformat()} {city} updated) # 每天 6:00 和 18:00 各跑一次预报内容会随接口更新 scheduler BlockingScheduler(timezoneAsia/Shanghai) scheduler.add_job(collect_job, cron, hour6,18, minute0, idweather_collect) scheduler.start()hour6,18表示每天两个时间点执行。为什么不是每小时一次因为免费天气 API 的更新粒度通常是小时甚至更粗频繁请求只会增加限流风险。时区参数timezoneAsia/Shanghai一定要写如果服务器默认是 UTC定时任务会在北京时间的下午两点跑日期会错乱。采集过程中还要加异常捕获。一个城市接口超时不应该让整个任务中断可以在collect_job里包一层try-except把失败城市写进日志。很多人的系统跑了一个月才发现某城市数据缺了一周原因就是一次网络抖动把任务打断了。3. 把原始天气报文变成可分析的指标清洗与聚合数据落库只是第一步。天气预报接口返回的报文很原始可能带着单位、空格也可能存在缺失和异常值。直接把这些数据画到图上会出现「某天温度 99℃」这种一眼假的数据。所以需要用 pandas 做清洗和聚合把原始表变成可分析的指标表。这也是 Python 数据分析与可视化项目里最核心的一步。3.1 用 pandas 清洗气温 / 风 / 降水怎么对齐清洗的目标很明确让每一行都是一个城市、一个日期、一组可信的数值。先从 SQLite 读表再用 pandas 做类型转换和异常过滤。import pandas as pd import sqlite3 def load_weather(): conn sqlite3.connect(weather.db) df pd.read_sql(SELECT * FROM daily_weather, conn) conn.close() # 部分接口返回的字符串可能带“℃”或空格先统一转成数字 df[day_temp] df[day_temp].astype(str).str.replace(℃, ).str.strip() df[day_temp] pd.to_numeric(df[day_temp], errorscoerce) df[night_temp] pd.to_numeric(df[night_temp], errorscoerce) # 日期统一成 datetime 类型 df[date] pd.to_datetime(df[date], errorscoerce) # 明显异常值气温超过 50 或低于 -60 属于数据污染 df df[(df[day_temp] -60) (df[day_temp] 50)] df df.dropna(subset[city, date, day_temp]) # 同一城市同一天可能有多条记录保留最后一条 df df.drop_duplicates(subset[city, date], keeplast) return dferrorscoerce的作用是把无法转换的值变成NaN而不是让程序中断。这样你就能看到哪些行有脏数据而不是被一个异常字符卡死。紧接着的dropna负责删掉这些缺失行。异常值过滤那行用的是「合理范围」思路如果数据源里混了一个数字 99它不会被 SQLite 挡住但会被这个范围挡掉。城市名称也需要对齐比如接口里可能返回「北京市」而库里写的是「北京」可以在清洗时统一用df[city] df[city].str.replace(市, )确保后面按城市分组时不拆成两个组。3.2 聚合出日 / 月 / 年维度的统计表清洗后的daily_weather是明细表直接画图会很抖因为单日波动太大。可视化分析系统里更有价值的是月度和年度趋势。用groupby加上agg可以一次性算出均值、极值和温差。def with_derived_columns(df): 把日期拆成年月并计算单日温差。 df[year] df[date].dt.year df[month] df[date].dt.month df[temp_gap] df[day_temp] - df[night_temp] return df def monthly_summary(df): 按月聚合最高温、最低温、温差、记录条数。 df with_derived_columns(df) result (df.groupby([city, year, month]) .agg( avg_high(day_temp, mean), avg_low(night_temp, mean), max_temp(day_temp, max), min_temp(night_temp, min), avg_gap(temp_gap, mean), count(date, count) ) .reset_index()) result[avg_high] result[avg_high].round(1) result[avg_low] result[avg_low].round(1) return result这里的temp_gap是分析系统里非常有用的指标。日均温差大说明这个城市昼夜变化剧烈对穿衣、露营、光伏设备选型都有参考价值。count字段看起来不起眼但它能告诉你这个月的聚合结果是否可信如果某城市某月只有 2 条记录那这个月的均值就没有统计意义前端可以据此隐藏或标记该点。groupby后面的.agg写法是 pandas 里最值得记的语法每一行用新列名(旧列名, 统计方法)的形式定义聚合结果。一次分组可以同时算出多个指标避免写多个groupby再合并。3.3 分析结果导出成 JSON 接口聚合完成之后下一步是给前端提供数据。前端图表库不认识 DataFrame最通用的格式是「labels 数组 数值数组」。直接调用 Flask 接口时建议把 DataFrame 转成这种扁平结构而不是把整个 DataFrame 丢给to_json()。def build_series(monthly_df, city): 把某城市的月度聚合结果转成 ECharts 友好结构。 sub monthly_df[monthly_df[city] city].sort_values([year, month]) labels sub[year].astype(str) - sub[month].astype(str).str.zfill(2) return { city: city, labels: labels.tolist(), avgHigh: sub[avg_high].tolist(), avgLow: sub[avg_low].tolist(), tempGap: sub[avg_gap].tolist(), maxTemp: sub[max_temp].tolist(), minTemp: sub[min_temp].tolist(), }str.zfill(2)把月份补成两位数这样2025-03和2025-10在排序和坐标轴展示上不会错位。JSON 里用avgHigh这样的驼峰命名是为了贴近前端 JavaScript 的习惯但如果你更习惯 Python 的avg_high也完全可以关键是要让前后端字段名保持完全一致。很多图表空白的问题最后查下来就是后端叫avg_high前端读avgHigh字段名对不上。这套清洗聚合流程跑完后数据已经从「能看懂的表格」变成了「能直接喂给图表的接口」。到这一步后端就绪就差把图画出来。4. 用 Flask ECharts 把分析结果变成交互图表后端把数据接口准备好之后可视化部分是整个系统最直观的产出。常见做法是用 Flask 提供 JSON 接口前端用 ECharts 数据可视化库来渲染图表。ECharts 是纯前端方案画折线图、柱状图、地图热力图都不需要后端额外渲染切换主题和交互也很方便。如果你只想做一个本地演示工具这个组合是成本最低的。4.1 Flask 提供数据接口前端用 ECharts 渲染折线 / 柱状 / 地图先写一个最简 Flask 接口把 3.3 节的结果直接暴露出来。from flask import Flask, jsonify from clean import load_weather, monthly_summary, build_series app Flask(__name__) app.route(/api/weather/monthly/city) def monthly(city): df load_weather() monthly_df monthly_summary(df) data build_series(monthly_df, city) return jsonify(data) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)city是 Flask 的动态路由参数访问/api/weather/monthly/北京就能拿到北京的数据。host0.0.0.0允许同一局域网内其他设备访问方便用手机调试页面debugTrue只在开发时开生产环境记得关掉。前端页面放在templates/index.html里通过fetch拉取接口数据再用 ECharts 画温度曲线。!DOCTYPE html html langzh-CN head meta charsetutf-8 / script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idchart stylewidth:900px;height:500px;/div script fetch(/api/weather/monthly/北京) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(chart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [平均最高温, 平均最低温] }, xAxis: { type: category, data: data.labels }, yAxis: { type: value, name: 温度(℃) }, series: [ { name: 平均最高温, type: line, data: data.avgHigh, smooth: true }, { name: 平均最低温, type: line, data: data.avgLow, smooth: true } ] }); }); /script /body /html这段代码里tooltip.triggeraxis让鼠标悬停在横轴上同时显示两条线smooth: true让折线更平滑去掉锯齿感。series里的data直接对应后端avgHigh和avgLow数组。ECharts 的setOption可以反复调用后续切换城市时不需要重新创建图表只要改series数据即可。需要特别提醒的一点如果前端页面和后端是同一个 Flask 服务同源请求不需要处理跨域但如果你把前端单独放在 Vite 或 Live Server 里调试端口不同就会触发 CORS。最简单的解决方法是让 Flask 直接渲染模板页面不做前后端分离少踩一个坑。4.2 关键配置项时间轴、温度区间、城市下钻折线图只是起点。想让它真正可用还要配置交互和展示参数。最常用的三个配置是dataZoom、axisPointer和visualMap。dataZoom: [ { type: inside, start: 0, end: 100 }, { type: slider, height: 20, bottom: 10 } ]dataZoom是时间轴缩放组件。type: inside支持鼠标滚轮缩放type: slider在底部显示一个可拖动的条。当数据量达到两年以上时这个配置几乎是必须的不然 24 个月的数据挤在一张图里完全没法看。温度区间也有讲究如果 yAxis 不写min和maxECharts 会自动按数据范围切结果可能把 20℃ 到 25℃ 的差异放大得很夸张。做天气分析时建议手动设置一个合理的温度区间比如min: -10, max: 40让不同城市之间的图表可以横向对比。城市下钻用「点击城市切换数据」比地图更轻量。ECharts 的地图热力图需要额外引入中国地图 GeoJSON文件较大如果只分析几个城市用柱状图加事件监听更实用。chart.on(click, params { const city params.name; fetch(/api/weather/monthly/${city}) .then(res res.json()) .then(data { chart.setOption({ xAxis: { data: data.labels }, series: [{ data: data.avgHigh }, { data: data.avgLow }] }); }); });通过chart.on(click)监听柱状图点击事件拿到城市名再请求对应接口这是「城市下钻」最简单的实现方式。如果后续要铺到全国再考虑geovisualMap的地图模式但当前阶段不必急着上地图。4.3 环境依赖和项目目录整理项目跑起来之后目录结构会直接影响维护成本。我一般把采集、清洗、接口、前端分开避免一个文件堆上千行。weather_project/ ├── collector.py # 采集调度 ├── clean.py # 清洗聚合 ├── app.py # Flask 接口 ├── templates/ │ └── index.html # 图表页面 ├── static/ │ └── echarts.min.js # 本地化 ECharts避免内网加载 CDN └── weather.db # SQLite 数据库依赖安装一行命令搞定pip install requests pandas flask apscheduler如果你用 PyCharm 或 VS Code建议先建虚拟环境再把解释器指到项目目录下的venv不要用全局 Python。天气项目依赖会随版本升级变化全局环境很容易出现版本冲突。把pip freeze requirements.txt跑一次换机器部署也方便。5. 天气预报可视化避坑指南5 个高频翻车点这类系统代码量不大但坑不少。下面 5 个问题几乎每个做天气可视化的人都可能遇到按「现象 → 原因 → 解决」写清楚照着排查能省不少时间。5.1 接口返回「城市不存在」但城市名明明是对的现象请求里传了「北京」接口返回status失败提示城市不存在或者参数错误。原因很多天气 API 不直接接受城市中文名而是要求行政区划 adcode。北京作为直辖市城市编码和省份编码不同传「北京」两个字未必能被识别传「110000」才是标准做法。解决把城市名和 adcode 的映射单独放到一个字典或 JSON 文件里接口请求一律用编码展示时才用中文名。采集脚本启动时可以先打印一次城市码对照表确认每个城市都能查到数据再批量跑。5.2 爬虫被限流页面结构一变就全挂现象用网页爬虫跑了两天第三天开始请求超时或者解析结果为空过了一个月再看原来的选择器已经匹配不到任何节点。原因网页端反爬策略会监控请求频率无节制地抓取会被限流另一方面网页改版会导致 HTML 结构变化任何基于节点路径的解析都会失效。解决优先使用官方 API不要跟网页结构硬刚。如果必须爬网页把解析函数和入库函数拆开页面改版时只改 parser。同时加上重试和请求间隔。import time def fetch_weather_with_retry(adcode, retries3): for i in range(retries): try: return fetch_weather(adcode) except Exception: time.sleep(2 * (i 1)) raise RuntimeError(ffetch {adcode} failed)5.3 时区与日期对齐问题现象每天 0 点跑采集任务数据库里 1 号的日期总是比预期少一天或者「今天」存进去变成了「昨天」。原因服务器默认时区是 UTCdatetime.now()取到的时间比北京时间慢 8 小时部分 API 返回的date字段用的是当地日期而脚本里却用服务器日期去对齐自然错位。解决所有时间相关逻辑显式指定时区包括调度器参数和日志时间。入库时以接口返回的date字段为准不要用datetime.now()去猜「今天」。from datetime import datetime, timezone, timedelta BEIJING timezone(timedelta(hours8)) now datetime.now(BEIJING).isoformat()5.4 SQLite 并发写入导致锁死现象采集脚本在跑Flask 页面同时打开偶尔报sqlite3.OperationalError: database is locked。原因SQLite 是单写多读的数据库采集脚本长事务持锁时页面的读请求会被阻塞。解决让采集和页面读操作使用不同的连接并且给每次连接设置timeout。更彻底的做法是采集入库走独立进程页面只读再用PRAGMA journal_modeWAL;开启 WAL 模式读写并发会好很多。conn sqlite3.connect(weather.db, timeout10) conn.execute(PRAGMA journal_modeWAL;)5.5 ECharts 数据格式不匹配图表空白现象接口返回数据正常但页面一片空白控制台报data is null或者数组为空。原因前端字段名和后端 JSON 字段名不一致比如后端avg_high前端读avgHigh或者labels为空数组ECharts 没有坐标轴数据可画。解决先在浏览器直接访问/api/weather/monthly/北京确认返回 JSON 结构再对照前端data.xxx的字段名。前端加一行兜底判断。fetch(/api/weather/monthly/北京) .then(res res.json()) .then(data { if (!data || !data.labels || !data.labels.length) return; chart.setOption({ /* ... */ }); });6. 让结果可信历史回测与页面自查的实用技巧系统做到能画图只完成了一半。天气数据可视化分析系统最容易被人质疑的一点是你画的温度到底准不准所以最后一章讲两个让我受益最多的验证方法。6.1 用历史预报做回测验证数据可信度很多接口存的是「预报」而不是「实况」。如果你的采集表里没有区分预报和实况画出来的历史曲线可能只是「过去某天对未来某天的预测」而不是真实天气。一个简单做法是在表里加一个data_type字段forecast表示预报actual表示实况。如果拿不到实况至少把历史预报按提前天数做误差统计。def backtest(result): result[predict_error] (result[avg_high] - result[actual_high]).abs() return result.groupby(lead_days)[predict_error].mean()lead_days是提前预报的天数。提前 1 天预报通常比提前 7 天更准这个回测能直观反映数据源的可靠性。如果误差过大说明可视化分析建立在不可信的数据上再漂亮的图也只是数字陈列。6.2 页面自查的三个习惯每张图表上线前我先问自己三个问题它能回答什么决策单位有没有显示空数据有没有提示温差图回答穿衣降水天数图回答出行最高温趋势图回答空调采购图表不是摆设。其次温度图一定要在 y 轴和 tooltip 里带上「℃」否则数值很容易被误读。最后接口没有数据时不要留给前端一张白图直接显示「暂无数据」提示。做这类系统最大的教训是可视化不能脱离数据源单独炫技。我现在每版上线前都会跑一遍回测脚本把预报误差画在图表下方时刻提醒自己和使用者这份数据有多可信。希望帮到你。本文还有配套的精品资源点击获取