| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- import requests
- import pandas as pd
- import time
- import os
- import random
- # ============================================================
- # 配置
- # ============================================================
- START_DATE = "2020-01-01"
- # 你真正需要的数据截止日期
- END_DATE = "2026-08-31"
- # 实际请求时,向后多请求几天
- # 例如 END_DATE=2026-08-31
- # 实际请求到 2026-09-03
- REQUEST_EXTRA_DAYS = 3
- # 输出目录
- OUTPUT_DIR = "weather_data"
- os.makedirs(OUTPUT_DIR, exist_ok=True)
- # 最终合并文件
- FINAL_FILE = "china_city_temperature_2020_2026.csv"
- # Open-Meteo 历史 API
- URL = "https://archive-api.open-meteo.com/v1/archive"
- # ============================================================
- # 城市
- # ============================================================
- cities = {
- "北京": (39.9042, 116.4074),
- "上海": (31.2304, 121.4737),
- "广州": (23.1291, 113.2644),
- "深圳": (22.5431, 114.0579),
- "成都": (30.5728, 104.0668),
- "武汉": (30.5928, 114.3055),
- "西安": (34.3416, 108.9398),
- "南京": (32.0603, 118.7969),
- "青岛": (36.0671, 120.3826),
- "哈尔滨": (45.8038, 126.5349),
- "乌鲁木齐": (43.8256, 87.6168),
- "昆明": (25.0389, 102.7183),
- }
- # ============================================================
- # 计算实际请求结束日期
- # ============================================================
- request_end_date = (
- pd.Timestamp(END_DATE)
- + pd.Timedelta(days=REQUEST_EXTRA_DAYS)
- ).strftime("%Y-%m-%d")
- print("=" * 60)
- print("数据配置")
- print("=" * 60)
- print(f"实际需要:{START_DATE} ~ {END_DATE}")
- print(f"API请求: {START_DATE} ~ {request_end_date}")
- print(f"额外请求:{REQUEST_EXTRA_DAYS} 天")
- print("=" * 60)
- # ============================================================
- # 创建 Session
- # ============================================================
- session = requests.Session()
- session.headers.update({
- "User-Agent": "Mozilla/5.0 (Weather Data Collector)"
- })
- # ============================================================
- # 获取一个城市
- # ============================================================
- def get_city_weather(city, lat, lon, max_retries=6):
- output_file = os.path.join(
- OUTPUT_DIR,
- f"{city}_{START_DATE}_{END_DATE}.csv"
- )
- # --------------------------------------------------------
- # 计算真正需要的数据天数
- # --------------------------------------------------------
- expected_days = (
- pd.Timestamp(END_DATE)
- - pd.Timestamp(START_DATE)
- ).days + 1
- # --------------------------------------------------------
- # 如果已经下载成功,直接跳过
- #
- # 注意:
- # 不再简单使用 len(old) > 3000 判断。
- # 必须同时检查:
- # 1. 日期数量
- # 2. 三个温度字段没有缺失
- # --------------------------------------------------------
- if os.path.exists(output_file):
- try:
- old = pd.read_csv(output_file)
- required_columns = {
- "date",
- "city",
- "mean_temp",
- "max_temp",
- "min_temp"
- }
- columns_ok = required_columns.issubset(
- old.columns
- )
- row_count_ok = len(old) == expected_days
- temp_ok = False
- if columns_ok:
- temp_ok = not old[
- [
- "mean_temp",
- "max_temp",
- "min_temp"
- ]
- ].isna().any().any()
- if (
- columns_ok
- and row_count_ok
- and temp_ok
- ):
- print(
- f"✓ {city} 已存在且完整,跳过"
- )
- return old
- else:
- print(
- f"⚠ {city} 已存在,但数据不完整,重新获取"
- )
- except Exception as e:
- print(
- f"⚠ {city} 原文件读取失败,重新获取:{e}"
- )
- # ========================================================
- # Open-Meteo 请求参数
- # ========================================================
- params = {
- "latitude": lat,
- "longitude": lon,
- # ----------------------------------------------------
- # 重点:
- # START_DATE 不变
- # END_DATE 向后增加 3 天
- # ----------------------------------------------------
- "start_date": START_DATE,
- "end_date": request_end_date,
- "daily": (
- "temperature_2m_mean,"
- "temperature_2m_max,"
- "temperature_2m_min"
- ),
- "timezone": "Asia/Shanghai",
- # 长期历史数据
- "models": "era5_land",
- # 陆地网格选择
- "cell_selection": "land",
- # 摄氏度
- "temperature_unit": "celsius",
- }
- # ========================================================
- # 开始请求
- # ========================================================
- for attempt in range(1, max_retries + 1):
- try:
- print(
- f"正在获取:{city} "
- f"(第 {attempt}/{max_retries} 次)"
- )
- print(
- f" API范围:{START_DATE} ~ {request_end_date}"
- )
- response = session.get(
- URL,
- params=params,
- timeout=(15, 180)
- )
- response.raise_for_status()
- data = response.json()
- if data.get("error"):
- raise RuntimeError(
- data.get(
- "reason",
- "Open-Meteo API error"
- )
- )
- daily = data["daily"]
- # =================================================
- # 构造 DataFrame
- # =================================================
- df = pd.DataFrame({
- "date": daily["time"],
- "city": city,
- "mean_temp": daily[
- "temperature_2m_mean"
- ],
- "max_temp": daily[
- "temperature_2m_max"
- ],
- "min_temp": daily[
- "temperature_2m_min"
- ],
- })
- # =================================================
- # 重点:
- # 只保留用户真正要求的 END_DATE
- # =================================================
- df["date"] = pd.to_datetime(df["date"])
- target_end = pd.Timestamp(END_DATE)
- df = df[
- df["date"] <= target_end
- ].copy()
- # 日期重新排序
- df = df.sort_values(
- "date"
- ).reset_index(drop=True)
- # =================================================
- # 数据数量检查
- # =================================================
- if len(df) != expected_days:
- raise RuntimeError(
- f"数据数量异常:{len(df)} 条,"
- f"预期 {expected_days} 条"
- )
- # =================================================
- # 日期连续性检查
- # =================================================
- expected_dates = pd.date_range(
- start=START_DATE,
- end=END_DATE,
- freq="D"
- )
- actual_dates = pd.DatetimeIndex(
- df["date"]
- )
- if not actual_dates.equals(
- expected_dates
- ):
- raise RuntimeError(
- "日期不连续或存在缺失日期"
- )
- # =================================================
- # 温度数据缺失检查
- # =================================================
- temperature_columns = [
- "mean_temp",
- "max_temp",
- "min_temp"
- ]
- missing_count = (
- df[temperature_columns]
- .isna()
- .any(axis=1)
- .sum()
- )
- if missing_count > 0:
- missing_dates = df.loc[
- df[temperature_columns]
- .isna()
- .any(axis=1),
- "date"
- ].dt.strftime(
- "%Y-%m-%d"
- ).tolist()
- raise RuntimeError(
- f"存在 {missing_count} 天温度数据为空:"
- f"{missing_dates[-10:]}"
- )
- # =================================================
- # 保存单城市文件
- # =================================================
- df["date"] = df[
- "date"
- ].dt.strftime("%Y-%m-%d")
- df.to_csv(
- output_file,
- index=False,
- encoding="utf-8-sig"
- )
- print(
- f"✓ {city} 获取成功:"
- f"{len(df)} 天"
- )
- print(
- f" 数据范围:"
- f"{df['date'].iloc[0]} ~ "
- f"{df['date'].iloc[-1]}"
- )
- return df
- # =====================================================
- # 请求失败
- # =====================================================
- except Exception as e:
- print(
- f"✗ {city} 第 {attempt} 次失败:"
- f"{type(e).__name__}: {e}"
- )
- if attempt < max_retries:
- # 指数退避
- wait = min(
- 60,
- 5 * (2 ** (attempt - 1))
- )
- # 随机延迟
- wait += random.uniform(0, 3)
- print(
- f"等待 {wait:.1f} 秒后重试..."
- )
- time.sleep(wait)
- else:
- print(
- f"✗ {city} 最终失败,"
- f"后面会继续处理其他城市"
- )
- return None
- # ============================================================
- # 开始下载
- # ============================================================
- all_data = []
- failed = []
- for city, (lat, lon) in cities.items():
- df = get_city_weather(
- city,
- lat,
- lon
- )
- if df is not None:
- all_data.append(df)
- else:
- failed.append(city)
- # 城市之间稍微等待
- time.sleep(2)
- # ============================================================
- # 合并所有成功数据
- # ============================================================
- if all_data:
- result = pd.concat(
- all_data,
- ignore_index=True
- )
- result.to_csv(
- FINAL_FILE,
- index=False,
- encoding="utf-8-sig"
- )
- print("\n" + "=" * 60)
- print("下载完成")
- print("=" * 60)
- print(f"成功城市:{len(all_data)}")
- print(f"失败城市:{len(failed)}")
- print(f"总数据量:{len(result):,}")
- print(
- f"理论数据量:"
- f"{expected_days * len(cities):,}"
- )
- print(
- f"最终文件:{FINAL_FILE}"
- )
- if failed:
- print("\n失败城市:")
- for city in failed:
- print(" -", city)
- else:
- print(
- "没有成功获取任何城市数据"
- )
- # ============================================================
- # 数据检查
- # ============================================================
- print("\n数据预览:")
- if all_data:
- print(
- result.head(10)
- )
- print("\n每个城市数据量:")
- print(
- result.groupby("city").size()
- )
- print("\n缺失值检查:")
- print(
- result.isna().sum()
- )
- print("\n日期范围检查:")
- print(
- result.groupby("city")["date"]
- .agg(["min", "max", "count"])
- )
|