📖 目录
一、需求
1.1 应用场景
天气数据是许多数据分析项目的重要基础数据,例如:
- 零售销量预测与天气关联分析
- 绘制热力图等
1.2 目标
- 多城市支持:同时获取多个城市的天气数据
- 数据类型全面:包含实况天气和未来天气预报
- 数据持久化:将采集的数据存储到MySQL数据库
- 自动更新:支持数据的插入和更新操作
- 错误处理:完善的异常处理和日志记录
1.3 技术选型
- 数据源:高德地图天气API
- 编程语言:Python 3.x
- 数据库:MySQL 8.0+
- 核心库:Requests, Pandas, SQLAlchemy, PyMySQL
二、核心类设计
2.1 WeatherAPI类
class WeatherAPI:
"""高德地图天气API封装类"""
def __init__(self, key):
"""
初始化API配置
:param key: 高德地图API密钥
"""
self.base_url = "https://restapi.amap.com/v3/weather/weatherInfo"
self.key = key
功能特性:
- ✅ 参数验证:自动检查API参数有效性
- ✅ 错误处理:HTTP错误和API错误分别处理
- ✅ 超时控制:设置10秒请求超时
- ✅ 重试机制:网络异常时自动重试
2.2 MySQLDatabase类
class MySQLDatabase:
"""MySQL数据库操作类"""
def __init__(self, host, port, user, password, database):
"""
初始化数据库连接
:param host: 数据库主机地址
:param port: 数据库端口
:param user: 数据库用户名
:param password: 数据库密码
:param database: 数据库名称
"""
self.engine = create_engine(
f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}"
)
核心方法:
connect(): 建立数据库连接create_table_if_not_exists(): 创建数据表upsert_weather_data(): 插入或更新天气数据
三、数据表设计
3.1 表结构定义
-- 天气数据表
CREATE TABLE IF NOT EXISTS weather (
-- 主键和标识字段
id VARCHAR(50) PRIMARY KEY COMMENT '主键ID: 日期_区域编码',
data_type VARCHAR(20) NOT NULL COMMENT '数据类型: live-实况, forecast-预报',
time_period VARCHAR(10) COMMENT '时间段: day-白天, night-夜间, live-实况',
-- API状态信息
status VARCHAR(10) NOT NULL COMMENT 'API状态码',
count VARCHAR(10) COMMENT '返回数据条数',
info VARCHAR(255) COMMENT 'API返回信息',
infocode VARCHAR(20) COMMENT 'API信息代码',
-- 地理位置信息
province VARCHAR(50) COMMENT '省份名称',
city VARCHAR(50) COMMENT '城市名称',
adcode VARCHAR(20) COMMENT '高德区域编码',
-- 天气详细信息
date VARCHAR(20) NOT NULL COMMENT '日期 (YYYY-MM-DD)',
week VARCHAR(10) COMMENT '星期几',
weather VARCHAR(50) COMMENT '天气状况',
temperature VARCHAR(20) COMMENT '温度 (摄氏度)',
winddirection VARCHAR(50) COMMENT '风向',
windpower VARCHAR(50) COMMENT '风力',
humidity VARCHAR(20) COMMENT '湿度 (%)',
-- 时间信息
reporttime VARCHAR(30) COMMENT 'API报告时间',
fetch_time VARCHAR(30) COMMENT '系统采集时间',
-- 索引设计
INDEX idx_date (date),
INDEX idx_city (city),
INDEX idx_adcode (adcode),
INDEX idx_data_type (data_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天气数据表';
3.2 字段设计原则
- 主键设计:采用复合主键
date_adcode确保数据唯一性 - 数据类型明确:区分实况(live)和预报(forecast)数据
- 时间维度:包含API报告时间和系统采集时间
- 地理位置:支持省份、城市、区域编码多级定位
- 天气要素:涵盖温度、天气状况、风力、湿度等关键指标
3.3 索引优化策略
| 索引名称 | 字段 | 使用场景 |
|---|---|---|
| idx_date | date | 按日期范围查询 |
| idx_city | city | 按城市查询统计 |
| idx_adcode | adcode | 按区域编码关联查询 |
| idx_data_type | data_type | 按数据类型过滤 |
四、代码实现详解
4.1 实时天气数据采集
def get_today_weather_data(key, cities):
"""
获取当天实况天气数据
使用省级编码获取实时天气信息
:param key: API密钥
:param cities: 城市字典 {城市名: 区域编码}
:return: 实况天气数据列表
"""
处理流程:
1. 遍历城市列表
2. 调用API获取实时数据(extensions='base')
3. 解析API返回的lives数据
4. 提取关键字段并格式化
5. 添加系统时间戳
关键处理:
- 时间处理:从reporttime字段提取日期信息
- 数据验证:检查API返回状态和数据完整性
- 错误恢复:单城市失败不影响其他城市采集
4.2 预报天气数据采集
def get_forecast_weather_data(key, cities):
"""
获取天气预报数据
使用市级编码获取未来天气预报
:param key: API密钥
:param cities: 城市字典 {城市名: 区域编码}
:return: 预报天气数据列表
"""
处理流程:
1. 遍历城市列表
2. 调用API获取预报数据(extensions='all')
3. 解析forecasts数据中的casts列表
4. 过滤只保留未来日期的预报
5. 处理白天预报数据
特殊处理:
- 日期过滤:只保留今天之后的预报数据
- 数据精简:目前只处理白天预报,可扩展夜间预报
- 时间间隔:设置0.5秒延迟避免API限流
4.3 数据合并与展示
def combine_weather_data(today_data, forecast_data):
"""合并实时和预报数据"""
combined = today_data + forecast_data
print(f"数据汇总: 实时{len(today_data)}条 + 预报{len(forecast_data)}条 = 总计{len(combined)}条")
return combined
def display_weather_results(weather_data):
"""格式化显示天气数据"""
if not weather_data:
return
df = pd.DataFrame(weather_data)
# 显示实况天气
live_data = df[df['data_type'] == 'live']
if not live_data.empty:
display_cols = ['province', 'city', 'date', 'weather', 'temperature']
print(live_data[display_cols].to_string(index=False))
def show_statistics(weather_data):
"""显示数据统计信息"""
df = pd.DataFrame(weather_data)
print("数据统计:")
print(f" 数据类型分布: {df['data_type'].value_counts().to_dict()}")
print(f" 城市分布: {df['city'].value_counts().to_dict()}")
print(f" 日期分布: {df['date'].value_counts().sort_index().to_dict()}")
4.4 数据库操作实现
4.4.1 表结构管理
def create_table_if_not_exists(self):
"""
创建天气数据表
使用IF NOT EXISTS避免重复创建
包含完整的字段注释和索引定义
"""
4.4.2 数据Upsert操作
def upsert_weather_data(self, weather_data):
"""
使用REPLACE INTO实现插入/更新
技术要点:
1. 生成唯一ID: date_adcode
2. 使用事务确保数据一致性
3. 逐条处理保证错误隔离
4. 使用参数化查询防止SQL注入
"""
步骤:
1. 为每条记录生成唯一ID
2. 开启数据库事务
3. 逐条执行REPLACE INTO
4. 提交事务或回滚
REPLACE INTO优势:
- 自动处理重复数据
- 简化代码逻辑
- 保证数据最新性
五、使用说明
5.1 环境准备
# 安装依赖包
pip install requests pandas pymysql sqlalchemy
# 数据库准备
CREATE DATABASE weatherdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
5.2 API密钥申请
- 访问 高德开放平台
- 注册开发者账号
- 创建新应用获取Web服务API Key
5.3 配置文件
# 主要配置项
CONFIG = {
# 高德API配置
'AMAP_KEY': '您的API密钥',
# 数据库配置
'DB_HOST': 'localhost',
'DB_PORT': 3306,
'DB_USER': 'root',
'DB_PASSWORD': '密码',
'DB_NAME': 'weatherdb',
# 城市配置
'PROVINCES': {
'福建省': '350000',
},
'CITIES': {
'厦门市': '350200',
'福州市': '350100',
# 其他城市...
}
}
5.4 运行方式
# 直接运行
python weather_collector.py
# 定时运行(Linux Crontab)
0 */3 * * * /usr/bin/python3 /path/to/weather_collector.py
# 定时运行(Windows计划任务)
# 设置每3小时运行一次
5.5 数据查询示例
-- 查询最新实况天气
SELECT city, date, weather, temperature, humidity, reporttime
FROM weather
WHERE data_type = 'live'
AND date = CURDATE()
ORDER BY reporttime DESC;
-- 查询未来3天预报
SELECT city, date, week, weather, temperature, winddirection
FROM weather
WHERE data_type = 'forecast'
AND date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 3 DAY)
ORDER BY city, date;
-- 城市天气统计
SELECT
city,
COUNT(*) as total_records,
MIN(temperature) as min_temp,
MAX(temperature) as max_temp,
AVG(CAST(temperature AS UNSIGNED)) as avg_temp
FROM weather
WHERE data_type = 'live'
GROUP BY city
ORDER BY avg_temp DESC;
5.6 错误处理与调试
# 常见问题及解决方法
问题1: API密钥无效
解决: 检查密钥是否正确,确认Web服务权限已开通
问题2: 数据库连接失败
解决: 检查数据库配置、网络连接、用户权限
问题3: 数据保存失败
解决: 检查表结构、字段类型、数据格式
问题4: API请求超限
解决: 增加请求间隔,申请更高配额
六、完整代码
import requests
import pandas as pd
import time
from datetime import datetime
import pymysql
from sqlalchemy import create_engine, text
class WeatherAPI:
"""高德地图天气API封装类"""
def __init__(self, key):
self.base_url = "https://restapi.amap.com/v3/weather/weatherInfo"
self.key = key
def get_weather_data(self, city, extensions='base', output='JSON'):
"""获取天气数据"""
params = {
'key': self.key,
'city': city,
'extensions': extensions,
'output': output
}
try:
response = requests.get(self.base_url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if data.get('status') == '0':
print(f"API错误: {data.get('info', '未知错误')}")
return None
return data
except Exception as e:
print(f"请求失败: {e}")
return None
class MySQLDatabase:
"""MySQL数据库操作类"""
def __init__(self, host='localhost', port=3306, user='root',
password='', database='weatherdb'):
self.host = host
self.port = port
self.user = user
self.password = password
self.database = database
self.engine = None
self.connect()
def connect(self):
"""建立数据库连接"""
try:
connection_str = f"mysql+pymysql://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}"
self.engine = create_engine(connection_str)
print("数据库连接成功")
except Exception as e:
print(f"数据库连接失败: {e}")
def create_table_if_not_exists(self):
"""创建天气数据表"""
create_table_sql = """
CREATE TABLE IF NOT EXISTS weather (
id VARCHAR(50) PRIMARY KEY COMMENT '主键ID: date_adcode',
data_type VARCHAR(20) NOT NULL COMMENT '数据类型: live-实况, forecast-预报',
time_period VARCHAR(10) COMMENT '时间段: day-白天, night-夜间, live-实况',
status VARCHAR(10) NOT NULL,
count VARCHAR(10),
info VARCHAR(255),
infocode VARCHAR(20),
province VARCHAR(50),
city VARCHAR(50),
adcode VARCHAR(20),
date VARCHAR(20) NOT NULL COMMENT '日期',
week VARCHAR(10),
weather VARCHAR(50),
temperature VARCHAR(20),
winddirection VARCHAR(50),
windpower VARCHAR(50),
humidity VARCHAR(20),
reporttime VARCHAR(30) COMMENT '报告时间',
fetch_time VARCHAR(30) COMMENT '获取时间',
INDEX idx_date (date),
INDEX idx_city (city),
INDEX idx_adcode (adcode),
INDEX idx_data_type (data_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天气数据表'
"""
try:
with self.engine.connect() as conn:
conn.execute(text(create_table_sql))
print("数据表创建完成")
except Exception as e:
print(f"创建表失败: {e}")
def upsert_weather_data(self, weather_data):
"""插入或更新天气数据"""
if not weather_data:
print("没有数据可保存")
return False
try:
# 生成唯一ID
for record in weather_data:
date = record.get('date', '')
adcode = record.get('adcode', '')
record['id'] = f"{date}_{adcode}"
# 批量处理
with self.engine.connect() as conn:
with conn.begin():
for record in weather_data:
replace_sql = """
REPLACE INTO weather (
id, data_type, time_period, status, count, info, infocode,
province, city, adcode, date, week, weather, temperature,
winddirection, windpower, humidity, reporttime, fetch_time
) VALUES (
:id, :data_type, :time_period, :status, :count, :info, :infocode,
:province, :city, :adcode, :date, :week, :weather, :temperature,
:winddirection, :windpower, :humidity, :reporttime, :fetch_time
)
"""
conn.execute(text(replace_sql), record)
print(f"成功保存 {len(weather_data)} 条天气数据")
return True
except Exception as e:
print(f"保存数据失败: {e}")
return False
def get_today_weather_data(key, cities):
"""获取当天实况天气数据"""
weather_api = WeatherAPI(key)
today_weather_data = []
print("开始获取实况天气数据...")
for city_name, city_code in cities.items():
print(f"处理: {city_name}")
# 获取实况天气
real_time_data = weather_api.get_weather_data(city_code, 'base')
if real_time_data and real_time_data.get('status') == '1' and 'lives' in real_time_data:
for live in real_time_data['lives']:
report_time = live.get('reporttime', '')
date = report_time.split()[0] if report_time else datetime.now().strftime('%Y-%m-%d')
weather_record = {
'data_type': 'live',
'time_period': 'live',
'status': str(real_time_data.get('status', '')),
'count': str(real_time_data.get('count', '')),
'info': str(real_time_data.get('info', '')),
'infocode': str(real_time_data.get('infocode', '')),
'province': str(live.get('province', '')),
'city': str(live.get('city', '')),
'adcode': str(live.get('adcode', '')),
'date': date,
'week': '',
'weather': str(live.get('weather', '')),
'temperature': str(live.get('temperature', '')),
'winddirection': str(live.get('winddirection', '')),
'windpower': str(live.get('windpower', '')),
'humidity': str(live.get('humidity', '')),
'reporttime': str(live.get('reporttime', '')),
'fetch_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
today_weather_data.append(weather_record)
print(f" 实况: {live.get('weather')} {live.get('temperature')}°C")
else:
print(" 实况天气获取失败")
time.sleep(0.5)
return today_weather_data
def get_forecast_weather_data(key, cities):
"""获取预报天气数据"""
weather_api = WeatherAPI(key)
forecast_weather_data = []
print("开始获取预报天气数据...")
for city_name, city_code in cities.items():
print(f"处理: {city_name}")
# 获取预报天气
forecast_data = weather_api.get_weather_data(city_code, 'all')
if forecast_data and forecast_data.get('status') == '1' and 'forecasts' in forecast_data:
for forecast in forecast_data['forecasts']:
casts = forecast.get('casts', [])
for cast in casts:
forecast_date = cast.get('date', '')
today = datetime.now().strftime('%Y-%m-%d')
if forecast_date > today:
day_record = {
'data_type': 'forecast',
'time_period': 'day',
'status': str(forecast_data.get('status', '')),
'count': str(forecast_data.get('count', '')),
'info': str(forecast_data.get('info', '')),
'infocode': str(forecast_data.get('infocode', '')),
'province': str(forecast.get('province', '')),
'city': str(forecast.get('city', '')),
'adcode': str(forecast.get('adcode', '')),
'date': forecast_date,
'week': str(cast.get('week', '')),
'weather': str(cast.get('dayweather', '')),
'temperature': str(cast.get('daytemp', '')),
'winddirection': str(cast.get('daywind', '')),
'windpower': str(cast.get('daypower', '')),
'humidity': '',
'reporttime': str(forecast.get('reporttime', '')),
'fetch_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
forecast_weather_data.append(day_record)
future_days = len([c for c in casts if c.get('date', '') >= today])
print(f" 预报: 获取{future_days}天预报数据")
else:
print(" 预报天气获取失败")
time.sleep(0.5)
return forecast_weather_data
def combine_weather_data(today_data, forecast_data):
"""合并天气数据"""
combined_data = today_data + forecast_data
print(f"数据合并: 实况{len(today_data)}条 + 预报{len(forecast_data)}条 = 总计{len(combined_data)}条")
return combined_data
def display_weather_results(weather_data):
"""显示天气结果"""
if not weather_data:
print("没有获取到天气数据")
return
df = pd.DataFrame(weather_data)
print("实况天气数据:")
live_data = df[df['data_type'] == 'live']
if not live_data.empty:
display_cols = ['province', 'city', 'date', 'weather', 'temperature']
print(live_data[display_cols].to_string(index=False))
print("\n天气预报数据:")
forecast_data = df[df['data_type'] == 'forecast']
if not forecast_data.empty:
display_cols = ['city', 'date', 'week', 'weather', 'temperature']
print(forecast_data[display_cols].to_string(index=False))
def show_statistics(weather_data):
"""显示统计信息"""
if not weather_data:
return
df = pd.DataFrame(weather_data)
print("\n数据统计:")
print(f" 数据类型: {df['data_type'].value_counts().to_dict()}")
print(f" 城市分布: {df['city'].value_counts().to_dict()}")
print(f" 日期分布: {df['date'].value_counts().sort_index().to_dict()}")
def main():
"""主函数"""
# 配置信息(高德申请)
KEY = "your_amap_api_key"
DB_CONFIG = {
'host': 'localhost',
'port': 3306,
'user': 'root',
'password': 'your_password',
'database': 'weatherdb'
}
if KEY == "your_amap_api_key":
print("请配置高德地图API Key")
return
# 初始化数据库
db = MySQLDatabase(**DB_CONFIG)
db.create_table_if_not_exists()
# 城市配置
provence = {
'福建省': '350000',
}
cities = {
'厦门市': '350200',
'福州市': '350100',
'莆田市': '350300',
'三明市': '350400',
'泉州市': '350500',
'漳州市': '350600',
'南平市': '350700',
'龙岩市': '350800',
'宁德市': '350900',
}
# 采集数据
today_data = get_today_weather_data(KEY, provence)
forecast_data = get_forecast_weather_data(KEY, cities)
combined_data = combine_weather_data(today_data, forecast_data)
# 显示结果
display_weather_results(combined_data)
show_statistics(combined_data)
# 保存数据
db.upsert_weather_data(combined_data)
if __name__ == "__main__":
main()
转载请注明出处,欢迎交流讨论

872

被折叠的 条评论
为什么被折叠?



