Windy实战:从入门到精通的气象数据项目搭建
看了一堆教程还是不会写项目?别急,很多人卡在“懂代码”和“能落地”的中间地带。今天咱们不玩虚的,直接上手一个基于Windy气象数据API的实战项目,带你从入门到精通,把天气数据变成可交互的Web应用。
项目目标与痛点解析
很多转岗开发者都遇到过这种尴尬:Python能写、Java会敲,但真让你做个完整项目,脑子一片空白。问题出在哪?缺的不是语法,而是工程化思维和数据流设计。Windy(原Windy.com)作为全球知名的气象可视化平台,其背后有一套成熟的数据接口体系。我们今天要做的,就是调用Windy公开的气象数据源,构建一个轻量级的天气监控面板。
项目目标很明确:
- 数据获取:实时拉取指定坐标的风速、温度、降水概率。
- 前端展示:用HTML+CSS+JS渲染动态数据卡片。
- 后端支撑:用Python Flask搭建API中转层,处理缓存与鉴权。
- 工程化落地:完整的目录结构、错误处理、日志记录。
为什么选Windy?因为它的开发者文档(Windy API Documentation)结构清晰,字段定义准确,非常适合用来练习RESTful API的规范对接。很多教程只教你“怎么调接口”,但不会教你“怎么优雅地处理接口返回的JSON结构”,这正是我们今天要补的课。
目录结构设计
一个可维护的项目,目录结构就是它的骨架。咱们不搞花里胡哨,采用标准分层架构:
windy-weather/
├── app/
│ ├── __init__.py # 应用工厂
│ ├── config.py # 配置管理
│ ├── models/
│ │ └── weather.py # 数据模型
│ ├── routes/
│ │ └── api.py # API路由
│ └── services/
│ └── windy_service.py # Windy数据服务
├── static/
│ ├── css/
│ │ └── style.css # 样式
│ └── js/
│ └── main.js # 前端逻辑
├── templates/
│ └── index.html # 主页模板
├── requirements.txt # 依赖
└── run.py # 入口
为什么这么分?
services层:隔离外部依赖。如果明天换用OpenWeatherMap,只改这一个文件,不用动路由和模型。config层:环境配置分离。开发用测试Key,生产用正式Key,避免硬编码。static与templates分离:前端资源独立,方便后续接入Webpack或Vite。
转岗者常犯的错误是:所有逻辑堆在routes里。一旦业务变复杂,代码就变成“意大利面”。记住:单一职责原则不是空话,是每个文件的代码量不该超过200行。
核心代码实现
后端:Windy数据服务层
先看最关键的部分——数据获取。Windy的公开接口有速率限制,我们需要做缓存和超时控制。
# app/services/windy_service.py
import requests
from datetime import datetime, timedelta
from app.config import settingsclass WindyService:def __init__(self):self.base_url = "https://api.windy.com/webapi/forecast"self.timeout = 5 # 秒self.cache = {} # 简单内存缓存,生产环境用Redisself.cache_ttl = 300 # 5分钟缓存def get_weather(self, lat: float, lon: float) -> dict:"""获取指定坐标的天气数据:param lat: 纬度:param lon: 经度:return: 标准化天气数据"""# 1. 检查缓存cache_key = f"{lat}_{lon}"now = datetime.now()if cache_key in self.cache:data, cached_time = self.cache[cache_key]if now - cached_time < timedelta(seconds=self.cache_ttl):return data # 命中缓存,直接返回# 2. 构造请求参数params = {"latitude": lat,"longitude": lon,"product": "wind", # 产品类型:wind/temp/precip"forecast": "forecast", # 预报类型}# 3. 发起请求,带超时和异常处理try:response = requests.get(self.base_url,params=params,timeout=self.timeout,headers={"User-Agent": "WindyWeatherApp/1.0"})response.raise_for_status() # 400+状态码抛异常data = response.json()except requests.exceptions.Timeout:raise Exception("Windy API超时,请稍后重试")except requests.exceptions.HTTPError as e:raise Exception(f"Windy API错误: {e}")except ValueError:raise Exception("返回数据格式错误,非JSON")# 4. 数据标准化(关键步骤!)normalized_data = self._normalize(data)# 5. 写入缓存self.cache[cache_key] = (normalized_data, now)return normalized_datadef _normalize(self, raw_data: dict) -> dict:"""将Windy原始数据转换为前端友好的格式这一步是避免“前端拿到数据不会用”的关键"""if "error" in raw_data:raise Exception(f"Windy返回错误: {raw_data['error']}")# 假设Windy返回结构为 {wind: [{speed, dir}, ...]}wind_data = raw_data.get("wind", [{}])[0]return {"speed_ms": float(wind_data.get("speed", 0)),"direction": int(wind_data.get("dir", 0)),"timestamp": datetime.now().isoformat(),"source": "windy"}
逐行讲解重点:
raise_for_status():很多人忽略这个,导致HTTP 404/500被当成成功响应处理。_normalize方法:这是工程化的核心。外部API的字段名、单位、结构可能随时变,我们在服务层做一层“适配器”,前端只认我们的标准化格式。- 缓存策略:Windy免费版有速率限制(通常每分钟10次),不加缓存,用户多点几次页面就封IP了。
后端:API路由层
路由层要薄,只做参数校验和响应包装。
# app/routes/api.py
from flask import Blueprint, request, jsonify
from app.services.windy_service import WindyService
import logginglogger = logging.getLogger(__name__)
api_bp = Blueprint('api', __name__)
windy_svc = WindyService()@api_bp.route('/api/weather', methods=['GET'])
def get_weather():"""获取天气数据Query参数: lat(必填), lon(必填)"""# 1. 参数校验lat = request.args.get('lat', type=float)lon = request.args.get('lon', type=float)if lat is None or lon is None:return jsonify({"error": "缺少lat或lon参数"}), 400if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):return jsonify({"error": "坐标超出有效范围"}), 400# 2. 调用服务层try:data = windy_svc.get_weather(lat, lon)return jsonify({"code": 0, "data": data}), 200except Exception as e:logger.error(f"获取天气失败: {str(e)}")return jsonify({"code": 500, "error": str(e)}), 500
注意:这里没有直接返回windy_svc的原始异常,而是包装成统一的{code, error}格式。前端只需判断code字段,不用关心后端是数据库挂了还是API超时。
前端:动态数据渲染
前端不用框架,原生JS足够。重点是异步加载和错误提示。
// static/js/main.js
document.addEventListener('DOMContentLoaded', () => {const weatherCard = document.getElementById('weather-card');const statusText = document.getElementById('status');// 1. 获取坐标(这里简化,实际用Geolocation API)const lat = 39.9042; // 北京const lon = 116.4074;// 2. 发起请求fetch(`/api/weather?lat=${lat}&lon=${lon}`).then(response => {if (!response.ok) {throw new Error(`HTTP错误: ${response.status}`);}return response.json();}).then(result => {if (result.code !== 0) {throw new Error(result.error || "未知错误");}renderWeather(result.data);}).catch(err => {statusText.textContent = `加载失败: ${err.message}`;statusText.classList.add('error');});// 3. 渲染函数function renderWeather(data) {const speed = (data.speed_ms * 3.6).toFixed(1); // m/s转km/hconst direction = data.direction;weatherCard.innerHTML = `<div class="temp">${speed} km/h</div><div class="direction">风向: ${direction}°</div><div class="update">更新于: ${new Date(data.timestamp).toLocaleTimeString()}</div>`;statusText.textContent = '数据已更新';statusText.classList.remove('error');}
});
关键点:
response.ok检查:HTTP 200不代表业务成功,必须结合code字段。- 单位转换:Windy返回的是m/s,前端展示更习惯km/h,在渲染层转换,不污染后端数据。
- 错误提示:用户看到“加载失败: HTTP错误: 500”比看到白屏友好得多。
运行与测试
环境准备
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate# 安装依赖
pip install -r requirements.txt
requirements.txt内容:
Flask==2.3.3
requests==2.31.0
启动与测试
python run.py
打开浏览器访问http://localhost:5000,你应该能看到风速和风向数据。
测试要点:
- 正常场景:输入有效坐标,数据正常显示。
- 边界场景:输入
lat=91,应返回400错误。 - 异常场景:断网运行,应显示“Windy API超时”。
- 缓存验证:连续请求同一坐标,第二次响应时间应明显缩短(缓存命中)。
常见问题排查:
- CORS错误:如果前后端分离部署,需在Flask配置
CORS扩展。 - JSON解析失败:用Postman先调后端API,确认返回格式正确,再查前端。
- 缓存不生效:检查
cache_key是否一致,timedelta比较逻辑是否正确。
优化扩展方向
项目跑通只是开始,真正的入门到精通体现在可扩展性上。
1. 缓存升级:Redis替换内存缓存
当前用dict做缓存,重启服务就丢失。生产环境必须用Redis:
# app/config.py
class Config:REDIS_URL = "redis://localhost:6379/0"CACHE_TTL = 300
# app/services/windy_service.py
import redis
from app.config import settingsclass WindyService:def __init__(self):self.redis = redis.from_url(settings.REDIS_URL)# ... 其他代码不变def get_weather(self, lat, lon):cache_key = f"windy:{lat}:{lon}"cached = self.redis.get(cache_key)if cached:return json.loads(cached)# ... 请求逻辑self.redis.setex(cache_key, settings.CACHE_TTL, json.dumps(normalized_data))return normalized_data
2. 数据持久化:记录历史趋势
加一个SQLite,存储每次查询的结果,后续可做“风速变化曲线”。
# app/models/weather.py
from flask_sqlalchemy import SQLAlchemy
from datetime import datetimedb = SQLAlchemy()class WeatherRecord(db.Model):id = db.Column(db.Integer, primary_key=True)lat = db.Column(db.Float, nullable=False)lon = db.Column(db.Float, nullable=False)speed_ms = db.Column(db.Float, nullable=False)timestamp = db.Column(db.DateTime, default=datetime.utcnow)
3. 前端增强:地图可视化
引入Leaflet.js,把数据点标在地图上,点击标记显示详情。这是转岗者最容易被问到的“前端+后端”结合场景。
4. 监控与告警
用prometheus-client暴露指标:API调用次数、平均响应时间、错误率。接入Grafana监控,生产环境必备。
避坑提醒:
- 不要过度设计:MVP阶段用内存缓存+SQLite足够,别一上来就上Kafka、K8s。
- 日志规范:所有
except块必须logger.error,否则线上问题无从查起。 - 版本管理:
requirements.txt必须锁定版本号,避免依赖漂移。
小结
这个项目不大,但覆盖了数据获取、缓存、异常处理、前后端联调、工程化结构等核心能力。很多教程教你“怎么调API”,但很少教你“怎么把API调用的数据,稳定地展示给用户”。
Windy的开发者文档里,每个字段都有精确的单位和说明,但文档不会告诉你:
- 如何处理API突然返回
null字段? - 如何在高并发下避免被限流?
- 前端拿到数据后,如何优雅地降级展示?
这些“灰色地带”的能力,才是区分“会写代码”和“能写项目”的关键。转岗者最缺的,不是语法知识,而是这种把零散知识点串成完整链路的工程直觉。
这个知识点你面试被问过吗?留言说说