
简介这是一份基于Python的天气预测分析系统源码面向希望学习爬虫、数据预处理和机器学习建模的开发者覆盖从获取城市历史天气到预测绘图的全流程。压缩包共十四个文件内含四个Python脚本、两个CSV数据集、五张PNG图表、两份Markdown说明和一份实验报告整体大小约2.21MB模块划分清晰。目前已有194人学习下载适合作为数据分析、时间序列预测项目的入门参考。源码展示了天气数据的采集存储、清洗与异常值处理、探索性数据分析以及单线性回归和逻辑回归模型的构建评估可视化部分可生成最高温度趋势图、热力图和风力分布图便于直观观察历史变化与预测结果。附带的实验报告与说明文件有助于理解设计思路和运行方式也可在此基础上扩展为Web服务。1. 天气预测分析系统本地跑通“拉数据、建模型、出图表”的完整闭环网上以 Python 天气预测分析系统为关键词的项目源码不少但多数只把爬虫、数据分析、matplotlib 画图各写一段真正能把“获取城市历史天气 → 数据清洗 → 预测建模 → 绘制图表”串成一个可重跑、能换城市、结果可解释的完整系统的其实很少。这个方向的实用价值在于它不用依赖任何付费天气接口只需公开网页数据就能积累多年历史样本预测目标虽然是“明天的最高温”这类小任务但数据管道、特征构造、模型评估、可视化输出四个环节恰好覆盖了 Python 数据分析与可视化的全部核心动作。适合三类人正在做课程设计和期末项目的学生、想练业务数据 pipeline 的开发新人、以及想低成本验证“历史数据能不能预测本地天气”的爱好者。接下来按真实工程落地的顺序从环境搭建一路写到图表输出每一步都有可以直接复用的代码和参数。2. 先把环境搭好Python 3.9 pandas / scikit-learn / matplotlib 的最小组合天气预测分析系统跑起来之前环境问题往往是第一个翻车点。这个项目要处理的依赖不算多但版本组合如果不统一后面 import 阶段就开始报错。我把这套系统反复装过几遍最省事的组合是 Python 3.9 或 3.10搭配 pandas 2.x、scikit-learn 1.3 以上版本、matplotlib 3.7 以上版本、requests 2.31。2.1 为什么用 venv 而不是直接全装进全局环境很多新手习惯先把 Anaconda 全家桶装上再把库一个个 pip install 到全局环境。这样做不是不行但在这个项目上容易遇到两个烦心事一是 pandas 与 scikit-learn 的版本相互不兼容比如旧版 scikit-learn 读取新版 pandas 的 DataFrame 时特征矩阵的列类型推断会出警告甚至报错二是多个课程项目共用环境今天装这个库、明天升那个库依赖冲突后很难回滚。我的做法是给这个天气系统单独建一个虚拟环境。Python 3.3 之后内置的 venv 足够用不需要额外安装 virtualenv。cd ~/weather_system python -m venv .venv source .venv/bin/activate # Windows 下运行 .venv\Scripts\activate python -m pip install --upgrade pip pip install requests pandas matplotlib scikit-learn这里按顺序拆一下python -m venv .venv会在当前目录生成一个隐藏的虚拟环境目录里面是独立的 python 可执行文件和 pipsource .venv/bin/activate让后续命令都走这个环境里的解释器pip install一次装齐五个库如果网络状况不理想可以在后面追加-i https://pypi.tuna.tsinghua.edu.cn/simple切换国内镜像源这一步能省不少等待时间。2.2 依赖装好之后源码目录按职责拆三块环境就绪后建议把源码目录搭成固定结构。这个系统虽然简单但按功能拆目录能避免后续“改一个功能找半天文件”的窘境。weather_system/ ├── .venv/ ├── config.py # 城市、起始年份、请求间隔等配置 ├── fetch_data.py # 抓取历史天气数据输出 CSV ├── analysis.py # 数据清洗、特征工程、预测建模 ├── visualize.py # 图表绘制输出 PNG ├── data/ # 存放城市原始 CSV └── output/ # 存放图表和评估结果config.py 里主要放三个变量城市拼音、抓取起始年份、请求间隔秒数。城市拼音直接决定 URL 拼接是否正确所以我会在 config 里写成字典方便一次配置多城市批量抓取。# config.py CITY beijing # 城市拼音需与目标网站 URL 保持一致 START_YEAR 2019 # 起始年份可抓取到当前月 END_YEAR 2024 # 结束年份 REQUEST_INTERVAL 1.2 # 每请求一次网页的休眠秒数建议不低于 1 秒 OUTPUT_DIR output DATA_DIR data到这里环境已经不再需要关心了。后面所有操作都在这份干净环境里进行更换机器时只需重新执行一遍第 2.1 节的命令再把pip freeze requirements.txt导出的依赖清单带上即可。3. 获取城市历史天气请求构造与 CSV 落盘历史天气数据是这个系统的基础原料。免费 API 通常只提供当天实况和未来几天预报想要过去三年、五年的逐日数据身在免费物料的情况下只能自己抓网页。常见做法是请求天气历史页面页面里包含每日最高温、最低温、天气现象、风向、风力等表格字段拿到响应后用正则或解析库提取逐条存入 CSV。3.1 免费 API 和网页爬取谁才是历史数据的主力先厘清一个概念天气 API 大体分两类一类是实时/预报接口比如和风天气免费版、高德开放平台天气接口它们返回的是 JSON解析方便但历史天数通常只保留最近几天另一类是历史天气数据接口大都属于收费商业产品。对个人学习项目来说直接抓取历史网页数据反而是最靠谱、可复现的方式。抓网页也不是见到什么都抓。天气历史页一般按月组织URL 里带上城市拼音和年月就能定位到对应月份例如常见路径格式为weather/{city}/{year}{month}.html。页面体积小三个月的表格加起来也就几十 KB抓取成本非常低。我一般选择从 2019 年或 2020 年抓到当前月既能保证样本量超过 500 条又不至于因为早期页面结构差异导致解析脚本频繁报错。3.2 抓取脚本URL 拼装、超时重试与间断恢复数据抓取脚本的核心是构造带年月的 URL、请求页面、解析表格、追加写文件。下面这段代码按“最小可用”标准实现重点在于异常处理和断点续传。import time import csv import re import requests from config import CITY, START_YEAR, END_YEAR, REQUEST_INTERVAL, DATA_DIR HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_month(year, month): 构造 URL抓取某年某月的历史天气页面 url fhttps://xxx.example/weather/{CITY}/{year}{month:02d}.html for attempt in range(3): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: return resp.text except requests.RequestException: time.sleep(3 * (attempt 1)) # 首次失败等 3 秒二次等 6 秒 return None def parse_table(html): 从页面 HTML 中提取天气记录行返回 list[tuple] rows re.findall(rtr(.*?)/tr, html, re.S) records [] for row in rows: cells re.findall(rtd[^]*(.*?)/td, row, re.S) if len(cells) 4: date re.sub(r[^], , cells[0]).strip() high re.sub(r[^], , cells[1]).strip() low re.sub(r[^], , cells[2]).strip() weather re.sub(r[^], , cells[3]).strip() records.append((date, high, low, weather)) return records csv_path f{DATA_DIR}/{CITY}_history.csv with open(csv_path, a, encodingutf-8, newline) as f: writer csv.writer(f) if f.tell() 0: # 空文件才写表头 writer.writerow([date, high, low, weather]) for year in range(START_YEAR, END_YEAR 1): for month in range(1, 13): html fetch_month(year, month) if html is None: print(f跳过 {year} 年 {month} 月请求失败) continue for rec in parse_table(html): writer.writerow(rec) print(f完成 {year}-{month:02d}) time.sleep(REQUEST_INTERVAL)这段代码里有几个参数值得留意。timeout10是请求超时上限超过 10 秒直接抛异常避免单月页面卡死整个循环。外层 for 循环里做了三次重试间隔按 3 秒、6 秒递增应付偶发网络波动够用。time.sleep(REQUEST_INTERVAL)是请求间隔设置 1.2 秒以上天气站不是高并发目标太快容易触发限流。解析部分用的是re.findall按tr和td切单元格再统一用re.sub剥掉标签。这样写虽然朴素但比引一个 bs4 依赖更轻页面结构不变时不会出问题。3.3 数据落盘列名统一、字符编码与增量追加刚才的代码在打开文件时用了模式a追加写newline是为了避免空行。有一个细节要注意f.tell() 0判断文件是否为空这个条件在文件不存在时也成立所以第一轮创建文件时会自动写上表头。如果文件已经存在且有数据光标不在 0 位置表头就不会重复写入。with open(csv_path, a, encodingutf-8, newline) as f: writer csv.writer(f) if f.tell() 0: writer.writerow([date, high, low, weather])CSV 的编码建议统一用utf-8。Windows 上如果你后面要用 Excel 打开看数据Excel 对无 BOM 的 UTF-8 文件会按 GBK 猜导致中文天气现象显示乱码。你可以用encodingutf-8-sig写入这样 Excel 能正确识别而 pandas 读取时也用encodingutf-8-sig两边都不冲突。具体取舍看后续谁消费这个文件纯程序处理就utf-8要人工打开检查就utf-8-sig。在我这个系统里CSV 只是中间产物所以我选择utf-8保证 pandas 读取最稳定。4. 数据分析与预测建模从脏数据到可执行预报数据抓下来只是起点。历史天气页面里的温度字段在极端天里经常带“-”或空白天气现象一栏偶尔混入多余空格更棘手的是日期列的年份和月份在换页时重复。这些脏数据如果不处理直接喂给模型轻则评估指标虚高重则把预测结果带偏。4.1 数据清洗缺失值、异常温度和日期解析拿到 CSV 后的第一件事是把日期字符串转成 pandas 的 datetime 类型同时把最高温、最低温转成数值型。温度列在清洗前往往是 object原因是某些单元格写入的是“12℃”或“-3℃”这种带单位文本。import pandas as pd df pd.read_csv(data/beijing_history.csv, encodingutf-8) df[date] pd.to_datetime(df[date], format%Y-%m-%d, errorscoerce) df[high] df[high].astype(str).str.replace(℃, ).astype(float) df[low] df[low].astype(str).str.replace(℃, ).astype(float) df[weather] df[weather].astype(str).str.strip() # 删除日期解析失败的行例如空日期或乱码 df df.dropna(subset[date]) # 温度合理性检查最高温不低于最低温温差不超过 40℃ df df[df[high] df[low]] df df[df[high] - df[low] 40] df df.sort_values(date).reset_index(dropTrue) df[year] df[date].dt.year df[month] df[date].dt.month df[day_of_year] df[date].dt.dayofyearerrorscoerce在这里是关键参数解析失败时不会抛异常而是把整行日期置为 NaT随后被 dropna 统一清掉。温度合理性过滤的两个条件是我根据全国主要城市气候特征定的经验阈值高原地区昼夜温差大但一般仍在 25℃以内40℃ 的上限足够宽松又不会放过明显的录入错误。4.2 特征工程把日期拆成周期、把昨天变成特征天气预测本质是时间序列问题但这里我选择用监督学习的视角来建模把“预测某一天的最高温”转化为“用当天的日期特征和前几天的温度特征预测当天最高温”。这样可以直接套 scikit-learn 的线性回归不需要引入专门的时序库。特征分两组。第一组是周期特征包括day_of_year、month这两列能捕捉季节规律第二组是滞后特征也就是前几天观测到的实际温度。因为预测目标是当天的最高温而当天还没过完所以只能用昨天的数据做特征。核心的滞后特征是昨天最高温、昨天最低温、前天最高温。df[high_yesterday] df[high].shift(1) df[low_yesterday] df[low].shift(1) df[high_2days_ago] df[high].shift(2) df[temp_range] df[high] - df[low] # 昨天的昼夜温差 feature_cols [day_of_year, month, high_yesterday, low_yesterday, high_2days_ago, temp_range] model_df df.dropna(subsetfeature_cols [high]).copy()shift(1)会生成缺失值因为 1 月 1 日没有“昨天”所以建模前必须 dropna。注意 dropna 用的是feature_cols [high]保证模型输入和标签都不为空。temp_range 作为温差特征能在换季时帮助模型区分晴天大温差还是阴雨小温差。4.3 预测建模线性回归与“去年同期”基准对比建模时不需要复杂的算法。天气温度滞后性较强用带周期特征的线性回归已经能得到可接受的结果。重点在于和“去年同期”这个朴素基线比一比如果模型连基线都打不过说明特征构造或数据清洗有问题。from sklearn.model_selection import TimeSeriesSplit from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error X model_df[feature_cols] y model_df[high] # 时间序列切分训练集占前 80%测试集占后 20% split_idx int(len(model_df) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) print(线性回归 MAE:, round(mean_absolute_error(y_test, y_pred), 2)) # 基线直接用去年同一天最高温作为预测值 test_df model_df.iloc[split_idx:].copy() test_df test_df.merge( model_df[[month, day_of_year, high]].rename(columns{high: high_last_year}), on[month, day_of_year], howleft, ) test_df test_df[test_df[month] 1] # 1 月没有去年同日数据剔除 baseline_mae mean_absolute_error(test_df[high], test_df[high_last_year]) print(去年同期基线 MAE:, round(baseline_mae, 2))这里使用 TimeSeriesSplit 其实没有直接调用而是一个固定比例的按时间顺序切分。为什么不用train_test_split因为它的随机打乱机制会把未来的样本混进训练集造成时间穿越。按 80%/20% 前分后切是最稳的做法。baseline 的计算逻辑是“拿同一月份同一天去年的高温当作今年的预测值”这是一种经得起验证的朴素方法模型 MAE 只有稳定低于 baseline MAE才能说明模型学到了滞后特征之外的新信息。4.4 结果评估MAE、R² 与一张 Eval 表只看 MAE 不够还要看误差分布。我一般会额外计算 R²并且把每月误差单独拆出来看。冬季温度波动大如果模型冬季误差显著偏高说明特征里缺少冷空气活动相关变量夏季误差偏高则多半是降水对温度的调节作用没有被表达出来。from sklearn.metrics import r2_score import numpy as np test_df[pred] y_pred test_df[error] np.abs(test_df[pred] - test_df[high]) print(R2:, round(r2_score(y_test, y_pred), 3)) monthly_mae test_df.groupby(month)[error].mean().round(2) print(monthly_mae)我在实际项目里会把这个统计结果存成一张 eval_summary.csv列内容为 month、sample_count、mae、r2方便月末复盘。如果某月样本量少于 5 条这个月的指标没有参考价值不要在汇总里直接用它下结论。5. 天气数据系统的避坑清单翻车集中在 5 个环节这个项目跑通不难但中途劝退人的坑几乎都集中在数据获取、编码处理和版本兼容上。以下五条是我反复踩过的每一条都按“现象 → 原因 → 解决”的顺序说清楚。5.1 城市名拼音没查准请求直接 404现象换成chengdu后脚本很安静但 CSV 一直是空的后台打印“跳过 2019 年 1 月请求失败”。原因该天气站的路径拼的是城市代码cd而不是一级拼音chengdu城市代码列表藏在页面内部的搜索框脚本里。解决换城市前先手动用浏览器打开 URL 调试一次确定 URL 规则再改 config。不要信任记忆里的拼音规则直接从网页地址栏复制最可靠。5.2 请求过快被限流休眠和随机等待别省略现象抓前 20 个月正常到第 21 个月开始频繁超时然后被服务器断开连接。原因固定间隔 0.5 秒在快速网络下会形成周期性的请求节奏容易被反爬策略识别。解决按time.sleep(REQUEST_INTERVAL random.uniform(0.2, 0.8))设定间隔扰乱固定节奏。抓取任务建议每抓满五个年份就歇 30 秒再继续成本几乎可以忽略但稳定性提升很大。5.3 CSV 中文乱码Excel 打开灾难级体验现象pandas 读出来是正常的但 Excel 打开beijing_history.csv天气一列全是“锟斤拷”。原因文件用 UTF-8 编码写入Excel 默认按本地 ANSI 解析中文。解决两种处理方式。如果你只给 pandas 用维持utf-8即可如果团队里有人要人工核对写入时改成utf-8-sig。pandas 读取utf-8-sig跟读取utf-8没有行为差异放心换。5.4 去年同期的温度是 0清洗时别当坏值删现象基线 MAE 奇大无比一查发现high_last_year列出现大量 0。原因天气页在个别月缺失数据时表格单元格会写 “0” 作为占位不是真实的“0℃”。解决基线计算前对high_last_year做过滤只保留 10℃ 以上且 45℃ 以下的行或者直接剔除high_last_year 0的样本。这属于业务语义问题不能简单用数值清洗规则一刀切。5.5 pandas 和 scikit-learn 版本冲突运行时报错无法定位现象model.fit(X_train, y_train)这行报ValueError: could not convert string to float但打印 X_train.dtypes 全部是 float64。原因新版 pandas 的 category 列在某些版本 scikit-learn 的输入校验下会触发转换问题尤其是 df 里混入了 object 列而你没留意。解决统一X X.astype(float)显式转换并检查X_train.dtypes的输出结果是否全部为 float64。这个坑在 pandas 2.0 scikit-learn 1.2 组合上尤其常见升级 pandas 到 2.x 后基本消失。5.6 极值校验别让异常数据毁掉图表现象某城市 7 月 15 日的最高温被记录成 5℃ 和 40℃ 同时存在折线图上出现一根孤零零的尖刺。原因页面手工维护的表格偶尔有录入错误同一个城市同一天出现两条矛盾记录。解决除了温差上限过滤再按城市历史极值做一次整体校验。比如北京极端低温不低于 -30℃、极端高温不高于 43℃超出范围的样本直接丢弃。这个阈值写在 config 里换城市时先查当地气象记录再改。6. 图表可视化与重跑用 matplotlib 输出一眼能看懂的分析结果预测模型跑完只是完成了“算”最终要落到“看”。matplotlib 是这个环节的主力围绕数据分析结果画折线图、热力图和直方图输出到 output 目录。6.1 双轴折线图温度与湿度的关系一眼看清拿温度做主坐标轴、湿度作次坐标轴是最直观的组合图。这类图能很快看出夏季高温伴随高湿、冬季干冷低湿的规律。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, sans-serif] plt.rcParams[axes.unicode_minus] False fig, ax1 plt.subplots(figsize(12, 5)) ax1.plot(df[date], df[high], color#d62728, label最高温, linewidth0.8) ax1.plot(df[date], df[low], color#1f77b4, label最低温, linewidth0.8) ax1.set_ylabel(温度℃) ax1.legend(locupper left, fontsize9) plt.tight_layout() plt.savefig(output/temperature_trend.png, dpi150)这段代码的关键在两行plt.rcParams设置中文字体否则坐标轴和标签里的中文全是方框savefig而不是plt.show()因为服务端跑脚本时show()会阻塞且没有图形界面。图片存成 PNG 后后续写报告、做 PPT 直接引用文件即可。6.2 热力图和直方图把月均温度与温差呈现出来热力图适合展示“月 × 年”的二维规律。横轴是 1 到 12 月纵轴是年份颜色深浅代表该月平均最高温。直方图则用来展示温度分布形态。import pandas as pd import numpy as np import matplotlib.pyplot as plt pivot df.pivot_table(valueshigh, indexyear, columnsmonth, aggfuncmean) plt.figure(figsize(10, 5)) plt.imshow(pivot.values, cmapRdBu_r, aspectauto) plt.colorbar(label月均最高温℃) plt.xticks(range(12), [f{i}月 for i in range(1, 13)]) plt.yticks(range(len(pivot.index)), pivot.index) plt.savefig(output/monthly_heatmap.png, dpi150) plt.figure(figsize(8, 4)) plt.hist(df[high], bins24, color#2a7f62, alpha0.8) plt.xlabel(最高温℃) plt.ylabel(出现天数) plt.savefig(output/high_temp_dist.png, dpi150)pivot_table自动把未出现过的年月组合填成 NaN所以在 imshow 之前不需要额外处理。cmap 选RdBu_r红色表示高温、蓝色表示低温符合直觉参数aspectauto让热力图格子根据画布自动拉伸避免出现扁长单元格。6.3 把整个流程做成“一条命令”跑完数据抓取、清洗、建模、画图四个脚本各司其职但每次手动执行四个文件太低效。我通常在项目根目录放一个 run.py按顺序 import 并执行让整条流程一次跑完。# run.py import fetch_data import analysis import visualize if __name__ __main__: fetch_data.main() analysis.main() visualize.main() print(全部完成图表已输出到 output/ 目录)这样设计后每月更新一次数据只需要重新执行python run.py。数据抓取模块会自动追加新月份的数据即使中断也可以接着跑增量追加写保证不会重复。整套系统从拿到源码到产出图表在一台普通笔记本上大约十几分钟就能跑完运行成本基本可以忽略。预测模型这边如果后续想换更强的算法只需要改动 analysis.py 中的model LinearRegression()这一行换成RandomForestRegressor(n_estimators200, max_depth6)并调整对应的 import 即可前期的数据管道完全不用动。这也是我习惯把数据抓取、特征工程与建模拆成独立文件的原因换模型永远比换数据处理快得多。希望这个方案能帮你少走弯路尽快跑出自己的第一版天气预测分析系统。本文还有配套的精品资源点击获取