简介:这是一份面向Python初学者的期末大作业/课程设计完整方案,围绕天气数据爬取与可视化分析,实现从数据采集、清洗建模到结果展示的闭环流程。资源共24个文件,压缩包仅1.42MB,其中GetData.py、ProcessData.py、GetModel.py和main.py四个Python脚本分别承担爬虫采集、数据预处理、模型训练与主程序运行,配套csv格式训练/测试数据、Model.pkl模型文件以及天气网.html展示页面,另有12张jpg截图可直观对照每一步效果,readme.md说明文档便于快速上手。项目代码包含详细注释,部署简单,新手也能按教程完成环境配置并运行,适合课程设计、期末答辩或作为爬虫与数据分析入门实战参考。已有926人学习下载,功能完善、界面直观,具有较高参考与应用价值。
1. 从天气数据大作业说起:爬虫、清洗、建模一条链
很多 python 大作业和课程设计都会选“网络爬虫爬取天气数据及可视化分析”这个方向,因为链路完整、演示效果好,但真正拿到满分的人并不多。这份项目能给你的不是一段能跑的代码,而是把采集、清洗、建模、可视化串成一条工程流水线的套路:GetData.py 负责抓数据,ProcessData.py 把乱七八槽的网页字段整理成规整表格,GetModel.py 训练回归模型并导出 pkl,最后 main.py 一键跑通全流程并输出图表。适合期末大作业、课程设计、毕业设计前练手,也适合工作两三年的 Python 工程师拿来抄作业。一个反直觉的结论是:这个项目里真正难的不是爬虫,而是日期对齐和特征构造——数据只要错一天,模型分数再高也是假的。
2. 数据采集:requests + BeautifulSoup 抓天气网与参数设置
2.1 页面解析与字段抽取
项目里保留了天气网.html 这个静态文件,说明当时是先用浏览器保存页面,再用本地 HTML 做解析调试的。这个思路对大作业来说非常实用:服务器随时可能改版或封 IP,先把页面存成本地文件,解析逻辑稳定后再换线上 URL,能省不少时间。
抓取核心用 requests 获取页面源码,再用 BeautifulSoup 定位数据所在的标签。天气类网站常见结构是<ul class="t">下挂着每一天的<li>,每个<li>里有日期、最高温、最低温、天气现象、风力等字段。参考 GetData.py 里的做法,代码一般长这样:
import requests from bs4 import BeautifulSoup import pandas as pd HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" } def parse_weather(html_text): soup = BeautifulSoup(html_text, "html.parser") items = [] # 存储每条记录 for li in soup.select("ul.t li"): # 日期:如 2024-06-01 date_tag = li.select_one(".date") # 天气现象:如 晴 / 多云 / 小雨 wea_tag = li.select_one(".wea") # 最高温 / 最低温:如 32℃ / 21℃ temp_tag = li.select_one(".tem span") high, low = extract_temps(temp_tag) # 自定义函数拆分两个温度 items.append({ "date": date_tag.text.strip(), "weather": wea_tag.text.strip(), "high": high, "low": low }) return pd.DataFrame(items)extract_temps这一步很关键,因为页面里温度可能写在一个标签里,也可能是两个标签。我一般这样处理:
def extract_temps(temp_tag): if temp_tag is None: return None, None # 常见结构:<span>32℃</span>/<span>21℃</span> spans = temp_tag.find_all("span") if len(spans) >= 2: return clean_temp(spans[0].text), clean_temp(spans[1].text) # 也可能是直接文本:"32℃ / 21℃" text = temp_tag.get_text(" ", strip=True) parts = text.replace("℃", "").split() nums = [p for p in parts if p.lstrip("-").isdigit()] return (int(nums[0]), int(nums[1])) if len(nums) >= 2 else (None, None) def clean_temp(s): return int(s.replace("℃", "").strip())参数说明:soup.select用的是 CSS 选择器,比find_all更贴近前端 DOM 结构,调试时也容易对照第一个参数"ul.t li"的含义是“class 为 t 的 ul 下的所有 li”。temp_tag里如果直接调get_text(" "),多个 span 之间会用空格连接,方便后续 split 拆分;这里先尝试提取 span,再 fallback 到文本解析,是比较稳的写法。
2.2 抓取策略与反爬兜底
在线抓取时不要直接用 requests.get 裸奔,至少要带上Headers里的User-Agent和Referer。很多课程作业被 ban 是因为请求头里是默认 Python-requests 标识,一眼被识别。项目里应该也有类似设置,建议再加一层:
session = requests.Session() session.headers.update(HEADERS) resp = session.get(url, timeout=10) resp.encoding = resp.apparent_encoding # 避免中文乱码resp.apparent_encoding比resp.encoding更可靠,因为天气网页头可能没写 charset,或者写的与实际不符。抓取频率上,我一般会在两次请求之间加time.sleep(1),大作业数据量不大,控制在几十条记录就没有封 IP 风险。如果目标网站有验证码或滑块,果断放弃,直接换历史数据源或人工保存 HTML。
2.3 落盘与增量更新
项目输出两个 CSV:date_train.csv和date_test.csv。这说明采集不是一次性,而是按时间范围拆开。常见做法是把所有历史数据放到china_today.csv这个全量文件中,再用ProcessData.py按日期切分。增量更新时只需要读取已有 CSV 的最后日期,只抓新增部分:
def update_csv(old_file, new_data, date_col="date"): try: old = pd.read_csv(old_file, parse_dates=[date_col]) latest = old[date_col].max() new_data = new_data[new_data[date_col] > latest] merged = pd.concat([old, new_data], ignore_index=True) except FileNotFoundError: merged = new_data merged.drop_duplicates(subset=[date_col], keep="last", inplace=True) merged.to_csv(old_file, index=False, encoding="utf-8-sig")注意encoding="utf-8-sig"是为了 Excel 打开不出现乱码,而 pandas 读取时用默认 utf-8 即可。drop_duplicates的keep="last"用于覆盖同一天可能抓到的重复脏数据,这个比先查出再删除更高效。
3. 数据清洗与特征工程:ProcessData.py 的关键处理
3.1 缺失值与日期对齐
采集到的原始数据往往有三个问题:日期缺失、温度字段为 None、同一天出现多条记录。ProcessData.py 里最先处理的就应该是日期对齐,因为后续训练模型必须依赖连续的时间序列。
项目里有date_valid.csv,我推测这是一个包含完整日期范围的参考表,用于补齐缺失日期。常见做法是先生成完整日期序列:
import pandas as pd import numpy as np def align_dates(df, start="2023-01-01", end="2024-06-30"): full_dates = pd.date_range(start=start, end=end, freq="D") full_df = pd.DataFrame({"date": full_dates}) df["date"] = pd.to_datetime(df["date"], errors="coerce") # 丢弃无法解析的日期,避免 error 行混入 df = df.dropna(subset=["date"]).drop_duplicates("date") merged = full_df.merge(df, on="date", how="left") return merged这里errors="coerce"会把异常日期转成 NaT 并丢给后续 dropna 处理,比直接pd.to_datetime抛异常更可控。merge用how="left"保证日期完整,缺失的气象字段则留在后面填充——这些空值可以视为“当天没有抓到数据”,而不是“不存在”,所以不能直接删除行。
3.2 数值化与平滑处理
天气现象是分类变量,比如“晴”“多云”“小雨”,需要编码成数值才能进模型。最简单的做法是 One-Hot 编码,但对于日期回归预测来说,类别太少会导致维度爆炸。我建议按恶劣程度映射成等级,这也是大作业里容易拿分的点:
weather_map = { "晴": 0, "多云": 1, "阴": 2, "小雨": 3, "中雨": 4, "大雨": 5, "雷阵雨": 6, "小雪": 3, "中雪": 4, "大雪": 5 } df["weather_code"] = df["weather"].map(weather_map).fillna(-1)温度为数值型,但直接拿最高温和最低温当原始特征,模型容易过拟合。常规操作是构造温差和滑动平均:
df["temp_diff"] = df["high"] - df["low"] df["high_ma3"] = df["high"].rolling(window=3, min_periods=1).mean() df["low_ma3"] = df["low"].rolling(window=3, min_periods=1).mean()rolling(window=3)会让前两条记录产生 NaN,加min_periods=1可以避免丢掉开头数据。temp_diff的意义在于描述昼夜温差,这对预测次日最高温很有用。原始数据里温度如果带“℃”字符,需要先清洗:
df["high"] = pd.to_numeric(df["high"].astype(str).str.replace("℃", ""), errors="coerce")3.3 训练/测试集划分
项目里直接给出了三个文件:date_train.csv、date_test.csv、date_valid.csv。这不是随便切的,而是按时间顺序切,因为天气数据是时间序列,随机切分会让模型“偷看”未来数据。合理划分如下:
df = pd.read_csv("result.csv", parse_dates=["date"]).sort_values("date") train_end = int(len(df) * 0.7) valid_end = int(len(df) * 0.85) df_train = df.iloc[:train_end] df_valid = df.iloc[train_end:valid_end] df_test = df.iloc[valid_end:] df_train.to_csv("date_train.csv", index=False) df_valid.to_csv("date_valid.csv", index=False) df_test.to_csv("date_test.csv", index=False)为什么需要三段?因为模型要先用训练集拟合,再用验证集调参,最后用测试集评估泛化能力。很多大作业只切训练/测试两段,答辩时会被问“那验证集呢?”,所以你看到date_valid.csv存在,说明原始项目做得很规范。划分比例上 7:1.5:1.5 对几百条数据来说比较常见;如果数据达到几千条,可以放宽到 8:1:1。
4. 模型训练与预测:GetModel.py 的回归建模
4.1 特征选择与模型选型
预测目标一般是“明天最高温”或“明天最低温”。特征不能只用前一天的温度,那样模型就是无脑复制。项目里在 ProcessData.py 构造的特征可以用来做滞后特征:
for lag in [1, 2, 3]: df[f"high_lag{lag}"] = df["high"].shift(lag) df[f"low_lag{lag}"] = df["low"].shift(lag) df[f"weather_code_lag{lag}"] = df["weather_code"].shift(lag)shift(lag)的含义是“往前数第 lag 天的值”,这能帮模型捕获周期性。模型选型上,大作业不推荐 LSTM,因为数据量撑不起深度学习;随机森林或梯度提升更稳,一个重要的原因是它们对缺失值有容忍度,而且能输出特征重要性,答辩时可以用图表展示。参考 GetModel.py 的命名,里面大概率用了RandomForestRegressor,我按这个思路写:
from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, r2_score features = ["high_lag1", "low_lag1", "high_lag2", "low_lag2", "weather_code_lag1", "temp_diff", "high_ma3"] X_train = df_train[features].values y_train = df_train["high"].values model = RandomForestRegressor( n_estimators=200, max_depth=8, min_samples_leaf=2, random_state=42 ) model.fit(X_train, y_train)4.2 训练与交叉验证
随机森林的超参数里,n_estimators从 100 加到 200 对精度提升有限,但会增加训练时间;max_depth限制树深度,可以防止模型死记硬背;min_samples_leaf保证叶子节点最少有 2 个样本,避免过拟合。但这几个值需要验证,不能拍脑袋。用sklearn的时间序列交叉验证,也就是按时间顺序切组:
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) scores = [] for train_idx, val_idx in tscv.split(df_train): X_tr, X_val = X_train[train_idx], X_train[val_idx] y_tr, y_val = y_train[train_idx], y_train[val_idx] model.fit(X_tr, y_tr) y_pred = model.predict(X_val) scores.append(mean_absolute_error(y_val, y_pred)) print("MAE 均值:", np.mean(scores), "±", np.std(scores))TimeSeriesSplit和普通KFold的区别在于它只允许用过去预测未来,不会把未来的数据泄露到训练集里。这点必须写进代码注释里,答辩老师一看到时间序列交叉验证,加分的概率很大。
4.3 可视化分析与结论呈现
模型训练完成后,除了输出指标,还要生成图表。项目里的可视化分析通常包含两个图:一个是真实温度 vs 预测温度的曲线对比,另一个是残差分布图。用 matplotlib 画图时,中文乱码是个大坑,必须设置字体:
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] plt.rcParams["axes.unicode_minus"] = False # 正常显示负号 plt.figure(figsize=(12, 4)) plt.plot(df_test["date"], df_test["high"], label="真实最高温") plt.plot(df_test["date"], y_pred, label="预测最高温", alpha=0.8) plt.legend() plt.title("测试集最高温预测对比") plt.xticks(rotation=45) plt.tight_layout() plt.savefig("weather_forecast_result.png", dpi=150)axes.unicode_minus设置成 False 是为了让负号正常渲染,否则坐标轴上的 -1 会显示成方块。残差分布图可以画出预测误差的直方图,并标注 MAE 值,这也是大作业报告里“分析”部分的核心。模型本身的评估指标建议用 MAE 而不是 R²:天气预测的绝对误差更直观,比如 MAE=1.8℃ 就代表平均偏差不到两度,比“准确率 92%”这种说法更专业。
5. 部署与答辩:main.py 的串联和常见坑
5.1 主流程编排
main.py 的作用是把上面三步串起来。不要让用户手动先执行三个脚本,而是通过if __name__ == "__main__"依次调用:
from GetData import crawl_weather from ProcessData import process_all from GetModel import train_and_evaluate if __name__ == "__main__": crawl_weather() # 1. 抓取最新数据 process_all() # 2. 清洗 + 构造特征 + 切分 train_and_evaluate() # 3. 训练 + 评估 + 可视化每个函数里要加print提示当前阶段和耗时,方便在控制台观察进度。项目里还有Model.pkl,这个文件是训练好的模型持久化结果,正式使用时不需要每次都重新训练。
5.2 模型持久化与加载
模型保存用joblib比pickle更快且对 numpy 数组兼容性更好:
import joblib joblib.dump(model, "Model.pkl") # 加载 model = joblib.load("Model.pkl") new_predict = model.predict(X_new)注意Model.pkl是从Model.pkl这个文件读,还是从GetModel.py里直接训练?项目里已经提供了Model.pkl,说明离线训练已完成。如果你的环境重装了包,记得重新跑 GetModel.py 生成一次。
5.3 答辩常问问题和调参建议
| 常见问题 | 回答要点 | 对应参数 |
|---|---|---|
| 为什么用随机森林不用线性回归? | 天气与历史特征存在非线性关系,随机森林能捕捉交互,且不需要归一化。 | max_depth=8 |
| 缺失值为什么填充而不是删除? | 删除会打断时间序列连续性,用前向填充更合理。 | fillna(method="ffill") |
| 测试集 MAE 比训练集高很多? | 过拟合,增大min_samples_leaf,或者增加特征high_ma3平滑波动。 | min_samples_leaf=4 |
| 换城市后模型还能用吗? | 不能直接复用,因为天气模式不同,需要重新训练,但代码流程可复用。 | date_range参数改掉 |
最后提一个部署时容易忽略的点:Model.pkl里的模型是用旧版本 sklearn 训练的,如果你本地是不同版本,加载时会出现“sklearn version mismatch”警告。解决方法是重新运行 GetModel.py,或者在保存前指定protocol版本。把请求头里的 User-Agent 换成你浏览器实际的字符串,可以避免不少爬虫阶段的 403 报错。
本文还有配套的精品资源,点击获取