简介:一份基于Python的天气预测与可视化毕业设计项目,配套源码、数据库、视频演示与文档说明,主要面向Python学习者、毕业设计学生以及需要快速搭建完整项目的开发者。压缩包共155个文件,约88.36MB,包含Python后端代码、前端网页与样式、数据配置与数据库脚本,以及演示视频和说明文档,覆盖网页、脚本、样式、JSON、SQL、视频等常见类型,目录结构清晰,便于按模块查阅。已有330人学习/下载。项目通过解析天气接口获取实时数据,结合常用请求、数据处理与可视化库完成分析预测,并可基于历史数据或简单机器学习模型给出预测结论;前端集成现成UI组件框架,支持地点输入、日期选择与趋势查看,以图表清楚呈现实时天气、温度变化及预测结果,整体覆盖数据采集、清洗分析到交互展示的完整流程。这套资源既可作为毕业设计直接参考,也是Python Web与数据可视化练手的好素材,能帮助读者快速理解天气数据获取、分析展示的全链路思路,节省从零搭建项目的时间。
1. 从「天气App不好用」说起:为什么自己动手做一套天气预测与可视化系统
做开发这几年,我越来越觉得现成的天气App有一个根本问题:它们给你看的是「编辑精选」后的信息,而不是你真正需要的数据。比如你做户外活动策划,想看过去两周的逐小时温度曲线;做农业项目,需要对比不同区域的降雨概率;这些需求在商业App里要么没有,要么藏在三级菜单里。所以当我想做一套自己的天气预测与可视化系统时,核心思路很明确——用 Python 把 OpenWeatherMap 的原始数据拉下来,存进本地数据库,再用 Pandas 做清洗和特征提取,最后用 Matplotlib 和 Flask 呈现成自己能控制的图表和页面。这套项目的技术栈很干净,没有复杂的框架依赖,却能覆盖 requests、SQLite、Pandas、Matplotlib 到 Flask 的完整数据链路,特别适合作为毕业设计展示完整的工程能力,也适合想深入理解数据流转的开发者参考。下面我把整个拆解过程写出来,每一步都有能直接复现的命令和代码。
2. 数据采集层:用 requests 把 OpenWeatherMap 的实时数据拿下来
2.1 为什么选 OpenWeatherMap 而不是其他气象接口
做天气数据采集,第一步是选数据源。国内的和风天气、心知天气接口文档都很友好,但它们的免费额度对毕业设计来说够用,对频繁开发调试却不够宽松,尤其是每分钟请求次数限制得很紧。我最后选 OpenWeatherMap 是因为它免费版就能提供实时天气、逐小时预测和 5 天预报,而且返回的 JSON 结构非常规整,适合教学演示和二次封装。
另一个选它的理由是免费层 API Key 的申请流程非常简单——注册邮箱就能拿到,不需要企业资质审核。对于课程设计或者毕业设计来说,这意味着从注册到拿到第一份数据,可以控制在 20 分钟以内,不会因为审批流程卡住进度。
如果你做的是国内城市级项目,而且对数据源有合规要求,也可以换成和风天气,但下面这套采集逻辑——requests 请求、参数拼接、响应解析、异常兜底——是完全通用的,只需要改 API 地址和字段名。
2.2 请求参数怎么设:城市、单位、语言与超时重试
采集代码最核心的部分是请求参数的构造。我一般会单独写一个weather_client.py,把所有跟外部接口打交道的逻辑收拢在这个文件里,方便后面替换数据源或者修改参数。
import requests import time from datetime import datetime class WeatherClient: def __init__(self, api_key, timeout=5, retry_times=3): self.base_url = "https://api.openweathermap.org/data/2.5/weather" self.api_key = api_key self.timeout = timeout # 单次请求超时时间(秒) self.retry_times = retry_times # 失败重试次数 def fetch_current_weather(self, city_name): payload = { "q": city_name, # 城市名,支持 "Beijing,CN" 带国家码 "appid": self.api_key, "units": "metric", # 返回摄氏温度;设为 imperial 则返回华氏 "lang": "zh_cn" # 返回中文天气描述,如“多云”“小雨” } for attempt in range(self.retry_times): try: resp = requests.get(self.base_url, params=payload, timeout=self.timeout) resp.raise_for_status() # 4xx/5xx 状态码直接抛异常 data = resp.json() return self._normalize(data) except requests.exceptions.Timeout: print(f"[{datetime.now()}] 请求超时,第 {attempt + 1} 次重试") time.sleep(2 * (attempt + 1)) # 指数退避:2s, 4s, 6s except requests.exceptions.HTTPError as e: # 常见错误码:401 Key 无效,404 城市不存在,429 限流 print(f"HTTP错误: {e}") if e.response.status_code == 404: raise ValueError(f"城市 {city_name} 不存在,请检查拼写") time.sleep(10) # 限流或 Key 问题,等久一点再试 raise RuntimeError(f"请求 {city_name} 失败,已重试 {self.retry_times} 次") def _normalize(self, raw_data): return { "city": raw_data["name"], "temp": raw_data["main"]["temp"], "humidity": raw_data["main"]["humidity"], "pressure": raw_data["main"]["pressure"], "weather": raw_data["weather"][0]["description"], "wind_speed": raw_data["wind"]["speed"], "timestamp": int(time.time()) }这里有几个参数值得单独说明。units=metric是我强烈建议加上去的,不写这个参数时 OpenWeatherMap 默认返回开尔文温度,很多新手第一次拿到数据会发现温度是 300 多,就是因为没设单位。lang=zh_cn控制天气描述的语言,如果你后续要做图表标题,用中文会更直观。timeout参数一定要设,否则 requests 默认无限等待,在校园网环境里很容易卡死整个采集脚本。重试机制里我做了 2 倍递增的退避策略,第一次失败等 2 秒,第二次等 4 秒,这样既不会在瞬时抖动时误判,也不会因为高频重试被封 Key。
2.3 响应结构解析与异常兜底
拿到 JSON 之后,不能直接往数据库里塞,必经一步是字段抽取。OpenWeatherMap 的响应结构比较深,例如实时天气的完整路径是data["main"]["temp"],而不是data["temp"]。_normalize方法在这里起到的作用是:把嵌套的 JSON 拍平成一条扁平的记录,同时过滤掉我们不需要的字段,比如经纬度、时区偏移量、云量百分比等。
异常兜底方面,除了代码里的Timeout和HTTPError,还有一个常见坑是KeyError——有时候接口返回的数据里没有main字段,比如城市名拼错时返回的是{ "cod": "404", "message": "city not found" }。所以我在项目里会再加一层判断:
if "main" not in raw_data: raise ValueError(f"接口返回异常: {raw_data.get('message', '未知错误')}")这部分做好之后,数据采集层就稳定了。下一步要考虑的是:采集到的数据不能只留在内存里,要有一个可长期累积的存储方案。
3. 数据仓库与特征工程:从 JSON 到 SQLite 再到 Pandas
3.1 为什么要自建 SQLite 存储而不过度依赖 API
如果你只是做完一个页面就交差,可以直接在 Flask 路由里写requests.get实时拉数据。但真实场景不是这样:免费 API 有调用次数上限,而且你需要历史数据来画趋势图和做预测模型。所以我在系统里加了一个 SQLite 存储层。
选 SQLite 而不是 MySQL 的原因很直接——毕业设计和课程设计阶段没有服务器运维需求,SQLite 单文件、零配置、支持标准 SQL,Python 标准库直接import sqlite3就能用,不需要额外装驱动。等数据量到了几百万条,再平滑迁移到 PostgreSQL 也不迟,而且 SQL 语句基本不用改。
为了更好地衔接后续的 Pandas 分析和可视化,我建议在存储层就把时间字段和城市字段做成规范格式,不要存乱七八糟的字符串。下面是建表语句和入库逻辑。
3.2 表结构设计与入库脚本
-- schema.sql CREATE TABLE IF NOT EXISTS weather_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, city TEXT NOT NULL, -- 城市名,如 "Beijing" temp REAL NOT NULL, -- 温度(摄氏度) humidity INTEGER, -- 相对湿度(%) pressure INTEGER, -- 气压(hPa) weather TEXT, -- 天气描述,如 "多云" wind_speed REAL, -- 风速(m/s) record_time INTEGER NOT NULL -- Unix 时间戳,秒级 ); CREATE INDEX IF NOT EXISTS idx_city_time ON weather_data(city, record_time);建索引是很多人容易忽略的一步。后续做时序分析时要反复按城市和时间范围过滤数据,没有索引的话数据量稍大就会明显变慢。idx_city_time这个联合索引把两个高频查询字段都覆盖了。
入库脚本沿着上一章的采集流程往后接,在_normalize拿到干净字典之后,写入 SQLite:
import sqlite3 def save_to_db(record, db_path="weather.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute( """ INSERT INTO weather_data (city, temp, humidity, pressure, weather, wind_speed, record_time) VALUES (:city, :temp, :humidity, :pressure, :weather, :wind_speed, :timestamp) """, record ) conn.commit() conn.close()这段逻辑里用了:city这种命名占位符,而不是 Python 的%格式化,这样做有两点作用:第一,sqlite3 自带参数转义功能,可以避免 SQL 注入问题;第二,字典直接传参,不用再维护一个位置对应的元组数组,减少字段顺序出错的可能。另一个值得注意的细节是conn.commit()的位置——写完一条就提交,虽然吞吐量不高,但胜在稳妥,毕竟采集脚本中间崩溃的概率远大于单条写入的性能差距。
3.3 用 Pandas 做特征提取:均值、趋势、缺失值处理
数据落库之后,分析和特征提取的工作就交给 Pandas。这一层要做的事情,是把原始的逐条观测数据聚合成我们画图时的输入,比如日平均温度、温度标准差、逐周趋势线等。
import pandas as pd import sqlite3 def load_weather_df(db_path="weather.db", city="Beijing", days=7): conn = sqlite3.connect(db_path) query = """ SELECT city, temp, humidity, record_time FROM weather_data WHERE city = ? AND record_time >= ? ORDER BY record_time """ start_ts = int(pd.Timestamp.now().timestamp()) - days * 86400 df = pd.read_sql_query(query, conn, params=(city, start_ts)) conn.close() df["datetime"] = pd.to_datetime(df["record_time"], unit="s") df.set_index("datetime", inplace=True) return df def daily_aggregation(df): daily = df.resample("D").agg({ "temp": ["mean", "max", "min"], "humidity": "mean" }) daily.columns = ["temp_mean", "temp_max", "temp_min", "humidity_mean"] return daily.dropna()pd.read_sql_query可以直接把 SQL 查询结果转为 DataFrame,省去手动遍历游标的步骤。这里有个细节:params=(city, start_ts)传参和 sqlite3 原生写法保持一致,注意必须是元组,不能是简单字符串。resample("D")是按自然日聚合,同时计算均值、最大值、最小值,这些统计量后面做可视化的时候正好对应温度曲线的高低点展示。
缺失值处理也是特征工程里躲不开的问题——比如某天接口限流没有采到数据,resample之后那一天的记录就是 NaN,直接画图会在曲线上留下一个断点。我通常的做法是先用上一章数据库里的最近一条记录回填,实在没有就去拉一次历史记录补齐。Pandas 里最简单的回填方式是:
daily = daily.ffill() # 用上一个有效值填充缺失如果缺失值跨度超过三天,ffill就没有意义了,这时我会在图上直接标注「数据缺失区间」,而不是硬画一条平滑曲线骗自己。分析层的原则就是不造假数据,宁可让用户看到缺口,也不能推断出不存在的趋势。
4. 可视化层:Matplotlib 静态图 + Flask 页面展示
4.1 图表设计:温度曲线、降雨概率柱状、风向玫瑰
数据准备好了之后,下一步是把它们变成人能直接看懂的图。可视化这部分我在系统里做了三种图表,各自的适用场景和代码细节我拆开说。
第一种是温度曲线图,展示近 7 天或近 30 天的温度变化趋势。虽然称为曲线图,但我通常把最高温和最低温用两条线画出来,中间用fill_between填充浅色区域,这样一眼就能看出温差范围,比单条平均温度线信息量丰富得多。
import matplotlib.pyplot as plt import matplotlib.dates as mdates plt.rcParams["font.sans-serif"] = ["SimHei"] # 解决中文乱码 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示为方块 fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(daily.index, daily["temp_max"], label="最高温", color="#d9534f", linewidth=1.8) ax.plot(daily.index, daily["temp_min"], label="最低温", color="#337ab7", linewidth=1.8) ax.fill_between(daily.index, daily["temp_min"], daily["temp_max"], alpha=0.15, color="gray") ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d")) ax.set_xlabel("日期") ax.set_ylabel("温度 (°C)") ax.set_title(f"{city} 近 {days} 天温度变化") ax.legend() ax.grid(True, linestyle="--", alpha=0.4) plt.xticks(rotation=45) plt.tight_layout() plt.savefig(f"static/plots/{city}_temp.png", dpi=120) plt.close()这套配置里有几个容易踩坑的地方。中文字体必须设置SimHei或Microsoft YaHei,否则 Matplotlib 默认字体不支持中文,横轴标签在 Linux 服务器上会显示成一堆方块。axes.unicode_minus也一样,如果不设成False,负温度显示出来是乱码。savefig之后必须plt.close(),否则每跑一次就新增一个 figure 对象,在 Flask 里跑时间长了会内存泄漏——这个问题在开发环境不容易察觉,部署后跑几天就会把内存吃满。
第二种是降雨概率柱状图,适合近 12 小时或近 3 天的逐段降水概率展示。柱状图的逻辑简单,但有一个设计原则要注意:如果所有小时段的降雨概率都是 0,图表会变成一条水平线,这时我会在图上额外标注「近 48 小时无降水」,让看的人不用盯着坐标轴猜到底有没有雨。
4.2 动态画像:图表刷新策略与缓存处理
图表不能每次都重新生成,那样很耗资源。做毕业设计时老师问得最多的一个问题就是「你如何保证数据实时性」,这里我建议把「实时」拆成两个层面:页面访问时是实时的,但底层的图表文件是按频率刷新的。也就是说,采集脚本定时把新数据写入 SQLite 并重新生成图表文件,页面加载时只读已生成好的 PNG。这样把重活挪到后台,前端响应飞快,还能避免高并发场景下 Matplotlib 线程不安全的问题。
from flask import Flask, render_template from apscheduler.schedulers.background import BackgroundScheduler app = Flask(__name__) def scheduled_job(): # 拉取天气 -> 存库 -> 生成图表 cities = ["Beijing", "Shanghai", "Guangzhou"] for city in cities: client = WeatherClient(api_key) record = client.fetch_current_weather(city) save_to_db(record) generate_all_plots(cities) # 封装所有画图逻辑 scheduler = BackgroundScheduler() scheduler.add_job(scheduled_job, "interval", hours=1) # 每小时整点拉取 scheduler.start()这里选 APScheduler 而不是自己写 while 循环,是因为它自带了 misfire 处理机制——如果某个任务因为网络卡顿超过预定时间,它可以自动补跑或者跳过,不会像time.sleep那样越跑越偏。hours=1是综合考虑后的结果:免费 API 的调用额度一般是每分钟 60 次,每小时拉一次远在限额内;而且一小时内的天气变化对可视化展示来说并不明显,没必要每分钟刷新。
页面端用 Flask 的路由渲染模板,图表文件路径作为变量传给 HTML:
@app.route("/") def index(): return render_template("index.html", city="Beijing", temp_plot="/static/plots/Beijing_temp.png", rain_plot="/static/plots/Beijing_rain.png")4.3 页面渲染:Flask 路由与数据库联动
模板层面我用了从开源后台模板中拆下来的 UI 框架,比如layui.css、font-awesome.css、layuimini.css这一类现成的样式资源。这里我一般不做页面静态化,会保留一个简单的刷新按钮,让用户主动触发最新数据。每次点击刷新时,Flask 这边不重新生成图表,而是返回当前数据库里最新的记录,并在页面上标注「数据更新于 HH:MM」,这样用户能直观地看到系统在正常工作。
页面布局上我推荐一个「总览卡 + 明细表」的结构:第一行放城市、温度、湿度、风速、天气描述这几张信息卡片,第二行放温度曲线图,第三行放降雨概率柱状图再加一个最近 7 天记录表格。表格数据直接从数据库翻出来,不用走 Matplotlib,效率高很多:
@app.route("/api/weather/<city>") def api_weather(city): conn = sqlite3.connect("weather.db") df = pd.read_sql_query("SELECT * FROM weather_data WHERE city = ? ORDER BY record_time DESC LIMIT 10", conn, params=(city,)) conn.close() return df.to_json(orient="records", force_ascii=False)orient="records"会输出 JSON 数组格式,和前端fetch之后直接JSON.parse完美对应。force_ascii=False让中文城市名和天气描述在 JSON 响应里不被转义成\uXXXX,否则前端还要多做一步解码,而且排查问题时看着很费劲。
5. 进阶:用线性回归自建温度预测模型,并给出量化验证
这一章我在原项目基础上再往前走一步,把「天气预测」从口号变成可运行的代码。不用机器学习框架,就用最朴素的线性回归来预测明天的最高温度,让老师或面试官看到你不只会画图,还理解预测的本质。这里用到的库只有scikit-learn,相比 TensorFlow 或 PyTorch,它的依赖更轻,安装也更省心。
预测的思路:用过去 7 天的最高温和最低温作为特征,预测明天的最高温。训练数据从 SQLite 里按时间顺序取出最近 60 天记录,随机分割出 80% 做训练集、20% 做验证集。关键代码我拆成特征构造和模型训练两段。
from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error import numpy as np def build_features(df): X, y = [], [] temps = df["temp"].values for i in range(7, len(temps)): X.append(temps[i-7:i]) # 前 7 天的温度序列 y.append(temps[i]) # 第 8 天的温度 return np.array(X), np.array(y) X, y = build_features(df) # df 从 load_weather_df 得到 split = int(len(X) * 0.8) model = LinearRegression() model.fit(X[:split], y[:split])build_features里做的事情本质上是把时间序列转成监督学习格式:每条样本的输入是连续 7 天的温度,输出是第 8 天的温度。这种方式叫滑动窗口,在时间序列预测里是基础但管用的特征构造手段。窗口大小定为 7 是为了匹配自然周的周期——温度的预测上,一周前的温度比一个月前的温度信息量更大。
模型训练好之后,用验证集算误差:
y_pred = model.predict(X[split:]) mae = mean_absolute_error(y[split:], y_pred) print(f"验证集平均绝对误差: {mae:.2f} °C")平均绝对误差在 2°C 以内说明模型可用,超过 3°C 就说明特征不够或者数据量太小。我实际跑下来发现,夏天预测误差一般比冬天小,因为冬季温度受冷空气活动影响,单靠历史温度序列确实难以准确捕捉。这个结论本身就是你好的一段分析素材——说明你不只会调库,还理解模型在什么场景下失效。
如果你想把准确率再往上推一档,一个不用改模型的做法是增加特征维度:把湿度、气压、风速也加进X序列,让每个时刻的特征从一个数变成四个数。
def build_multi_feature(df): feature_cols = ["temp", "humidity", "pressure", "wind_speed"] X, y = [], [] values = df[feature_cols].values for i in range(7, len(values)): X.append(values[i-7:i].flatten()) # 展平成 7*4=28 维向量 y.append(values[i, 0]) # 只预测温度 return np.array(X), np.array(y)flatten()的作用是把 7 天乘 4 个特征的两维数组展平成 28 个元素的向量,让LinearRegression能正常工作,这一步在一些旧教程里经常漏掉,漏掉后会直接报维度相关错误。
验证完模型之后,还需要把预测结果接回原来的可视化系统——我一般会在 Flask 的/api/weather/<city>接口里增加一个tomorrow_temp字段,前端模板里加一张「明日温度预报」卡片,把模型的预测值和当前实测温度并列展示。卡片下方放一行小字「基于历史数据的线性回归预测,误差通常在 ±2°C」,这行注释我建议保留,它展示了你对模型边界的认知,比单纯写「预测准确」可信得多。整个系统的闭环到这里就完成了,你可以尝试连续运行一周验证模型预测误差的稳定性。
本文还有配套的精品资源,点击获取