资讯详情

基于Python的天气预报系统:从数据获取到可视化分析全攻略

📅 2026/10/11 14:22:03 | 华诺云谱 👁 阅读
基于Python的天气预报系统:从数据获取到可视化分析全攻略
简介基于Python的天气预报系统设计与数据可视化分析项目面向需要完成课程设计或入门爬虫及桌面应用的Python学习者。资源包含一个可通过Python或Jupyter直接运行的天气查询程序支持选择多个城市、查看15天预报并对获取到的天气数据进行绘图处理和本地保存。压缩包共5个文件含2个Python脚本、1个界面图标、1张运行效果图和1份说明文档整体约754KB结构简洁便于对照查看。目前已有21310人学习下载适合作为综合练习参考读者可拿到可直接运行的主程序与数据获取模块并结合说明文档快速理解爬虫采集、Tkinter界面搭建以及数据可视化输出的实现思路便于在此基础上扩展城市列表、调整展示样式或接入更多天气数据源。整个项目文件结构紧凑适合初学者拆解练习也可直接作为课程设计提交。1. 基于 Python 的天气预报系统从数据获取到可视化分析一次讲透手机上的天气 App 能告诉你明天几度但回答不了「这个冬天是不是比往年更冷」「今年 7 月到底下了多少雨」这类问题。要做这种回答你需要的是历史气象数据、一套清洗逻辑和一张能说服自己的图——这正是「基于 Python 的天气预报系统设计和可视化数据分析」这个方向的真正价值。它不是做一个 App 的壳子而是把 Python 爬虫/API 请求、pandas 数据处理、SQLite 存储和 matplotlib 可视化串成一条完整链路最终交付「能查过去、能看趋势、能预测未来」的轻量系统。适合正在学 pandas 和 matplotlib、想找一个完整练手项目的 Python 学习者也想给简历贴一块「数据分析实战」标签的从业者。这篇文章全是可直接复制运行的代码配合我踩过的坑一起讲。2. 天气数据从哪来公开 API 与爬虫两条路的选择与落地2.1 先决定数据源免费 API 优先爬虫只做补盲做天气系统第一步不是写代码是决定数据从哪来。我一般分两种场景取舍。如果你的目标是「能跑、能分析、不被封」优先走免费天气 API。常见的有和风天气的免费开发者版、OpenWeatherMap 的 free tier还有聚合数据这类第三方接口。它们的共性是注册后拿一个 key用 HTTP GET 请求就能拿到 JSON字段结构稳定省去了解析 HTML 的时间。缺点是免费额度有限历史数据往往只能拉到近几天真正想要「过去三年逐日温度」免费 API 基本给不了这时候就得爬网页。如果你要的是历史数据做趋势分析爬虫是绕不开的。常见来源是天气后报lishi.tianqi.com这类提供历史天气查询的网站或者中国天气网的历史归档页。爬虫的好处是能拿到 2011 年至今的逐日最高/最低温度、天气现象、风力风向坏处是页面结构会改你可能今天写的 selector 下个月就失效。我的个人偏好是先用 API 搭骨架验证整条分析链路能跑通再针对历史缺口写爬虫——这样即使爬虫翻车系统的核心功能也不会瘫。下面这段代码是走 API 路线的获取模块我用 requests 库请求城市天气并解析成本地 DataFrame这是后面所有分析的地基。import requests import pandas as pd from datetime import datetime def fetch_weather_from_api(city_key, api_key): 通过和风天气 API 获取实时天气数据 :param city_key: 城市ID例如 101010100 代表北京 :param api_key: 你的开发者密钥 url https://devapi.qweather.com/v7/weather/now params { location: city_key, key: api_key, unit: metric # 使用摄氏度避免华氏单位换算 } resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() now data[now] df pd.DataFrame([{ city: data[fxLink], # 实际城市名可根据 fxLink 解析 temp: float(now[temp]), feels_like: float(now[feelsLike]), humidity: float(now[humidity]), wind_dir: now[windDir], time: datetime.now().strftime(%Y-%m-%d %H:%M:%S) }]) return df if __name__ __main__: # 北京为例真实使用请替换为自己的 API key df fetch_weather_from_api(101010100, your_qweather_key_here) print(df.head())这段代码几个关键参数说明一下。unitmetric这个参数容易漏漏了默认返回华氏后面你会被 70 度的数据搞晕。timeout10必须写天气接口偶尔会慢不写超时会让程序卡死。raise_for_status()是防御习惯接口返回 401 或 403 时立即报错而不是带着错误数据往下跑。第一次跑通后把 DataFrame 打印出来看一眼字段是否齐全、温度是否存在非数值这一步比任何检查都重要。如果 API 额度用完或者需要历史数据就得写爬虫。我不建议用 selenium太重了requests BeautifulSoup 足够。核心是找到存放历史天气数据的页面结构用select定位表格行逐行解析。下面这个脚本抓的是天气后报的历史天气页面import requests from bs4 import BeautifulSoup def crawl_lishi_tianqi(city_code, year_month): 抓取天气后报的历史天气数据 :param city_code: 6位城市代码例如 101010100 :param year_month: 月份格式 202307 url fhttps://lishi.tianqi.com/{city_code}/{year_month}.html headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) rows soup.select(div.lishi_content ul li) records [] for row in rows: parts row.get_text().split( ) if len(parts) 4: records.append({ date: parts[0], high: int(parts[1].replace(℃, )), low: int(parts[2].replace(℃, )), weather: parts[3], wind: parts[4] if len(parts) 4 else }) return pd.DataFrame(records)这里有个血泪经验解析天气后报的页面时直接从ul li里取文本然后 split 是按空格拆但风力风向可能缺失导致 parts 长度不稳定。我的习惯是先打印一条原始记录看看结构再决定按索引取还是按正则取。另外resp.encoding utf-8要主动设置有些服务器返回的 header 里 charset 是错的不手动指定会得到乱码。2.2 多城市批量拉取用并发还是循环做可视化分析时单城市的数据往往不够看。比如你想对比上海、广州、北京三地夏季气温差异就需要批量请求。最简单的做法是 for 循环但慢requests 库本身不支持异步但可以用 concurrent.futures 的ThreadPoolExecutor并发请求。下面这段是并发拉取多城市实时天气的模板from concurrent.futures import ThreadPoolExecutor, as_completed city_map { 北京: 101010100, 上海: 101020100, 广州: 101280101, 深圳: 101280601 } def fetch_batch(city_map, api_key, max_workers4): 并发拉取多城市天气 max_workers 控制并发数免费 API 建议 4~5 个别开太多 results [] with ThreadPoolExecutor(max_workersmax_workers) as executor: future_to_city { executor.submit(fetch_weather_from_api, city_id, api_key): city_name for city_name, city_id in city_map.items() } for future in as_completed(future_to_city): city_name future_to_city[future] try: df future.result() df[city] city_name results.append(df) except Exception as e: print(f{city_name} 拉取失败: {e}) return pd.concat(results, ignore_indexTrue)max_workers4是经验值。免费 API 对同一 key 有 QPS 限制并发开到 10 容易被限流返回 403。如果发现请求频率高了报错最简单的方式是降低 max_workers或者在每次请求后加time.sleep(0.5)——土办法有时候最稳。拉完后的 DataFrame 会包含城市名、温度、湿度等字段下一步要做的事情是清洗和落库。3. 数据清洗与落库把 JSON 转成一张能分析的表3.1 常见脏数据缺失值、异常值和单位混用不管你从 API 拿还是爬虫拿原始数据都称不上「可分析」。我在实际清洗中遇到最多的是这三类问题第一是缺失值。API 偶尔会返回某个字段为空比如湿度字段在特殊天气下上报为 null爬虫抓历史天气时某些日期的风力风向可能没抓到。第二是异常值比如温度超过 60 度、湿度超过 100这类数据多半是解析错位导致的。第三是单位混用华氏和摄氏混在一起排序时单位不一致后面画图全是错的。处理思路分两种情况。如果你是在做历史趋势分析数据量大dropna()直接删掉缺失行通常影响不大如果每一天都很宝贵比如要做一年的逐日序列就改用fillna(methodffill)用前一天的值填补。异常值的处理就一句话超过物理可能的值就是脏的直接过滤掉。下面这段代码演示了怎么处理温度和湿度字段def clean_weather_data(df: pd.DataFrame) - pd.DataFrame: 清洗天气数据处理缺失值、过滤异常、统一温度范围 df df.copy() # 1. 删除完全空白的行 df df.dropna(howall) # 2. 温度异常过滤-40℃ ~ 50℃ 属于合理气象范围 df df[(df[temp] -40) (df[temp] 50)] # 3. 湿度范围 0~100超出直接删 df df[(df[humidity] 0) (df[humidity] 100)] # 4. 将华氏度转换成摄氏度如果你的源混用了单位 if df[temp].max() 80: df[temp] (df[temp] - 32) * 5 / 9 # 5. 时间字段标准化 df[date] pd.to_datetime(df[date]).dt.strftime(%Y-%m-%d) # 6. 排序确保时间序列顺序正确 df df.sort_values(date).reset_index(dropTrue) return df这里的第 4 步值得展开说。判断依据是最大值是否超过 80因为正常情况下没有哪个城市温度会高于 80 度一旦出现基本可以断定源数据返回的是华氏。这个启发式判断偶尔也有失误比如异常值本身就是 85所以稳妥做法是拉取时直接指定单位API 请求里加unitmetric不要把清洗的活留给后端。3.2 SQLite 存储为什么不用 CSV数据清洗完之后面临选择存 CSV 还是存 SQLiteCSV 的好处是直观、Excel 能打开坏处是当你积累了一两年的数据后CSV 每次全量读取会越来越慢而且多城市数据放在一个 CSV 里结构混乱。SQLite 是 Python 内置支持的关系型数据库单文件、零配置、标准 SQL 查询非常适合这个体量的项目。建表时我会把天气数据分成「城市表」和「天气记录表」两张表。城市表存城市代码和城市名天气记录表存温度和湿度等实测数据。主键用(city_code, date)这样同一城市同一天的数据不会重复插入——这就是后悔药如果哪天发现 API 返回了当天重复数据直接INSERT OR REPLACE不会炸。import sqlite3 def setup_database(db_pathweather.db): 初始化 SQLite 数据库创建城市表和天气记录表 conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS city ( city_code TEXT PRIMARY KEY, city_name TEXT NOT NULL ) ) cursor.execute( CREATE TABLE IF NOT EXISTS weather_daily ( city_code TEXT NOT NULL, date TEXT NOT NULL, high_temp REAL, low_temp REAL, humidity REAL, weather_desc TEXT, wind_dir TEXT, PRIMARY KEY (city_code, date) ) ) conn.commit() conn.close() return db_path def save_weather_to_db(df, db_pathweather.db): 将清洗后的天气数据写入 SQLite df 需要包含 city_code、date、high_temp 等字段 conn sqlite3.connect(db_path) df.to_sql(weather_daily, conn, if_existsappend, indexFalse) conn.close()这里有个细节to_sql中的if_existsappend是追加模式但如果你连续跑了两次主键冲突会直接抛异常。我的做法是在写入前先按(city_code, date)去重或者在建表时用INSERT OR REPLACE。更推荐后者因为它的逻辑是「有则更新无则插入」完全是幂等操作。不过to_sql默认生成INSERT语句想用OR REPLACE就得绕过to_sql改用executemany。DB 文件的字段类型也提醒一下date用 TEXT 存储格式2024-07-15这样支持字符串比较也就是WHERE date 2024-01-01直接能用。很多人会纠结用不用时间戳存我觉得没必要——除非你需要做毫秒级的计算不然 TEXT 的可读性远比效率重要。4. 可视化数据分析用四张图把天气「讲」明白4.1 中文字体与坐标轴密度两个最先遇到的坑数据进了 SQLite接下来进入最有成就感的环节——可视化分析。用 matplotlib 画天气数据的图第一个要处理的不是数据是字体。matplotlib 默认字体不支持中文你画出来的图全是方块。解决办法是设置plt.rcParams指定中文字体Windows 用 SimHeimacOS 用 Arial Unicode MSLinux 服务器得先确认系统装没装中文字体。第二个高频坑是横坐标日期过密。比如画一年的温度曲线365 个日期全摆上去坐标轴就会挤成一坨黑线——这就是热搜里常说的「画图横坐标太密集」。解决思路是设置MaxNLocator控制显示几个刻度再旋转 45 度让标签错开。下面这段代码是一张完整的 7 天温度趋势图包含最高温度和最低温度两条曲线import matplotlib.pyplot as plt import matplotlib.dates as mdates from matplotlib.ticker import MaxNLocator def plot_temp_trend(df, city_name北京): 画一个城市近 N 天的最高/最低温度趋势线 plt.rcParams[font.family] [SimHei] # 微软雅黑也可以 plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块的问题 fig, ax plt.subplots(figsize(12, 5)) # 确保日期列是 datetime 类型这事不能偷懒 df[date] pd.to_datetime(df[date]) ax.plot(df[date], df[high_temp], markero, markersize3, linewidth1.5, label最高温度) ax.plot(df[date], df[low_temp], markers, markersize3, linewidth1.5, label最低温度) ax.set_title(f{city_name} 温度趋势, fontsize16, pad15) ax.set_ylabel(温度 (℃)) # 核心控制横坐标刻度数避免挤成一坨 ax.xaxis.set_major_locator(MaxNLocator(nbins10)) plt.xticks(rotation45, haright) ax.legend() ax.grid(alpha0.3) plt.tight_layout() plt.show()MaxNLocator(nbins10)表示最多显示 10 个刻度标签matplotlib 会根据数据范围自动挑均匀散布的日期。rotation45和haright是配套的前者旋转文字后者对齐避免标签重叠。这个参数组合是我做过多次试验后确认最稳妥的横坐标不管塞 30 天还是 365 天的数据都能正常显示。markersize3是一个审美参数数据点太多时把点缩小一点图面更干净。4.2 降雨量与湿度的可视化时序柱状图和双轴图温度趋势对应折线图是典型的但天气分析远不止温度。降雨量和湿度是农业、户外活动决策的重要参考。画降雨量我建议用柱状图因为它是离散事件——某天下了 10mm某天没下柱状图能直观看出「哪天是降雨日」。而当你需要把降雨量和温度两条序列放在同一张图里对比时就得考虑双 y 轴。双轴图是可视化里一个经典的坑两张图共享 x 轴但 y 轴量纲不同。温度在 -10 到 35 之间降雨量在 0 到 50 之间直接画在同一个 y 轴会导致其中一条被压扁。解决办法是twinx()创建第二个 y 轴。下面这段代码就是把降雨量和温度放在一起对比def plot_precip_and_temp(df): 双 y 轴图柱状图表示降雨量折线图表示温度 plt.rcParams[font.family] [SimHei] fig, ax1 plt.subplots(figsize(12, 5)) ax2 ax1.twinx() # 创建共享 x 轴的第二个 y 轴 # 左轴降雨量柱状图 bars ax1.bar(df[date], df[precipitation], color#4C72B0, alpha0.6, label降雨量 (mm)) ax1.set_ylabel(降雨量 (mm), color#4C72B0) ax1.tick_params(axisy, labelcolor#4C72B0) # 右轴温度折线图 line ax2.plot(df[date], df[high_temp], color#C44E52, linewidth2, label最高温度) ax2.set_ylabel(最高温度 (℃), color#C44E52) ax2.tick_params(axisy, labelcolor#C44E52) # 图例合并处理否则会漏掉一条 h1, l1 ax1.get_legend_handles_labels() h2, l2 ax2.get_legend_handles_labels() ax1.legend(h1 h2, l1 l2, locupper left) ax1.xaxis.set_major_locator(MaxNLocator(nbins8)) plt.xticks(rotation45, haright) plt.title(降雨量与最高温度对比, pad15) plt.tight_layout() plt.show()这段代码里最容易被忽略的是最后图例合并的部分。ax1.plot和图例不会自动包含ax2的元素不合并的话要么温度线的图例消失要么降雨量的图例消失标准翻车现场。用get_legend_handles_labels()把两个轴的 handle 和 label 拿回来拼在一起才能得到一张完整的图。5. 更进一步用线性回归做简单的温度预测可视化只能回答「过去发生了什么」但天气预报系统的核心势必要回答「明天几度」。完整做温度预测需要 LSTM 或 Prophet 这类模型但对这个标题的体量来说线性回归已经能给出一个可以接受的经验性参考值。它的逻辑是明天的温度与今天、昨天的温度有强线性相关用过去 N 天的温度作为特征训练一个普通最小二乘回归预测未来一天的数值。from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error import numpy as np def build_prediction_model(df): 使用过去 7 天的最高温度预测明天的最高温度 df df.sort_values(date).reset_index(dropTrue) temps df[high_temp].values # 构造时序特征X 是最近 7 天的温度y 是第 8 天的温度 X, y [], [] window_size 7 for i in range(window_size, len(temps)): X.append(temps[i - window_size:i]) y.append(temps[i]) X np.array(X) y np.array(y) # 划分训练集和测试集注意时序数据不能乱序 shuffle train_size int(len(X) * 0.8) X_train, X_test X[:train_size], X[train_size:] y_train, y_test y[:train_size], y[train_size:] model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) mae mean_absolute_error(y_test, y_pred) print(f平均绝对误差: {mae:.2f} ℃) # 返回模型和最后 7 天的数据用于预测明天温度 return model, temps[-window_size:] def predict_tomorrow(model, last_window): 输入最近 7 天温度输出明天预测温度 pred model.predict(last_window.reshape(1, -1)) return pred[0]这里的训练测试划分与普通机器学习有本质区别时序数据绝对不能随机 shuffle。如果你用train_test_split(shuffleTrue)测试集会包含比训练集更早的数据等于用未来预测过去误差低得毫无意义。我当时第一次跑就是这么干的MAE 只有 0.3 度高兴了没几分钟就意识到是数据泄漏白高兴一场。正确的做法是train_size按位置切前 80% 训练后 20% 测试保证测试集永远是训练集之后的时段。MAE的值也有参考意义若预测误差在 2 度以内说明这套线性方法在这个城市还够用如果超过 3 度说明温度变化与历史 7 天的线性关系不强就该考虑加特征了常见的做法是把「日期对应的季节」「是否下雨」「湿度」加进特征矩阵这些都可以从数据库里直接 join 出来。6. 避坑与进阶五条踩坑记录和把这个系统变可用的技巧6.1 排查与避坑现象、原因、解决以下五条是我实际开发中反复踩过的坑按「现象 → 原因 → 解决」记录照着排查能省掉一下午的抓瞎时间。坑一JSON 解码报错Expecting value: line 1 column 1现象调用天气 API 时报 JSON 解析失败。原因请求被网关拦截返回的不是 JSON 而是 HTML 错误页或者接口 key 失效返回了纯文本。解决打印resp.status_code和resp.text[:200]看真实返回先确认是不是 200再确认内容是 JSON 结构。不要用爬虫的 response 直接调.json()永远先看文本再解析。坑二时间戳转出来比当地快 8 小时现象API 返回的时间字段转成北京时间后不对差了几个小时。原因API 返回的 unix 时间戳是 UTC 时区的没有指定tz直接datetime.fromtimestamp()用的是本地时间还是 UTC 取决于服务器设置。解决统一用datetime.utcfromtimestamp(ts)然后手动加上timedelta(hours8)或者直接用pandas.to_datetime(ts, units, utcTrue).dt.tz_convert(Asia/Shanghai)。关键是一套代码里只认一个时区别混用。坑三matplotlib 画出来全是方块现象标题、坐标轴中文全部显示成空心方块。原因matplotlib 默认字体是 DejaVu Sans不含中文字符。解决plt.rcParams[font.family] [SimHei]Linux 环境下要先apt install fonts-wqy-microhei装文泉驿字体再指定[WenQuanYi Micro Hei]。注意axes.unicode_minus也要设成False否则负号也会变成方块。坑四横坐标时间标签挤成一坨现象日期刻度全部重叠看不清任何标签。原因x 轴刻度数是 matplotlib 自动生成的数据点多时它会尝试全部显示。解决MaxNLocator(nbins8)限制显示 8 个刻度配合rotation45旋转文本。另外一个隐藏坑是haright没写旋转后的标签中心对齐会把相邻标签撞一起。坑五免费 API 拉两三天接口就 403现象同一台服务器前三天好好的第四天开始请求全部返回 403。原因免费 key 有每日请求次数限制你写了个定时任务每小时拉一次一天把一周的额度用完了。解决拉取后缓存到 SQLite同一城市同一天的数据命中缓存就不重复请求或者拉取频率降到每 6 小时一次写代码时加一层缓存检查逻辑这也是为什么我强调数据分析前要先落库——数据库天然就是缓存层。6.2 进阶把脚本变成真正可用的天气分析系统一个单次运行的脚本距离「系统」还差两步自动化和接入。自动化最简单的方式是 APScheduler 定时任务每天凌晨 6 点拉一次数据、追加进 SQLitefrom apscheduler.schedulers.blocking import BlockingScheduler def scheduled_job(): # 拉取数据、清洗、入库这一流程就是前面所有代码的整合 print(f{datetime.now()} 开始同步天气数据...) raw_df fetch_weather_from_api(101010100, your_key) clean_df clean_weather_data(raw_df) save_weather_to_db(clean_df) scheduler BlockingScheduler() scheduler.add_job(scheduled_job, cron, hour6, minute0) scheduler.start()之后你想看趋势只需要写一个查询脚本从 SQLite 里读近 30 天数据直接调用已经写好的plot_temp_trend画图不需要重新请求网络。这里有一个值得养成的习惯网络请求和数据分析分层。请求层只管拿数据不管画图分析层只管读库和可视化不管网络。分层后 API 挂了不影响已经入库的数据继续做分析整个系统的容错性会好很多。另外一个有价值的进阶是「多城市对比」。把爬虫或 API 的拉取目标从单个城市扩展到多个城市清洗入库后在查询时加一个城市维度的 group by。你会看到很直观的结论同一天里广州湿度 90% 的时候北京可能只有 20%这种横向对比是单城市数据完全展示不了的。这些进阶手段都不需要改核心代码关键在于你一开始设计数据结构时有没有为「城市」和「日期」留字段。我见过太多人把城市名直接写死在文件名里后面想扩展时推倒重来。教训是哪怕你只计划做一个城市也请把city_code存进表里这个习惯能给你留出无数的「后悔药」。希望这篇从数据获取到预测到落库再到可视化的全流程拆解能帮到你照着跑通一遍再慢慢调参。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑