
简介这份资源是一份以Python为核心的天气数据可视化平台毕业设计论文面向计算机、软件工程、大数据等专业需要完成毕业设计或课程设计的学生也适合想系统了解爬虫、数据处理与Web可视化全流程的开发者。全文围绕天气数据获取、清洗、存储与展示展开涉及Python网络爬虫、Pandas数据处理、MySQL数据库、Flask后端框架以及HTML、CSS、JavaScript、Echarts、WordCloud等前端可视化技术。压缩包内共1个docx文件包体约2.45MB为完整论文文档包含摘要、目录、开发工具与关键技术介绍、系统设计等章节可直接作为选题参考与写作模板。目前已有318人学习下载读者可借此梳理从数据采集到图表呈现的实现思路理解前后端协作方式并获取毕业设计报告的结构安排、技术选型说明与可视化方案参考适合作为毕设开题、论文撰写和项目复现的辅助资料。1. 天气数据可视化平台真正的工程量不在画图那一步很多人接到「基于 Python 的天气数据可视化平台」这个题目第一反应是打开 matplotlib 画一条气温折线截图贴进文档就算交付。真按这条路走第二天就会卡住接口限流、某几个小时缺测、要按城市切换、对方还想看历史同期对比。画图本身二十行代码就够剩下的全是数据工程。这个平台实际要兜住三件事数据能持续稳定地进来进来之后能按城市、时间、指标切片查询切片结果能在一个页面里被非技术的人一眼看懂。它适合两类人——写课程设计或论文、需要一条完整可复现链路的同学以及想把内部气象看板从 Excel 里搬出来的数据分析和运维岗。下面按我自己会用的顺序拆先定数据契约和采集再做图然后把脚本变成服务最后收在排错和验收上。每一段都给能直接抄的命令、参数和代码。2. 用 Python 把天气数据拿回来从接口到 DataFrame 的完整链路2.1 先定数据契约再折腾 python 环境安装大多数人失败在顺序上先装环境、先画图字段名边写边改等到要入库时发现气温一会儿叫temp一会儿叫temperature_2m时间戳一会儿是字符串一会儿是带时区的对象。正确顺序是先写一张字段表让它同时充当数据库表结构、DataFrame 列名和前端字段名。字段类型示例说明city_codestr101010100城市唯一标识别拿中文城市名当主键obs_timedatetime2024-06-01 08:00:00统一按 UTC 存展示时再转本地时区temp_cfloat23.4气温摄氏度humidityint62相对湿度百分比precip_mmfloat0.0小时降水量wind_speed_kmhfloat11.2风速wind_dir_degint180风向角度0 为正北sourcestropen-meteo来源标记多源时用来去重fetched_atdatetime2024-06-01 08:12:03采集时刻排查延迟靠它环境这边不用纠结。Python 3.10 以上建一个虚拟环境装三个包就够了python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install requests pandas pyecharts fastapi uvicorn在 vscode python 环境配置或 pycharm 配置 python 环境时最容易踩的坑是没有把解释器切到.venv里的那个结果命令行pip install装上了编辑器里import requests依然飘红。判断方法很简单在编辑器内置终端里敲which pythonWindows 用where python路径里带.venv才算对。2.2 请求写法、超时参数与退避重试采集端我只写一个函数把「请求」这件事的所有不确定性都关在里面。以 Open-Meteo 的 forecast 端点为例换成别家气象服务商时把 URL 和参数名映射一下就行import time import requests API https://api.open-meteo.com/v1/forecast PARAMS { latitude: 39.9042, longitude: 116.4074, hourly: temperature_2m,relative_humidity_2m,precipitation, wind_speed_10m,wind_direction_10m,surface_pressure, timezone: Asia/Shanghai, past_days: 3, # 回补过去 3 天防止某次任务漏跑 forecast_days: 4, } def fetch(params, retries3, backoff1.6): sess requests.Session() for i in range(retries): try: # timeout 是一个二元组(连接超时, 读取超时) resp sess.get(API, paramsparams, timeout(3.05, 10)) resp.raise_for_status() return resp.json() except (requests.Timeout, requests.HTTPError): if i retries - 1: raise time.sleep(backoff ** i) # 1s、1.6s、2.56s几个参数值得逐个说清。past_days3是刻意的冗余定时任务挂了半天只要下一次跑起来就能自动补齐不需要人工补数。timeout(3.05, 10)里的 3.05 秒不是随手写的连接阶段留出比整数稍多一点的时间能避开某些网络栈在整秒边界上的抖动读取给到 10 秒是因为逐小时数据一次返回上千行慢一点正常。backoff ** i是指数退避限流场景下比固定间隔重试友好得多。最后一定要raise_for_status()否则 429、500 会被当成正常响应解析报错点会漂到很远的地方。2.3 把 JSON 落成 DataFrame顺手做掉 python 类型转换拿到 JSON 之后不要直接pd.DataFrame(payload[hourly])那样列名和契约对不上后面每处都要改。加一层映射把「接口字段」和「平台字段」隔开import pandas as pd FIELD_MAP { time: obs_time, temperature_2m: temp_c, relative_humidity_2m: humidity, precipitation: precip_mm, wind_speed_10m: wind_speed_kmh, wind_direction_10m: wind_dir_deg, surface_pressure: pressure_hpa, } def to_frame(payload, city_code): hourly payload[hourly] df pd.DataFrame({dst: hourly[src] for src, dst in FIELD_MAP.items()}) df.insert(0, city_code, city_code) df[obs_time] pd.to_datetime(df[obs_time]) for col in [temp_c, humidity, precip_mm, wind_speed_kmh, wind_dir_deg, pressure_hpa]: df[col] pd.to_numeric(df[col], errorscoerce) # 缺测统一成 NaN df[fetched_at] pd.Timestamp.utcnow().tz_localize(None) return (df.dropna(subset[obs_time]) .drop_duplicates([city_code, obs_time]))errorscoerce是关键参数接口偶尔会把缺测值写成-或空串不加这个参数整列会退化成 object 类型画图时直接抛TypeError。drop_duplicates按「城市 观测时间」去重正好配合前面的past_days回补——重复拉回来的数据不会在库里堆成两份。落盘我一般先写 Parquet 做冷备再 UPSERT 进库中间格式换 CSV 也行但 CSV 会丢掉时区信息需要自己再解析一次。3. 基于 Python 的天气数据可视化从 pandas 画图到 echarts 数据可视化大屏3.1 matplotlib 和 ECharts 的分工边界做数据可视化最容易纠结选型我的判断标准只有一条这张图最终是给谁看的。场景推荐方案理由论文配图、离线报告matplotlib / seaborn出图稳定可直接导出 300dpi PNG/PDF探索阶段看数据分布pandas.plot()一行代码改起来不心疼平台内嵌的交互看板pyecharts 或 ECharts 原生有缩放、悬浮提示、图例联动大屏轮播、多图拼版pyecharts Grid/Page一张 HTML 承载全部图表论文里的静态配图和平台里的动态看板不是一套东西。常见的失误是拿 matplotlib 硬做网页图为了交互性去接 Flask 的图片流最后每个筛选动作都要重启一次渲染体验很差。凡是需要用户点选、缩放的一律走 ECharts 这条线Python 只负责把数据整理成 JSON。3.2 用 pyecharts 生成可直接嵌入页面的气温曲线pyecharts 的价值在于省掉手写 ECharts 配置对象的功夫。下面这个函数生成逐小时气温曲线重点看xaxis_opts和yaxis_optsfrom pyecharts import options as opts from pyecharts.charts import Line def temp_line(df, city_name): d df.sort_values(obs_time) return ( Line() .add_xaxis(d[obs_time].dt.strftime(%m-%d %H:%M).tolist()) .add_yaxis( 气温(℃), d[temp_c].round(1).tolist(), is_smoothTrue, is_symbol_showFalse, # 点太密时隐藏数据点标记 label_optsopts.LabelOpts(is_showFalse), ) .set_global_opts( title_optsopts.TitleOpts(titlef{city_name} 逐小时气温), xaxis_optsopts.AxisOpts( axislabel_optsopts.LabelOpts(rotate45, intervalauto), ), yaxis_optsopts.AxisOpts( name℃, min_dataMin, max_dataMax, # 让曲线撑满画布 ), datazoom_opts[opts.DataZoomOpts(type_inside)], tooltip_optsopts.TooltipOpts(triggeraxis), ) )is_symbol_showFalse是七天以上数据必开的开关一小时一个点、一周 168 个点全画圆点就是一团糊。min_dataMin和max_dataMax解决另一个高频问题ECharts 默认从 0 起算 y 轴25℃ 到 30℃ 的日变化会被压成一条直线看起来像没波动。triggeraxis让悬浮提示按整列显示比单点提示好读得多。生成的 HTML 可以直接render(temp.html)落到静态目录前端用 iframe 嵌进平台页面即可。3.3 视觉编码约定不同指标别用同一种图形温度用带渐变的折线或热力图色阶降水用柱状风向用极坐标或者玫瑰图湿度用面积图——这不是审美问题是识别效率问题。同一个平台上如果气温和湿度都画成折线用户扫一眼根本分不清哪条是哪条。还有一个隐蔽的误用把风向的wind_dir_deg当成普通数值做折线0° 和 359° 会画出一条从顶到底的竖线看着像数据异常其实是角度值不该用线性轴。风向一律转成(sin, cos)分量或者直接用极坐标别偷懒。4. 平台化把脚本变成能长期运行的天气数据可视化平台4.1 采集、存储、接口、前端四层怎么切单个脚本能跑通之后别急着加功能先分层。采集层只负责调接口、清洗、返回 DataFrame存储层只负责写入和查询对外暴露upsert_hourly(df)和query_series(...)两个函数接口层做参数校验和结果聚合前端只吃 JSON。分层的好处是任何一层出问题都能单独替换——接口限流了改采集层图表要换库改前端互不牵连。4.2 建表 SQL 与必须加的索引CREATE TABLE IF NOT EXISTS weather_hourly ( city_code TEXT NOT NULL, obs_time TIMESTAMP NOT NULL, temp_c REAL, humidity INTEGER, precip_mm REAL, wind_speed_kmh REAL, wind_dir_deg INTEGER, source TEXT, fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (city_code, obs_time) ); CREATE INDEX IF NOT EXISTS idx_weather_city_time ON weather_hourly (city_code, obs_time DESC); INSERT INTO weather_hourly (city_code, obs_time, temp_c, humidity, precip_mm, wind_speed_kmh, wind_dir_deg, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (city_code, obs_time) DO UPDATE SET temp_c excluded.temp_c, humidity excluded.humidity, precip_mm excluded.precip_mm, wind_speed_kmh excluded.wind_speed_kmh, wind_dir_deg excluded.wind_dir_deg, fetched_at CURRENT_TIMESTAMP;主键设成(city_code, obs_time)而不是自增 id是为了让ON CONFLICT的 upsert 生效——预报数据会被反复回补重复插入会让查询结果翻倍。索引(city_code, obs_time DESC)的顺序不能反平台最常执行的查询是「某城市最近 N 天」这个复合索引能直接命中。相反如果只建obs_time单列索引多城市查询仍要回表过滤。几个采集参数的建议取值范围参数默认值建议说明采集周期无30~60 分钟再密也不会比数据源更新更快回补天数02~3 天覆盖单次任务失败的时间窗批量写入行数1500~2000逐行 commit 在 SQLite 上慢十倍以上历史保留永久原始数据保留聚合表按需滚动逐小时数据一年也就几十万行4.3 定时采集python 多进程不是第一选择采集是典型的 IO 密集型任务瓶颈在网络往返不在 CPU。所以多城市并发用ThreadPoolExecutor或协程就够了上手就上multiprocessing只会带来额外的序列化开销和写库冲突。真正需要多进程的是后续的批量聚合计算比如对十年历史数据做日统计。from concurrent.futures import ThreadPoolExecutor, as_completed from apscheduler.schedulers.blocking import BlockingScheduler CITIES [(101010100, 39.9042, 116.4074), (101020100, 31.2304, 121.4737)] def collect_one(item): code, lat, lon item payload fetch({**PARAMS, latitude: lat, longitude: lon}) return to_frame(payload, code) def collect_all(): with ThreadPoolExecutor(max_workers4) as pool: futures {pool.submit(collect_one, c): c[0] for c in CITIES} for fut in as_completed(futures): code futures[fut] try: df fut.result() upsert_hourly(df) except Exception as exc: # 单城市失败不影响整体 print(f[warn] {code} 采集失败: {exc}) sched BlockingScheduler(timezoneAsia/Shanghai) sched.add_job(collect_all, interval, minutes30, max_instances1) sched.start()max_instances1必须显式写上默认值是 1 但很多人会改大结果上一轮还没跑完下一轮就开始SQLite 上直接触发database is locked。max_workers4对应大部分公开接口的并发友好区间开到 20 除了被限流没有任何收益。4.4 接口层把聚合逻辑放在服务端前端不需要原始逐小时数组它要的是「某个城市最近三天、每小时一个点」。这个聚合放在接口层做能显著减小传输体积from fastapi import FastAPI, Query, HTTPException app FastAPI() app.get(/api/series) def series( city: str, metric: str Query(temp_c, pattern^(temp_c|humidity|precip_mm)$), days: int Query(3, ge1, le30), ): rows query_series(city, metric, days) if not rows: raise HTTPException(status_code404, detail该城市暂无数据) return {city: city, metric: metric, points: [{t: r[0], v: r[1]} for r in rows]}pattern参数是白名单校验直接挡住了把metric拼进 SQL 的注入路径——指标名不可能来自用户自由输入只可能是这三者之一。ge1, le30限制查询窗口防止有人传days99999把服务拖死。同一城市的同一指标在 30 分钟内结果不变加一层 60 秒的进程内缓存就能挡掉绝大部分重复请求。5. 天气数据可视化平台的进阶技巧与排错清单5.1 python 画图横坐标太密集的三种处理横轴标签重叠是最高频的反馈。按生效顺序排第一优先用 ECharts 的intervalauto让渲染库自己抽稀再配合rotate45如果数据点是 168 个以上一周逐小时直接上DataZoomOpts让用户自己缩放最后才是降采样把逐小时数据用df.resample(6h).mean()聚成 6 小时一个点。别一上来就降采样那会丢掉短时强降水的峰值图上看着平滑实际上把最有价值的信号抹掉了。5.2 时区、空值、重复三个必然遇到的数据坑时区问题几乎必现接口返回本地时间数据库按 UTC 存前端再转一次中间只要有一处漏了图上就会整体偏移 8 小时看起来像预报不准。约定死一条规则——入库前一律pd.to_datetime(...).dt.tz_localize(Asia/Shanghai).dt.tz_convert(UTC)展示层统一加回本地时区。空值不要用 0 填充。降水缺测填 0 会被读成「确实没下雨」气温缺测填 0 会在夏天画出一个深谷。要么用interpolate(methodtime, limit3)只补短缺口要么让曲线断开ECharts 里的connectNullsFalse就是干这个的。重复记录用df.duplicated(subset[city_code, obs_time]).sum()先数一遍非零说明 upsert 没生效别急着在查询里DISTINCT掩盖过去——那是把问题从数据层推到展示层。5.3 上线前跑一遍质量断言def assert_quality(df, expect_hours24): assert df[obs_time].is_monotonic_increasing, 时间未按升序排列 assert df[city_code].nunique() 1, 城市字段为空 assert df[temp_c].between(-60, 60).all(), 气温超出物理合理区间 assert df[humidity].between(0, 100).all(), 湿度不在 0-100 之间 assert df[wind_dir_deg].between(0, 360).all(), 风向角度越界 gaps df[obs_time].diff().dt.total_seconds().max() assert gaps 3600 * 2, f存在超过 2 小时的断档{gaps}s这套断言比任何图表检查都有效因为它拦在数据层。湿度 105、风向 400、时间戳没排序——这些都是渲染成图之后靠肉眼很难发现的错误。把它挂进采集任务的末尾或者接进 CI每次改完清洗逻辑先跑一遍绝大多数「图看起来怪怪的」的问题在出图之前就已经被拦下来了。本文还有配套的精品资源点击获取