首页 / 资讯中心 / 文章详情

基于Python的天气预报系统:数据采集、清洗与可视化

基于Python的天气预报系统:数据采集、清洗与可视化 ★ FEATURED ARTICLE
简介基于Python语言的天气预报系统设计与可视化分析资料包定位清晰主要面向初学网络爬虫、图形界面编程和数据可视化的学生开发者也适合作为课程设计或毕业设计的参考项目。系统以Tkinter搭建交互窗口利用爬虫获取多座城市未来15天的天气数据支持数据绘图处理、多城市切换和结果保存代码在Python或Jupyter两种运行环境中均能直接执行。压缩包共有5个文件包含两个核心Python脚本主程序与数据抓取模块、一个图标文件、一张示例效果图和一份说明文档整体体积仅754KB结构清晰便于对照学习。截至目前已有21310人学习下载反映出该资源在同类项目中的实用性与认可度。借助此项目读者能够完整走通从城市选择、天气数据抓取、数据解析到图表绘制与本地持久化保存的整个流程对理解爬虫与数据可视化的协同应用很有帮助。1. 基于 Python 的天气预报系统不是查天气是让数据自己说话如果你以为“天气预报系统”就是调一个 API 把温度湿度打印出来那这个资源会推翻你的预期。拿到这套基于 Python 的天气数据采集与可视化分析项目时我第一反应是“又一个爬虫练手”真正跑完才发现它的重心根本不在“预报”而在“分析”——从多源天气接口的数据抓取、异常值清洗、SQLite 存储到温度趋势、降水分布、风速玫瑰图等一系列可视化产出完整走完了一条数据工程链路。对正在学数据分析、或想找一个能写进简历的 Python 综合项目的从业者来说它的价值不是能报明天的天气而是让你看清一份原始 JSON 是如何一步步变成决策图表的。这篇笔记会把数据流拆开讲透你照着搭一遍踩坑点我已经替你趟过了。2. 数据从哪来天气接口选型与请求层的工程化设计2.1 为什么不能只用免费接口还要做一层数据缓存做天气数据分析第一个绕不开的问题就是数据源。市面上的免费天气接口不少但大多数有单位时间内的请求次数限制。如果程序每次做可视化都要实时请求接口跑一次分析就要消耗几十次配额稍不注意就会被限流返回一串看不懂的错误码。这个资源的处理方式很务实在请求层和存储层之间加了一层本地缓存——请求到的数据先落 SQLite再对数据库做分析而不是直接对接口响应做分析。这样设计的好处很明显一是分析过程不依赖网络跑多少次都不消耗配额二是历史数据可以累积为后续的“同比往年温度”这类分析留下素材三是接口挂了也不影响分析流程拿缓存数据照样出图。我实际读源码时发现它把每一次请求的完整 JSON 原样存进了一个raw_data字段这层设计被很多人忽略但对数据溯源很重要。原始数据保留意味着后续如果发现某种可视化结果异常还能回头从原始 JSON 里排查而不是只能对着处理后的表干瞪眼。# 核心请求模块的核心逻辑简化 import requests import sqlite3 import json import time def fetch_weather(city, api_key, db_path): url https://api.openweathermap.org/data/2.5/weather params { q: city, appid: api_key, units: metric, lang: zh_cn } # 1. 先查本地缓存避免每次都打接口 conn sqlite3.connect(db_path) cached conn.execute( SELECT raw_data, fetch_time FROM weather_cache WHERE city? ORDER BY fetch_time DESC LIMIT 1, (city,) ).fetchone() # 2. 如果缓存存在且不超过30分钟直接返回历史数据 if cached: cache_time cached[1] if time.time() - cache_time 1800: return json.loads(cached[0]) # 3. 否则请求接口并把结果存入SQLite resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() conn.execute( INSERT INTO weather_cache (city, raw_data, fetch_time) VALUES (?,?,?), (city, json.dumps(data, ensure_asciiFalse), time.time()) ) conn.commit() return data这段代码看着简单但unitsmetric和langzh_cn这两个参数很容易被漏掉。前者决定温度单位是摄氏度还是华氏度后者决定天气描述是中文还是英文。如果不显式声明接口默认返回华氏度和英文描述后面的可视化图表会莫名其妙地出现“77°F”这种单位排查半天才发现是请求参数的锅。另外timeout10也是经验值天气接口偶尔会响应很慢不加超时时间的话程序在网络抖动时会一直挂在那整个分析流程都被拖死。2.2 多城市批量拉取控制频率与失败重试的取舍单城市请求写好后批量拉取又是另一层问题。直接在 for 循环里逐个requests.get是最粗暴的做法但也是最容易触发限流的做法。这个资源里采用了一个很实用的策略每请求一个城市后强制time.sleep(1)并对失败请求做最多 3 次重试重试间隔按 1 秒、2 秒、4 秒递增。这种退避策略在爬虫场景里很常见但很多新手写的时候只重试不等待结果越重试越容易被封。这里有个细节值得注意——重试次数达到上限后它不是报错退出而是把该城市的记录标记为“拉取失败”继续跑后续城市。这个设计很聪明数据分析场景下偶尔缺失一两个城市的数据不会影响整体分析结论没必要让一次失败中断整个任务。def fetch_cities_batch(city_list, api_key, db_path): results {} for city in city_list: for attempt in range(3): # 最多重试3次 try: data fetch_weather(city, api_key, db_path) results[city] data print(f[OK] {city} 温度{data[main][temp]}°C) break # 成功就跳出重试循环 except requests.exceptions.RequestException as e: wait_time 2 ** attempt # 1秒, 2秒, 4秒 print(f[RETRY] {city} 第{attempt1}次失败等待{wait_time}秒) time.sleep(wait_time) else: # for循环正常结束没break说明3次都失败了 print(f[FAIL] {city} 请求失败标记后继续) results[city] None return results这里的for...else...结构很多人不熟它的逻辑是for 循环里如果触发了breakelse块不会执行如果循环正常跑完没 breakelse块才执行。在这个场景里else天然成了“重试全部失败”的分支不用额外维护一个success标志位。这个写法确实能减少状态变量的使用但在团队协作时要注意加注释说明因为不熟悉这个语法的人第一眼会以为else对应的是if。3. 数据落地与清洗SQLite 表结构设计和“脏数据”的三种形态3.1 表结构不是随便建的字段设计决定了后续分析的上限天气数据的原始 JSON 嵌套层级很深直接把整个 JSON 塞进数据库虽然省事但后续做 SQL 查询和分析会很痛苦。这个资源在存储层做了一个折中一方面保留raw_data原样存档另一方面把常用的字段拆出来建成扁平表。这个思路很像数仓里的 ODS 层和 DWD 层的区别——原始层不动明细层做结构化。在实际代码里它建的表字段包括城市名、温度、体感温度、最低温、最高温、气压、湿度、风速、风向角度、天气描述、数据时间戳。值得注意的是它把风向角度单独存了一个字段而不是只存“东北风”这种文字描述。这个设计对后续画风玫瑰图至关重要因为风向图需要的是角度数值文字描述还得再映射回角度范围等于绕了一圈。# 建表语句关键部分 CREATE TABLE IF NOT EXISTS weather_daily ( id INTEGER PRIMARY KEY AUTOINCREMENT, city TEXT NOT NULL, temp REAL NOT NULL, -- 当前温度(°C) feels_like REAL, -- 体感温度 temp_min REAL, -- 当日最低温 temp_max REAL, -- 当日最高温 pressure INTEGER, -- 气压(hPa) humidity INTEGER, -- 湿度(%) wind_speed REAL, -- 风速(m/s) wind_deg INTEGER, -- 风向角度(0-360) description TEXT, -- 天气描述 fetch_time INTEGER, -- Unix时间戳 UNIQUE(city, fetch_time) -- 防重复插入 );UNIQUE(city, fetch_time)这个约束很多人建表时会忽略但它是防止重复数据的关键。如果同一分钟内对同一个城市请求了两次或者脚本被重复执行唯一约束会让第二次插入报错而不是在表里留下两条一模一样的数据。配合INSERT OR IGNORE使用效果更好。另外temp字段用 REAL 而不是 INTEGER是因为接口返回的温度可能是 23.76°C 这样的浮点数用整型会丢失精度后续算平均温度时误差会累积。3.2 清洗的三个坑单位、缺失值和类型陷阱数据清洗是这个项目里最容易翻车的地方绝对不是简单地“去掉空值”就完了。第一坑是单位问题前面说过的华氏度只是入门级气压、风速也可能有不同单位体系必须确认接口文档里写的是 hPa 还是 kPa。第二坑是缺失值接口不是每次都会返回所有字段比如某些地区的wind_deg可能是 null这时候硬转 float 会直接抛异常。第三坑是类型陷阱JSON 里数字在 Python 里解析后可能是 int 也可能是 float写入 SQLite 时如果字段类型声明的是 INTEGERfloat 会被静默取整这种“静默错误”最难排查。import pandas as pd import numpy as np def clean_weather_data(df): # 1. 统一温度单位如果数据源返回的是华氏度转成摄氏度 if df[temp].max() 60: # 摄氏度不可能超过60华氏度很可能 df[temp] (df[temp] - 32) * 5 / 9 # 2. 缺失值处理风速和风向缺失时用默认值填充 df[wind_speed] df[wind_speed].fillna(0) df[wind_deg] df[wind_deg].fillna(0) # 3. 类型强制转换气压转整型防止浮点残差 df[pressure] df[pressure].astype(int) # 4. 日期解析Unix时间戳转可读日期 df[fetch_date] pd.to_datetime(df[fetch_time], units) # 5. 异常值过滤气温超过[-50, 50]范围的数据直接丢弃 df df[(df[temp] -50) (df[temp] 50)] return df这里判断“数据是否为华氏度”用的是max() 60这个阈值是一种很实用的经验法则。全球有人居住的地区气温极少超过 50°C而华氏度 60°F 约等于 15.6°C是极其常见的温度值。用这条规则能快速识别单位是否正常不用每次都仔细查文档。但要注意这个法则只适用于室外气温如果是水温或体温数据就不适用了。异常值过滤里我一般会额外加一个风向有效范围检查wind_deg合法范围是 0-360如果接口返回了负数或者大于 360 的值说明接口端就有问题数据不能直接用。4. 可视化分析落地用 Matplotlib 和 Seaborn 让气象数据“开口说话”4.1 温度时间序列图一眼识别昼夜温差和异常波动这个项目里最有分析价值的可视化产物之一是连续 7 天的温度变化曲线。代码里用了 Matplotlib 绘制折线图X 轴是时间Y 轴是温度但关键的细节在于它对数据做了“平滑处理”——不是直接把原始采样点连成线而是计算了移动平均。为什么要这么做因为天气接口的数据是某个时刻的快照半小时内的两次请求可能因为云量变化导致温度差 3-4°C直接画折线图会出现很多锯齿掩盖了真实的温度趋势。移动平均相当于把高频噪声滤掉留下温度变化的低频趋势。import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei, Noto Sans CJK SC] # 解决中文乱码 def plot_temperature_trend(df, city): fig, ax plt.subplots(figsize(12, 5)) # 原始数据画浅色细线 ax.plot(df[fetch_date], df[temp], color#cccccc, linewidth1, label原始数据) # 移动平均画深色粗线窗口3小时 df[temp_smooth] df[temp].rolling(window3, centerTrue).mean() ax.plot(df[fetch_date], df[temp_smooth], color#d62728, linewidth2, label3h移动平均) ax.set_title(f{city} 温度变化趋势) ax.set_ylabel(温度 (°C)) ax.legend() plt.xticks(rotation45) plt.tight_layout() return figrolling(window3, centerTrue)这个参数里的center很关键。默认centerFalse时窗口的均值是右对齐的画出来的曲线会比真实趋势滞后一段时间设成centerTrue后窗口中心点对齐当前时间点曲线不会偏移。很多初学者画平滑曲线时觉得“形状对不上”十有八九是这里没设对。另外中文乱码问题是 Matplotlib 的经典坑Linux 服务器上经常找不到中文字体必须在代码里先声明font.sans-serif否则图表上会显示一排方框直接废掉。4.2 多城市对比分析箱线图揭示地域气候差异单城市的时间序列图只能看到“自己”的变化多城市对比才是真正能体现数据分析价值的部分。这个资源里有一个多城市温度箱线图的功能把不同城市在同一时间段内的温度分布画成并列的箱线图一眼就能看出哪里昼夜温差大、哪里温度稳定。箱线图相比折线图的好处是它展示了数据的分布形态——中位数、四分位距、离群点一目了然而不仅仅是平均值。比如某城市看起来平均温度和海滨城市差不多但箱体特别宽说明它昼夜温差极大体感可能完全不是一回事。def plot_multi_city_boxplot(df, cities): data_for_plot [] city_names [] for city in cities: city_data df[df[city] city][temp].dropna() data_for_plot.append(city_data.values) city_names.append(city) fig, ax plt.subplots(figsize(10, 6)) bp ax.boxplot(data_for_plot, labelscity_names, patch_artistTrue) # 按中位数大小着色直观呈现冷暖差异 for patch, city in zip(bp[boxes], cities): median_val df[df[city] city][temp].median() patch.set_facecolor(#ff7f0e if median_val 20 else #1f77b4) ax.set_ylabel(温度 (°C)) ax.set_title(多城市温度分布对比) plt.tight_layout() return fig这里的着色逻辑用了一个很直观的方式中位数低于 20°C 的用暖色高于的用冷色。但在实际项目里不建议硬编码这个阈值更好的做法是用“相对所有城市中位数的平均值”来动态决定用暖色还是冷色。我之前试过用固定阈值结果夏天分析时所有城市都是暖色冬天全是冷色图表失去对比意义。改成动态阈值后每个季节都能体现出城市间的冷暖相对差异。4.3 导出分析报告自动生成带结论的 HTML 而不是只给图片这个项目一个让我眼前一亮的设计是在最后增加了一个自动报告生成模块——把所有图表嵌入到一个 HTML 页面里并且在每个图表下方自动写入一段简单的结论文本比如“该城市本周最高温出现在 14 时最低温出现在 05 时”。相比于把图片一张张发给别人看这种单页报告的形式更适合做周报、月报也更容易看出完整的分析链条。实现方式也很朴素用 HTML 模板字符串拼接图片的 base64 编码不依赖额外库纯 Python 标准库就能完成。import base64 from io import BytesIO def fig_to_base64(fig): 把Matplotlib图形对象转成base64字符串嵌入HTML buf BytesIO() fig.savefig(buf, formatpng, dpi150, bbox_inchestight) buf.seek(0) return base64.b64encode(buf.read()).decode(utf-8) def generate_report(all_figs, conclusions, output_path): html_parts [] for i, (fig, conclusion) in enumerate(zip(all_figs, conclusions)): img_b64 fig_to_base64(fig) html_parts.append(f div stylemargin-bottom:30px img srcdata:image/png;base64,{img_b64} stylewidth:100%;max-width:900px p stylecolor:#444;font-size:14pxstrong结论{i1}:/strong {conclusion}/p /div ) html fhtmlheadmeta charsetutf-8title天气分析报告/title/headbody{chr(10).join(html_parts)}/body/html with open(output_path, w, encodingutf-8) as f: f.write(html)bbox_inchestight这个参数很多人在做图表嵌入时会漏掉它的作用是把图片周围多余的空白裁剪掉否则生成在 HTML 里的图四周会有大片白边整体观感差很多。dpi150是清晰度和文件体积的平衡点100 太低在 Retina 屏幕上会发糊300 太高 HTML 文件会膨胀到几十 MB。以及一条经验生成 HTML 报告时的encodingutf-8必须显式声明否则 Windows 环境下默认编码可能是 GBK中文会乱码。5. 避坑与常见问题我实测踩过的六个坎5.1 接口数据时区问题时间戳差八个小时现象拉取的数据画图后发现温度曲线整体向右平移了 8 个小时最高温出现在晚上而不是下午。原因接口返回的 Unix 时间戳是 UTC 时间而本地分析时用了pd.to_datetime(..., units)pandas 默认会把它当成 UTC 时间转换。如果不指定时区展示出来的时间永远是国际标准时间和本地时间差 8 小时。解决在转换时显式指定时区或者直接加 8 小时偏移。代码改成pd.to_datetime(df[fetch_time], units, utcTrue).dt.tz_convert(Asia/Shanghai)。这个方法在项目里数据采集如果用的是北京时间那么分析端就别再做一次时区转换否则会重复偏移。5.2 SQLite 并发写入锁死现象程序跑着跑着突然报database is locked而且不是偶发每次批量写入时都会出现。原因SQLite 同一时间只允许一个进程写库。如果脚本里有多个线程或者多个连接同时执行INSERT后到的连接会等锁超过默认超时时间就报错。这个项目里如果自己加了多线程采集很容易触发。解决最简单的方案是把所有写操作收敛到同一个连接并且用conn.execute(PRAGMA busy_timeout 5000)加长等待时间。或者用with语句保证每次写入后立即 commit 并关闭连接。如果并发要求更高就得换成 PostgreSQL 了但这个小项目没这个必要。5.3 Matplotlib 中文方框问题在 Linux 服务器上特别严重现象本机 Windows 跑代码一切正常部署到 Linux 服务器后图表里的中文全变成小方框。原因Windows 自带 SimHei 字体但大多数 Linux 发行版默认没有中文字体。Matplotlib 找不到字体时就回退到 DejaVu Sans不支持中文。解决服务器上执行fc-list :langzh先查看有哪些中文字体如果没输出就装一个字体包。Debian 系是sudo apt install fonts-noto-cjk然后在代码里显式指定matplotlib.rcParams[font.sans-serif] [Noto Sans CJK SC]。这个坑线上环境大概率会遇到本机复现没问题但一部署就废吃过的亏印象深。5.4 风向角度可视化用错坐标系现象风玫瑰图画出来后方向是反的北风显示在了南边。原因气象学里风向角度是从正北开始顺时针计算的比如 90° 是东风180° 是南风。但 Matplotlib 的极坐标系默认从右侧东方开始逆时针计算角度如果不做转换画出来的图就是镜像的。解决在polar坐标系里把风向角度做转换radians np.deg2rad(90 - wind_deg)这样才符合气象学的显示习惯。这个坑我身边好几个同事都踩过属于“不说话你绝对想不到”的问题。5.5 滑动平均窗口导致数据前段缺失现象温度曲线画出来后前半个小时或者最后几个小时没有平滑线只有原始细线。原因rolling(window3, centerTrue)在处理边界数据时前两个点和后两个点因为窗口不完整计算结果为 NaN。这种情况是预期行为但首次遇到的人会以为是 bug。解决如果不介意边界数据保持现状即可如果非要补齐可以加参数min_periods1用窗口内已有的数据计算均值代价是边界处平滑效果稍弱。我一般会保留 NaN 不做处理因为图表展示时缺口的视觉影响不大。6. 进阶技巧用回归分析给“体感温度”建个本地修正模型如果你已经把前面所有流程跑通了这个进阶方向值得尝试以接口返回的体感温度feels_like为基准用回归模型拟合“温度 湿度 风速 → 体感温度”的关系。为什么要做这件事因为气象接口的体感温度是一个基于通用模型的估算值但每个人的体感差异很大——南方人和北方人对湿度的耐受完全不同。基于本地历史数据拟合出一个个性化修正模型再叠加当前实况做预测就能得到一个“属于你的”体感温度。一种很直接的建模方式是用线性回归。体感温度受三个因素影响气温、湿度、风速。气温越高体感越热湿度高时体感温度更高因为汗液蒸发困难风速大时体感温度更低风寒效应。可以按这个逻辑构造回归模型用历史数据训练后检查各系数的显著性和符号是否符合直觉——这是一个非常好的模型验证习惯。from sklearn.linear_model import LinearRegression import pandas as pd def fit_comfort_model(df): # 特征温度、湿度、风速目标体感温度 X df[[temp, humidity, wind_speed]].dropna() y df.loc[X.index, feels_like] model LinearRegression() model.fit(X, y) # 打印系数验证方向是否符合预期 coef_dict dict(zip([温度, 湿度, 风速], model.coef_)) print(模型系数, coef_dict) print(截距, round(model.intercept_, 3)) # 预期温度系数 0, 湿度系数 0, 风速系数 0(风寒效应) return model跑完这个模型常见的期望结果是温度系数约 1.0气温升 1°C 体感升 1°C湿度系数约 0.1-0.3湿度每升 10% 体感温度升 1-3°C风速系数为负风速每增加 1m/s 体感温度降 1°C 左右。如果你的数据集里风速系数是正的先检查数据清洗环节是不是风向和风速弄反了如果湿度系数接近 0可能是数据样本量太小或者采集时间段太集中没有覆盖干湿差异大的天气。用这个模型后续只要拿到实时气象数据就能算出比接口更贴合本地的体感温度值你可以试着把这个修正值作为一个新的派生字段加回数据库里再做一轮可视化对比会比源接口的feels_like更有分析说服力。这套资源的完整价值在于它不是一个“能跑就行”的 demo而是一套能拿数据反复迭代的分析框架。从那以后我每次接手类似的数据类项目都强制自己先走一遍“数据源评估 → 缓存策略设计 → 清洗规则定义 → 可视化验证”这条链路不再急着写业务代码。希望这篇拆解笔记对你有用下载后照着跑一遍再结合自己的城市数据改改参数你会有不少收获。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站