Python机器学习气象预报系统:从爬虫采集到Flask部署全实现

发布时间:2026/10/3 2:50:40
Python机器学习气象预报系统:从爬虫采集到Flask部署全实现 简介该资源是一套完整的毕业设计项目基于Python与机器学习方法实现气象预报及动态展示系统。项目面向计算机、数据科学相关专业学生尤其适合需要完成毕业设计或课程设计的人群。压缩包内共94个文件包含python源码、JavaScript前端脚本、模型文件、数据集与HTML页面等核心代码涵盖数据爬取、数据库操作、模型训练与Web展示模块整体大小约2.1MB便于下载与本地运行。目前已有104人浏览学习。资源提供了从数据获取、预处理、模型训练到可视化展示的完整工程化流程既可作为学习机器学习实际落地应用的参考案例也可作为毕业设计文档撰写和系统开发的功能框架。1. 一个Python机器学习的气象预报系统拆开看能落地什么气象预报这个场景最容易让人误判难度。很多人以为核心是机器学习模型真正拿到项目才知道磨人的是数据链路——爬虫抓的数据能不能对齐、历史缺失值怎么补、训练集测试集怎么切。这个基于Python机器学习的毕业设计项目把整条链路完整走了一遍weather_spider.py负责采集ProcessData.py和dataexct.py做清洗GetModel.py训练出Model.pkl最后weatherWeb.py配合templates把预测结果动态展示在网页上。适合正在做毕设、或者想找一份完整可运行的机器学习落地案例的人。接下来按数据、模型、展示、避坑的顺序把每一步的写法和参数讲透。2. 数据链路打通爬虫采集、清洗与切分的三个关键写法2.1 weather_spider.py 的抓取逻辑站点选择与字段映射先说爬虫。气象数据不像普通文本数据它有严格的时间连续性和字段一致性要求——今天抓的温度、湿度、风力和明天抓的必须能拼到一张表里。所以weather_spider.py里最重要的不是请求代码而是字段映射的设计。import requests from bs4 import BeautifulSoup import pandas as pd import time def fetch_weather(city_code, days7): 抓取指定城市未来days天的气象数据返回DataFrame url fhttp://www.weather.com.cn/weather/{city_code}.shtml headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) items soup.select(ul.tabs li) records [] for item in items[:days]: date item.select_one(h1).text.strip() weather item.select_one(p.wea).text.strip() temp item.select_one(p.tem span).text.strip() wind item.select_one(p.win i).text.strip() records.append({ date: date, weather: weather, temp: temp, wind: wind }) time.sleep(1) # 控制请求频率避免被封IP return pd.DataFrame(records)这段代码的核心在select_one的解析链。中国天气网的页面结构里每天的气象信息包在一个li里h1是日期p.wea是天气现象p.tem span是温度p.win i是风力。不同站点的页面结构有差异如果选择器写错程序不会报错但会返回空列表——这是爬虫阶段最常见的“静默失败”。拿到空DataFrame后后续数据库写入和特征工程全都会静默跳过最终模型预测出来的值是上一次缓存的旧结果排查起来非常隐蔽。两个细节需要注意。第一resp.encoding utf-8必须放在请求之后、解析之前否则中文会乱码。第二time.sleep(1)不是可有可无的“礼貌”而是对目标站点的基本保护。抓太快容易被封IP单次任务抓7天数据7秒延迟完全在可接受范围内。如果想提高吞吐可以改成随机sleep 0.5到1.5秒效果类似但不建议完全去掉。字段映射上我一般会把weather这类中文字段保留原文同时把temp字段转成数值。项目里weather_dbhelper.py和weather_dao.py就是负责把爬下来的DataFrame写入数据库的——dbhelper管连接dao管增删改查爬虫模块自身不碰数据库。这个解耦在后边接新数据源时会省很多事比如你从另一个接口补抓气压数据只需要改爬虫模块的返回字段数据库层完全不用动。2.2 ProcessData.py 与 dataexct.py从原始数据到特征矩阵爬虫拿到的是“能用眼睛看”的数据模型需要的是“能计算”的数据。ProcessData.py干的就是这个转换活dataexct.py更多承担从原始文件或数据库里提取指定时间范围的记录get_exatdata.py负责抓取外部补充数据比如气压、降水量这类页面不直接给的字段。特征工程这一步气象数据的处理方式和普通回归任务有明显区别——时间依赖性意味着你不能简单地逐行处理而要造出“和历史挂钩”的特征。import pandas as pd def process_raw_data(df): 将原始气象数据转为可训练的特征矩阵 df df.copy() # 1. 统一时间格式并排序 df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 2. 温度列从字符串转数值处理23℃这类格式 if df[temp].dtype object: df[temp] df[temp].str.replace(℃, ).astype(int) # 3. 缺失值用线性插值而不是直接fillna(0) df[temp] df[temp].interpolate(methodlinear, limit_directionboth) df[humidity] df[humidity].interpolate(methodlinear, limit_directionboth) # 4. 构造滞后特征昨天和前天的温度、湿度 df[temp_lag1] df[temp].shift(1) df[temp_lag2] df[temp].shift(2) df[humidity_lag1] df[humidity].shift(1) # 5. 3日滑动均值基于滞后1天的序列滚动计算确保不含当天信息 df[temp_ma3] df[temp].shift(1).rolling(3).mean() # 6. 删除还没有完整滞后特征的前几行 df df.dropna() return df插值而非填0是气象数据处理的基本常识。温度、湿度这类连续变量的缺失值如果填0会让特征分布产生一个明显的尖峰模型学完这个分布之后预测值会被带偏。pandas的interpolate(methodlinear)会用前后两个有效值的连线补齐缺口对气象数据这种缓变序列基本不会出错。滞后特征lag feature是时间序列预测的关键。temp_lag1的含义是“昨天温度”temp_lag2是“前天温度”。模型看到今天和昨天的温度就能学到“温度不会突跳”这个隐含规律。滑动均值temp_ma3则进一步平滑单日异常波动——但注意它是基于shift(1)之后的序列算rolling也就是“昨天及之前三天的均值”不包含当天真实温度这是为了防止特征泄漏。如果直接对原始temp列做rolling(3)当天温度会混进特征里模型在训练集上表现极好、测试集崩掉这种翻车我在第5章会详细说。data_old_transfer.py的用途值得单独提一下。气象站早年的历史数据格式往往和现在不同日期列是字符串、温度带小尾巴、缺测值用-999表示。我一般会写一个独立脚本做老数据迁移把这些特殊标记统一成NaN或标准格式跑完再交给ProcessData.py。项目里单独放一个data_old_transfer.py就是这个定位——不要把它并到主流程里不然每次跑预处理都要带着旧格式的兼容逻辑改起来很痛苦。2.3 时间序列数据集切分train/valid/test不能随机打乱用sklearn的人最容易在这个点上翻车。train_test_split默认shuffleTrue对普通分类任务没问题但气象数据是时间序列——前天的天气和今天的天气本来就有强关联一旦随机打乱模型在训练集里“看到”了未来的样本验证集评估出来的指标虚高得离谱。import pandas as pd def temporal_split(df, train_ratio0.7, valid_ratio0.15): 按时间顺序切分训练集、验证集、测试集禁止随机打乱 df df.sort_values(date).reset_index(dropTrue) n len(df) train_end int(n * train_ratio) valid_end train_end int(n * valid_ratio) train df.iloc[:train_end] valid df.iloc[train_end:valid_end] test df.iloc[valid_end:] return train, valid, test三个集合的时间区间不重叠且严格按时间先后排列。train_ratio和valid_ratio分别控制训练集和验证集占比气象项目里7:1.5:1.5是比较常见的配比。数据量大可以适当给训练集更多比例数据量少就要留够验证集因为模型选型和调参都靠验证集说话验证集太小调参结果根本没参考价值。切完三个csvweather_train_train.csv、weather_train_valid.csv、weather_test.csv有一个容易忽略的执行顺序问题一定要先构造特征、再切分、最后丢NaN行。如果先切分再做rolling窗口会越界产生大量NaN这些NaN样本要么被drop要么被错误填充如果先丢NaN再切分切出来的三个集合仍然是对的但建议统一走“特征构造→temporal_split→丢NaN”的顺序避免不同脚本各写各的造成结果对不上。提示项目里最好统一写一个temporal_split函数所有切分都走它。Main.py和GetModel.py如果各写各的切分逻辑一个改了比例另一个忘了验证结果就对不上了。3. 模型训练与复用GetModel.py的参数调优与Model.pkl的工程化3.1 气象预测模型选型从线性回归到随机森林的实际考量气象预报的模型选择有个尴尬理论上ARIMA这类时间序列模型是正统但实际项目里反而很少单用。原因在于气象变量是强非线性的——温度受纬度、海拔、洋流、季节、气压系统共同影响ARIMA的线性假设很难拟合这种多因子耦合。项目里用的是机器学习回归模型这是个务实的决定。模型非线性拟合训练速度超参数量泛化风险适用场景线性回归弱极快少低基线对比决策树中快中高快速验证随机森林强中中低表格数据主力XGBoost/LightGBM强中慢多中追求精度ARIMA弱快中低纯单变量序列这个项目落在随机森林上我认为是合适的。气象特征表本质是“多列数值特征少量时序特征”随机森林对这类表格数据的拟合能力强对缺失值不敏感也不需要对特征做标准化。相比XGBoost随机森林的超参更少n_estimators、max_depth、min_samples_leaf三个参数调明白就能收敛对毕业设计场景足够。XGBoost能再压一点误差但代价是调参周期翻倍对“系统能跑通、结果可信”这个目标来说性价比不高。3.2 GetModel.py训练流程特征输入、标签构造与评估指标GetModel.py这个脚本的职责很明确——读取切分好的csv训练模型评估保存Model.pkl。我按项目结构复现了它的核心流程import pandas as pd import numpy as np from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error import pickle def train_model(): train pd.read_csv(weather_train_train.csv) valid pd.read_csv(weather_train_valid.csv) feature_cols [temp_lag1, temp_lag2, temp_ma3, humidity_lag1, humidity] target_col temp X_train train[feature_cols].values y_train train[target_col].values X_valid valid[feature_cols].values y_valid valid[target_col].values model RandomForestRegressor( n_estimators200, max_depth10, min_samples_leaf2, random_state42 ) model.fit(X_train, y_train) pred model.predict(X_valid) mae mean_absolute_error(y_valid, pred) rmse np.sqrt(mean_squared_error(y_valid, pred)) print(f验证集 MAE: {mae:.2f}C, RMSE: {rmse:.2f}C) with open(Model.pkl, wb) as f: pickle.dump(model, f) return model if __name__ __main__: train_model()先看三个参数。n_estimators200是随机森林的常用起点树越多预测越稳定、方差越小但训练时间和模型体积线性增长。气象特征表数据量通常不大200棵完全够用。我试过500棵MAE只降了不到0.05度代价是Model.pkl体积大了两三倍。max_depth10是为了限制单棵树的复杂度——气象特征里的滞后项和滑动均值之间存在多重共线性深树会死磕这些冗余特征导致过拟合。min_samples_leaf2保证叶子节点至少有2个样本进一步剪枝。评估指标用MAE和RMSE两个。MAE的直观含义是“平均差多少度”便于向评审或非技术同学解释。RMSE对大误差更敏感如果RMSE明显大于MAE说明样本里存在少数预测极差的天——通常对应天气快速变化的转折点这类样本用随机森林很难救可以考虑加更多气象因子或者把日期特征月份、季节也纳入训练。这里要特别强调特征列顺序的一致性。训练时feature_cols的顺序是lag1、lag2、ma3、humidity_lag1、humidity之后Web端加载模型做预测时构造的features列表必须严格按这个顺序排列。顺序错了模型不会报错但预测值完全对不上——这是个非常容易踩的坑。3.3 Model.pkl的保存与加载pickle的序列化细节与跨模块复用训练完的模型必须落盘否则每次Web服务启动都要重新训练一次。Model.pkl就是RandomForestRegressor的pickle序列化对象。import pickle def load_model(pathModel.pkl): 加载训练好的模型带文件存在性检查 try: with open(path, rb) as f: model pickle.load(f) return model except FileNotFoundError: raise FileNotFoundError( f{path} 不存在请先运行 GetModel.py 再启动Web服务 )pickle.load有几个必须知道的点。第一Python大版本必须一致。训练时用3.9加载时也必须是3.9。3.8和3.9之间的pickle协议不完全兼容常见的报错是unsupported pickle protocol或module lookup error。第二依赖库版本最好一致尤其是sklearn。训练端是sklearn 1.0Web端是sklearn 0.24load的时候会直接抛AttributeError因为随机森林类的内部结构变了。第三Model.pkl文件本身不小200棵深度10的树大概几MB放Web服务里没压力但如果以后换XGBoost模型文件会到几十MB要注意部署环境的磁盘空间和加载耗时。另外GetModel.py里那个if __name__ __main__是必须的。这个脚本被Main.py或其他模块import的时候不应该触发训练逻辑只有直接运行时才训练。很多人在这一步翻车——import一个模块结果整个模型重新训练了一遍白白等十几分钟然后发现训练数据还是旧的。4. 动态展示层weatherWeb.py、templates与数据访问层的配合4.1 Flask路由设计weatherWeb.py如何串联预测与页面Web层是项目面向用户的出口。weatherWeb.py基于Flask搭建核心思路是“模型只管预测Web只管展示”两者之间通过Model.pkl和数据库隔离。from flask import Flask, render_template import pickle import pandas as pd app Flask(__name__) # 模型在进程启动时加载一次之后所有请求复用 with open(Model.pkl, rb) as f: model pickle.load(f) # 特征列顺序必须与GetModel.py训练时完全一致 FEATURE_COLS [temp_lag1, temp_lag2, temp_ma3, humidity_lag1, humidity] app.route(/) def index(): 首页展示历史气温信息和下一次预测结果 df pd.read_csv(weather_test.csv) df df.sort_values(date) latest df.iloc[-1] features [latest[col] for col in FEATURE_COLS] pred model.predict([features])[0] history df.tail(14).to_dict(records) for row in history: row[date] str(row[date])[:10] # 只留日期部分 return render_template( index.html, predictionround(float(pred), 1), historyhistory, todaystr(latest[date])[:10] )这里有一个工程细节模型在Flask进程启动时加载一次之后每个请求直接复用不要在路由函数里反复load。pickle.load成本不低如果每次请求都load一遍并发一上来Web服务立刻卡死。model.predict([features])里的[features]是为单个样本加了外层维度predict要求输入是二维数组。如果预测多个城市或多个日期就把多个特征行拼成二维矩阵传进去一次调用返回所有结果。history用to_dict(records)转成普通字典列表再把date字段转成字符串这一步是为了避免Jinja2模板渲染时遇到pandas的Timestamp类型出现格式问题。prediction用round(float(pred), 1)转成原生float否则numpy的float64会被模板当成字符串直接输出页面显示会带上一长串小数。4.2 动态展示的实现plot.png与模板数据渲染项目里static/plot.png是预生成的趋势图templates下的模板负责渲染动态数据。静态图加动态表格是这套系统展示层的基本组合——图展示趋势表格展示精确数值。!-- templates/index.html -- !DOCTYPE html html head title气象预报系统/title /head body h1城市气象预测/h1 p今天是 {{ today }}预测气温{{ prediction }} °C/p img src{{ url_for(static, filenameplot.png) }} alt历史气温曲线 stylemax-width: 800px; table border1 trth日期/thth气温/thth天气/th/tr {% for row in history %} tr td{{ row.date }}/td td{{ row.temp }}/td td{{ row.weather }}/td /tr {% endfor %} /table /body /htmlJinja2模板里的{{ row.date }}和{% for row in history %}是核心语法render_template传入的history是字典列表模板直接循环渲染。这里有一个必须注意的点传入模板的数据必须是可以安全序列化的原生Python类型。如果你直接传numpy数组或numpy标量页面显示出来的格式会乱掉。所以在4.1的代码里prediction用round转成floatdate字段转成字符串都是为了模板渲染的稳定性。plot.png的更新频率取决于预测运行频率。如果需要每天更新图片常见做法是在定时任务里用matplotlib生成图后写入static目录前端刷新页面时url_for(static, filenameplot.png)会自动拿到新图。浏览器会有缓存可以加版本号强制刷新比如plot.png?v20250601这个参数变了浏览器就会重新请求图片。如果想把展示做得更“动态”可以将Flask接口改成返回JSON前端用ajax定期拉取最新预测值这样页面不用整体刷新就能更新数据。但毕业设计或演示场景下模板渲染加定时刷新已经足够不必引入前端框架增加复杂度。4.3 数据访问层weather_dbhelper.py与weather_dao.py的分层设计系统里数据库相关的代码分了两层weather_dbhelper.py管数据库连接weather_dao.py管数据操作。这个分层不是过度设计而是让爬虫、Web、模型脚本三方共用一套数据库访问逻辑互相不耦合。# weather_dbhelper.py import sqlite3 def get_connection(db_pathweather.db): 获取数据库连接返回带Row工厂的连接对象 conn sqlite3.connect(db_path) conn.row_factory sqlite3.Row return conn# weather_dao.py from weather_dbhelper import get_connection def insert_weather_records(records): 批量插入气象记录已有日期的记录跳过 conn get_connection() sql INSERT OR IGNORE INTO weather ( city, date, temp, humidity, weather, wind ) VALUES (?, ?, ?, ?, ?, ?) conn.executemany(sql, records) conn.commit() conn.close() def query_recent(date_from, date_to): 查询指定时间范围的气象记录按日期升序 conn get_connection() sql SELECT * FROM weather WHERE date BETWEEN ? AND ? ORDER BY date ASC rows conn.execute(sql, (date_from, date_to)).fetchall() conn.close() return rowsINSERT OR IGNORE是气象数据入库的一个很实用的写法。爬虫每天都会跑同一个日期的数据被抓两次很正常如果不做去重表里会出现重复记录特征工程阶段lag特征会错位。sqlite的OR IGNORE依赖表上对(date, city)的唯一索引建表时加上UNIQUE约束重复数据就会被静默跳过。dbhelper和dao分开的好处是Web服务需要改数据库类型时——比如从sqlite迁到MySQL——只需要改dbhelper里的连接逻辑dao里的SQL语句基本不用动。反过来如果调整了查询逻辑也只改dao不会碰连接层。这套分层思路在小型项目里看起来有点“重”但一旦数据量和业务逻辑涨起来你就知道当初这个拆分有多值。5. 避坑指南气象预报系统里最常见的6个翻车点5.1 爬虫数据时区错位预测整体偏移一天现象模型预测结果整体比实际气温偏高2到3度误差呈现出周期性的波动。原因爬虫抓取的是气象站发布的UTC时间入库时没做时区转换。特征工程按“昨天”聚合时实际取到的是前一天的部分数据和当天的数据混合在一起时间边界整个错位。解决入库前统一转成北京时间用pd.to_datetime(df[date]).dt.tz_localize(UTC).dt.tz_convert(Asia/Shanghai)再转成无时区格式存储。所有脚本的时间字段统一走这一个转换函数不要各写各的。5.2 验证集表现好、测试集崩掉现象验证集MAE只有1.2度测试集MAE突然变成3.8度差了3倍。原因切分时用了train_test_split的默认shuffleTrue验证集里混入了训练集时间段的样本模型在验证集上“作弊”——它已经见过那些天的特征了。时间序列数据必须按时间顺序切分不能随机打乱。解决改走2.3节的temporal_split函数按时间顺序切分。切完之后打印三个集合的日期范围确认没有重叠。5.3 Model.pkl在Web端load失败现象模型在GetModel.py里加载没问题weatherWeb.py一启动就报AttributeError: module sklearn.ensemble has no attribute RandomForestRegressor或者直接抛ModuleNotFoundError。原因当前环境的sklearn版本与训练时不一致。pickle序列化记录的是类的import路径训练端升级或降级了大版本加载端就找不到对应类的属性。解决把训练和运行环境的依赖写成requirements.txt统一管理。pip freeze requirements.txt部署新环境后pip install -r requirements.txt。至少保证Python主版本和sklearn大版本一致。5.4 缺失值粗暴填0现象气温特征的分布直方图在0度位置出现一个异常高的尖峰模型预测结果在冬季频繁出现负偏差。原因数据清洗时用fillna(0)填充缺失值把缺测的温度全算成了0度模型学到“温度是0”的错误模式。解决改用interpolate线性插值。如果缺失值集中在某一段比如连续降雨导致湿度传感器故障用limit_directionboth保证序列两端也能补上。填0留给那些“真0”才有意义的字段比如降水量。5.5 特征泄漏当天的目标变量混进特征现象训练集MAE接近0验证集和测试集MAE高得离谱模型形同虚设。原因构造特征时把当天的实际温度放进了feature_cols模型直接“抄答案”。更隐蔽的一种是窗口聚合泄漏——对未shift的temp列直接做rolling(3)均值均值里包含当天真实温度标签也是当天温度模型又作弊了。解决审一遍特征列凡是用当天数据计算、又能直接或间接推出当天标签的列全部移除。滞后特征和滑动均值必须基于过去的数据比如df[temp].shift(1).rolling(3).mean()才是安全的滑动均值。5.6 模型成了“黑匣子”答辩讲不清楚现象Model.pkl预测结果正常但被问到“为什么预测这个值”时完全答不上来。原因随机森林的解释性弱于线性回归加上特征列命名不规范难以回溯模型决策依据。解决用model.feature_importances_输出每个特征的贡献度展示“昨天温度贡献了40%的预测权重”这类结论。同时把特征列名统一维护在一个config文件里训练端和应用端都从config读取保证顺序一致、口径一致。6. 把Main.py改成定时任务让预报系统每天自动跑完整链路到这里项目核心模块都过了一遍最后说一个能让系统真正“活”起来的小改造——定时执行。气象预报的价值在于“每天给一个最新的预测”手工跑Main.py不是不行但总会忘。常见的做法是用schedule库在脚本内部做定时简单直接。import schedule import time import subprocess def run_pipeline(): 每天执行完整预测链路采集→清洗→训练→发布 print(开始执行气象预报流水线...) subprocess.run([python, weather_spider.py], checkTrue) subprocess.run([python, ProcessData.py], checkTrue) subprocess.run([python, GetModel.py], checkTrue) subprocess.run([python, Main.py], checkTrue) print(流水线执行完成) schedule.every().day.at(06:00).do(run_pipeline) print(定时任务已启动每天06:00执行) while True: schedule.run_pending() time.sleep(60)checkTrue保证前一个脚本失败时立刻终止流水线不会带着脏数据往下跑。06:00这个时间点是个人习惯——气象数据源通常在凌晨更新完前一天的数据早上跑既能拿到全部数据、又避免和用户访问高峰冲突。如果你把服务部署在NAS或服务器上也可以用crontabschedule的好处是跟代码走换机器不丢配置。从那以后我每次做数据类项目都会把“自动化执行”当作必选项而不是可选项——手工跑一次没问题但没人记得连续跑30天。把采集、清洗、训练、发布串成流水线加上日志输出系统才算真正落地。如果你也打算用这份源码做毕设或练手建议先照这个顺序跑通一遍再动手改你自己的城市和特征。希望帮到你。本文还有配套的精品资源点击获取