1. 项目概述:通过和风天气API获取实时天气数据
在开发天气类应用或需要天气数据的项目时,直接调用专业的天气API是最可靠高效的解决方案。和风天气作为国内领先的气象数据服务商,提供了稳定可靠的天气数据接口。本项目将使用Python语言,通过和风天气的API实现实时天气数据的获取功能。
实时天气数据在很多场景下都非常有用,比如:
- 出行类APP的天气提醒功能
- 智能家居系统的天气联动控制
- 农业监测系统的气象数据采集
- 运动健康类APP的户外活动建议
使用API获取天气数据相比自己搭建气象站或爬取网页数据,具有数据准确、更新及时、接口稳定等优势。和风天气API提供了全球范围内的天气数据,包括实时天气、天气预报、空气质量等多种气象信息。
2. 准备工作:获取API访问权限
2.1 注册和风天气开发者账号
首先需要访问和风天气开发者平台注册账号。注册过程简单,只需提供邮箱和设置密码即可。注册完成后,登录开发者控制台。
提示:建议使用企业邮箱注册,个人项目也可以使用常用邮箱,但要注意保管好账号信息。
2.2 创建项目获取凭证
在控制台中创建一个新项目,项目类型根据实际需求选择。创建完成后,系统会分配一个唯一的项目ID(Project ID),这是后续API调用中必须的参数。
和风天气提供两种身份认证方式:
- JWT(JSON Web Token)认证 - 安全性更高,推荐使用
- API Key认证 - 简单易用但安全性较低
考虑到安全性,本教程将使用JWT认证方式。需要在项目中添加JWT凭据:
- 生成Ed25519密钥对
- 将公钥上传到和风天气控制台
- 保存好私钥用于本地签名
2.3 安装必要的Python库
在开始编码前,需要安装以下Python库:
bash复制pip install requests pyjwt cryptography
这些库的作用分别是:
requests:用于发送HTTP请求pyjwt:用于生成和验证JWTcryptography:提供加密算法支持
3. 实现JWT认证机制
3.1 生成Ed25519密钥对
JWT认证需要使用Ed25519算法生成的密钥对。可以通过以下方式生成:
python复制from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
# 生成私钥
private_key = ed25519.Ed25519PrivateKey.generate()
# 获取私钥的PEM格式
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
# 生成公钥
public_key = private_key.public_key()
# 获取公钥的PEM格式
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# 保存密钥到文件
with open('ed25519-private.pem', 'wb') as f:
f.write(private_pem)
with open('ed25519-public.pem', 'wb') as f:
f.write(public_pem)
生成后,将公钥内容上传到和风天气控制台的凭据管理中。
3.2 构建JWT Token
JWT由三部分组成:Header、Payload和Signature。以下是构建JWT的Python实现:
python复制import jwt
import time
def generate_jwt_token(private_key_path, kid, project_id):
# 读取私钥
with open(private_key_path, 'rb') as f:
private_key = f.read()
# 设置Header
headers = {
'alg': 'EdDSA',
'kid': kid # 控制台中凭据的ID
}
# 设置Payload
current_time = int(time.time())
payload = {
'sub': project_id, # 项目ID
'iat': current_time - 30, # 签发时间(提前30秒防止时钟偏差)
'exp': current_time + 900 # 过期时间(15分钟后)
}
# 生成JWT
token = jwt.encode(
payload,
private_key,
algorithm='EdDSA',
headers=headers
)
return token
注意事项:
- 私钥必须妥善保管,不要上传到代码仓库
- JWT的过期时间(exp)不宜设置过长,建议15-30分钟
- 签发时间(iat)建议设置为当前时间减去30秒,防止服务器时间不同步
3.3 使用JWT调用API
生成JWT后,可以将其添加到HTTP请求的Authorization头中:
python复制import requests
def get_weather_data(token, location_id):
url = f'https://api.qweather.com/v7/weather/now?location={location_id}'
headers = {
'Authorization': f'Bearer {token}',
'Accept-Encoding': 'gzip' # 启用压缩减少数据传输量
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
raise Exception(f'API请求失败: {response.status_code} - {response.text}')
4. 获取并解析天气数据
4.1 查询实时天气
和风天气API提供了多个天气相关的接口,获取实时天气使用的是/v7/weather/now接口。需要传入位置ID作为参数。
位置ID是和风天气定义的地理位置编码,可以通过以下方式获取:
- 使用和风天气的城市搜索API查询
- 在控制台的"常见城市列表"中查找
- 使用GeoAPI转换经纬度为位置ID
示例调用:
python复制# 北京的位置ID是101010100
location_id = '101010100'
token = generate_jwt_token('ed25519-private.pem', 'your_kid', 'your_project_id')
weather_data = get_weather_data(token, location_id)
4.2 解析返回数据
API返回的JSON数据包含丰富的天气信息,主要字段包括:
python复制{
"code": "200",
"updateTime": "2023-05-20T14:30+08:00",
"fxLink": "http://hfx.link/2ax1",
"now": {
"obsTime": "2023-05-20T14:25+08:00",
"temp": "24",
"feelsLike": "26",
"icon": "101",
"text": "多云",
"wind360": "135",
"windDir": "东南风",
"windScale": "3",
"windSpeed": "12",
"humidity": "65",
"precip": "0.0",
"pressure": "1005",
"vis": "10",
"cloud": "75",
"dew": "18"
},
"refer": {
"sources": ["QWeather"],
"license": ["commercial license"]
}
}
可以编写一个解析函数提取关键信息:
python复制def parse_weather_data(data):
if data['code'] != '200':
raise Exception('API返回错误')
now = data['now']
return {
'更新时间': data['updateTime'],
'温度': f"{now['temp']}℃",
'体感温度': f"{now['feelsLike']}℃",
'天气状况': now['text'],
'风力': f"{now['windDir']}{now['windScale']}级",
'风速': f"{now['windSpeed']}km/h",
'湿度': f"{now['humidity']}%",
'降水量': f"{now['precip']}mm",
'能见度': f"{now['vis']}公里"
}
5. 完整实现与优化
5.1 完整的天气查询类
将上述功能封装成一个完整的类,方便复用:
python复制import jwt
import time
import requests
from pathlib import Path
class QWeatherAPI:
def __init__(self, private_key_path, kid, project_id):
self.private_key_path = private_key_path
self.kid = kid
self.project_id = project_id
self.base_url = 'https://api.qweather.com/v7'
# 验证私钥文件是否存在
if not Path(private_key_path).exists():
raise FileNotFoundError(f'私钥文件 {private_key_path} 不存在')
def _generate_token(self):
"""生成JWT Token"""
with open(self.private_key_path, 'rb') as f:
private_key = f.read()
current_time = int(time.time())
payload = {
'sub': self.project_id,
'iat': current_time - 30,
'exp': current_time + 900
}
headers = {
'alg': 'EdDSA',
'kid': self.kid
}
return jwt.encode(
payload,
private_key,
algorithm='EdDSA',
headers=headers
)
def _make_request(self, endpoint, params=None):
"""发送API请求"""
if params is None:
params = {}
url = f'{self.base_url}/{endpoint}'
token = self._generate_token()
headers = {
'Authorization': f'Bearer {token}',
'Accept-Encoding': 'gzip'
}
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
return response.json()
else:
raise Exception(f'API请求失败: {response.status_code} - {response.text}')
def get_real_time_weather(self, location_id):
"""获取实时天气数据"""
endpoint = 'weather/now'
params = {'location': location_id}
return self._make_request(endpoint, params)
def parse_weather_data(self, data):
"""解析天气数据"""
if data['code'] != '200':
raise Exception('API返回错误')
now = data['now']
return {
'更新时间': data['updateTime'],
'温度': f"{now['temp']}℃",
'体感温度': f"{now['feelsLike']}℃",
'天气状况': now['text'],
'风力': f"{now['windDir']}{now['windScale']}级",
'风速': f"{now['windSpeed']}km/h",
'湿度': f"{now['humidity']}%",
'降水量': f"{now['precip']}mm",
'能见度': f"{now['vis']}公里"
}
5.2 使用示例
python复制# 初始化
weather_api = QWeatherAPI(
private_key_path='path/to/ed25519-private.pem',
kid='your_kid',
project_id='your_project_id'
)
# 查询天气
location_id = '101010100' # 北京
weather_data = weather_api.get_real_time_weather(location_id)
# 解析并打印结果
parsed_data = weather_api.parse_weather_data(weather_data)
for key, value in parsed_data.items():
print(f"{key}: {value}")
5.3 性能优化与错误处理
在实际应用中,还需要考虑以下优化点:
- Token缓存:JWT Token在有效期内可以重复使用,不需要每次请求都重新生成
python复制def __init__(self, private_key_path, kid, project_id):
self._cached_token = None
self._token_expire_time = 0
def _generate_token(self):
"""带缓存的Token生成"""
current_time = time.time()
if self._cached_token and current_time < self._token_expire_time - 60:
return self._cached_token
# 重新生成Token
with open(self.private_key_path, 'rb') as f:
private_key = f.read()
payload = {
'sub': self.project_id,
'iat': int(current_time) - 30,
'exp': int(current_time) + 900
}
headers = {'alg': 'EdDSA', 'kid': self.kid}
self._cached_token = jwt.encode(
payload,
private_key,
algorithm='EdDSA',
headers=headers
)
self._token_expire_time = payload['exp']
return self._cached_token
- 请求重试机制:网络请求可能会失败,添加重试逻辑提高稳定性
python复制def _make_request(self, endpoint, params=None, max_retries=3):
"""带重试机制的请求"""
if params is None:
params = {}
url = f'{self.base_url}/{endpoint}'
token = self._generate_token()
headers = {
'Authorization': f'Bearer {token}',
'Accept-Encoding': 'gzip'
}
last_exception = None
for attempt in range(max_retries):
try:
response = requests.get(url, headers=headers, params=params, timeout=10)
if response.status_code == 200:
return response.json()
elif response.status_code == 401: # Token过期
self._cached_token = None # 强制刷新Token
continue
else:
raise Exception(f'API返回错误: {response.status_code}')
except Exception as e:
last_exception = e
time.sleep(1 * (attempt + 1)) # 指数退避
raise Exception(f'请求失败,重试{max_retries}次后仍然错误: {str(last_exception)}')
- 数据缓存:天气数据不需要实时更新,可以缓存一段时间
python复制from functools import lru_cache
import time
@lru_cache(maxsize=32)
def get_cached_weather(self, location_id, ttl=600):
"""带缓存的天气查询,默认缓存10分钟"""
return self.get_real_time_weather(location_id)
6. 常见问题与解决方案
6.1 JWT生成失败
问题现象:生成JWT时出现jwt.exceptions.InvalidKeyError错误
可能原因:
- 私钥格式不正确
- 私钥文件损坏
- 使用了错误的算法
解决方案:
- 确认私钥文件内容完整,以
-----BEGIN PRIVATE KEY-----开头 - 重新生成密钥对并更新控制台中的公钥
- 确保代码中指定了正确的算法
EdDSA
6.2 API返回401未授权
问题现象:API返回401 Unauthorized错误
可能原因:
- JWT Token过期
- 项目ID或凭据ID不正确
- 服务器时间不同步
解决方案:
- 检查Token的过期时间,确保在有效期内
- 核对控制台中的项目ID和凭据ID
- 确保服务器时间与网络时间同步
6.3 返回数据不完整
问题现象:API返回了成功响应,但某些字段缺失
可能原因:
- 该地区不支持某些数据指标
- 订阅的API套餐权限不足
解决方案:
- 检查API文档确认该地区支持的数据
- 升级API套餐或联系和风天气支持
6.4 请求频率限制
问题现象:API返回429 Too Many Requests错误
可能原因:
- 超过了API的调用频率限制
- 短时间内发送了大量请求
解决方案:
- 实现请求限流,控制调用频率
- 使用缓存减少不必要的API调用
- 考虑升级API套餐提高限额
7. 扩展功能实现
7.1 多城市天气查询
通过循环调用接口,可以实现多城市天气查询:
python复制def get_multiple_locations_weather(self, location_ids):
"""获取多个城市的天气数据"""
results = {}
for loc_id in location_ids:
try:
data = self.get_real_time_weather(loc_id)
results[loc_id] = self.parse_weather_data(data)
except Exception as e:
results[loc_id] = {'error': str(e)}
return results
7.2 天气预警信息
和风天气还提供天气预警接口,可以获取特定地区的天气预警信息:
python复制def get_weather_warning(self, location_id):
"""获取天气预警信息"""
endpoint = 'warning/now'
params = {'location': location_id}
return self._make_request(endpoint, params)
7.3 天气预报数据
除了实时天气,还可以获取多天天气预报:
python复制def get_weather_forecast(self, location_id, days=3):
"""获取天气预报"""
endpoint = 'weather/7d' if days == 7 else 'weather/3d'
params = {'location': location_id}
return self._make_request(endpoint, params)
8. 项目部署与最佳实践
8.1 安全注意事项
- 私钥保护:私钥文件不要上传到代码仓库,可以通过环境变量或配置管理工具注入
- 访问限制:在服务器上设置防火墙规则,限制出站连接到和风天气API域名
- 权限控制:使用最小权限原则,只授予必要的API访问权限
8.2 性能优化建议
- 批量请求:对于多城市查询,考虑使用和风天气的批量请求接口(如果可用)
- 缓存策略:根据数据更新频率设置合理的缓存时间
- 异步处理:对于非实时要求的场景,可以使用异步任务获取天气数据
8.3 监控与日志
- API调用监控:记录API调用次数、响应时间等指标
- 错误日志:详细记录API错误信息,便于排查问题
- 配额监控:监控API调用配额使用情况,避免超额
9. 替代方案比较
除了和风天气,还有其他天气数据提供商可供选择:
| 服务商 | 免费额度 | 数据覆盖 | 特色功能 | 认证方式 |
|---|---|---|---|---|
| 和风天气 | 有限免费 | 全球 | 分钟级降水预报 | JWT/API Key |
| 心知天气 | 有限免费 | 全球 | 天气预警推送 | API Key |
| OpenWeather | 有限免费 | 全球 | 历史天气数据 | API Key |
| WeatherAPI | 有限免费 | 全球 | 海洋天气数据 | API Key |
选择建议:
- 国内项目优先考虑和风天气或心知天气,数据更本地化
- 国际项目可以考虑OpenWeather等国际服务商
- 根据具体需求(如是否需要预警、历史数据等)选择最适合的API
10. 项目总结与经验分享
在实际开发中,调用第三方天气API看似简单,但要做好一个稳定可靠的天气服务模块,还需要考虑很多细节。以下是一些经验总结:
-
认证机制选择:JWT虽然实现稍复杂,但安全性更高,适合生产环境使用。开发测试阶段可以先用API Key快速验证。
-
错误处理:天气API的稳定性通常很高,但仍需要做好各种错误情况的处理,包括网络错误、认证失败、数据解析错误等。
-
数据更新策略:根据应用场景确定合适的数据更新频率。对于实时性要求高的场景可以缩短间隔,一般应用可以适当延长以减少API调用。
-
私钥管理:在实际部署时,建议使用专门的密钥管理服务来存储私钥,而不是直接放在代码或配置文件中。
-
监控报警:设置API调用失败报警,及时发现并解决问题,避免影响用户体验。
这个项目展示了如何通过Python调用和风天气API获取实时天气数据,涵盖了从认证到数据获取、解析的全过程。代码实现可以根据实际需求进行调整和扩展,比如添加更多天气指标、集成到Web服务中,或者开发成独立的天气服务模块。
