weathergetNoNull.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import requests
  2. import pandas as pd
  3. import time
  4. import os
  5. import random
  6. # ============================================================
  7. # 配置
  8. # ============================================================
  9. START_DATE = "2020-01-01"
  10. # 你真正需要的数据截止日期
  11. END_DATE = "2026-08-31"
  12. # 实际请求时,向后多请求几天
  13. # 例如 END_DATE=2026-08-31
  14. # 实际请求到 2026-09-03
  15. REQUEST_EXTRA_DAYS = 3
  16. # 输出目录
  17. OUTPUT_DIR = "weather_data"
  18. os.makedirs(OUTPUT_DIR, exist_ok=True)
  19. # 最终合并文件
  20. FINAL_FILE = "china_city_temperature_2020_2026.csv"
  21. # Open-Meteo 历史 API
  22. URL = "https://archive-api.open-meteo.com/v1/archive"
  23. # ============================================================
  24. # 城市
  25. # ============================================================
  26. cities = {
  27. "北京": (39.9042, 116.4074),
  28. "上海": (31.2304, 121.4737),
  29. "广州": (23.1291, 113.2644),
  30. "深圳": (22.5431, 114.0579),
  31. "成都": (30.5728, 104.0668),
  32. "武汉": (30.5928, 114.3055),
  33. "西安": (34.3416, 108.9398),
  34. "南京": (32.0603, 118.7969),
  35. "青岛": (36.0671, 120.3826),
  36. "哈尔滨": (45.8038, 126.5349),
  37. "乌鲁木齐": (43.8256, 87.6168),
  38. "昆明": (25.0389, 102.7183),
  39. }
  40. # ============================================================
  41. # 计算实际请求结束日期
  42. # ============================================================
  43. request_end_date = (
  44. pd.Timestamp(END_DATE)
  45. + pd.Timedelta(days=REQUEST_EXTRA_DAYS)
  46. ).strftime("%Y-%m-%d")
  47. print("=" * 60)
  48. print("数据配置")
  49. print("=" * 60)
  50. print(f"实际需要:{START_DATE} ~ {END_DATE}")
  51. print(f"API请求: {START_DATE} ~ {request_end_date}")
  52. print(f"额外请求:{REQUEST_EXTRA_DAYS} 天")
  53. print("=" * 60)
  54. # ============================================================
  55. # 创建 Session
  56. # ============================================================
  57. session = requests.Session()
  58. session.headers.update({
  59. "User-Agent": "Mozilla/5.0 (Weather Data Collector)"
  60. })
  61. # ============================================================
  62. # 获取一个城市
  63. # ============================================================
  64. def get_city_weather(city, lat, lon, max_retries=6):
  65. output_file = os.path.join(
  66. OUTPUT_DIR,
  67. f"{city}_{START_DATE}_{END_DATE}.csv"
  68. )
  69. # --------------------------------------------------------
  70. # 计算真正需要的数据天数
  71. # --------------------------------------------------------
  72. expected_days = (
  73. pd.Timestamp(END_DATE)
  74. - pd.Timestamp(START_DATE)
  75. ).days + 1
  76. # --------------------------------------------------------
  77. # 如果已经下载成功,直接跳过
  78. #
  79. # 注意:
  80. # 不再简单使用 len(old) > 3000 判断。
  81. # 必须同时检查:
  82. # 1. 日期数量
  83. # 2. 三个温度字段没有缺失
  84. # --------------------------------------------------------
  85. if os.path.exists(output_file):
  86. try:
  87. old = pd.read_csv(output_file)
  88. required_columns = {
  89. "date",
  90. "city",
  91. "mean_temp",
  92. "max_temp",
  93. "min_temp"
  94. }
  95. columns_ok = required_columns.issubset(
  96. old.columns
  97. )
  98. row_count_ok = len(old) == expected_days
  99. temp_ok = False
  100. if columns_ok:
  101. temp_ok = not old[
  102. [
  103. "mean_temp",
  104. "max_temp",
  105. "min_temp"
  106. ]
  107. ].isna().any().any()
  108. if (
  109. columns_ok
  110. and row_count_ok
  111. and temp_ok
  112. ):
  113. print(
  114. f"✓ {city} 已存在且完整,跳过"
  115. )
  116. return old
  117. else:
  118. print(
  119. f"⚠ {city} 已存在,但数据不完整,重新获取"
  120. )
  121. except Exception as e:
  122. print(
  123. f"⚠ {city} 原文件读取失败,重新获取:{e}"
  124. )
  125. # ========================================================
  126. # Open-Meteo 请求参数
  127. # ========================================================
  128. params = {
  129. "latitude": lat,
  130. "longitude": lon,
  131. # ----------------------------------------------------
  132. # 重点:
  133. # START_DATE 不变
  134. # END_DATE 向后增加 3 天
  135. # ----------------------------------------------------
  136. "start_date": START_DATE,
  137. "end_date": request_end_date,
  138. "daily": (
  139. "temperature_2m_mean,"
  140. "temperature_2m_max,"
  141. "temperature_2m_min"
  142. ),
  143. "timezone": "Asia/Shanghai",
  144. # 长期历史数据
  145. "models": "era5_land",
  146. # 陆地网格选择
  147. "cell_selection": "land",
  148. # 摄氏度
  149. "temperature_unit": "celsius",
  150. }
  151. # ========================================================
  152. # 开始请求
  153. # ========================================================
  154. for attempt in range(1, max_retries + 1):
  155. try:
  156. print(
  157. f"正在获取:{city} "
  158. f"(第 {attempt}/{max_retries} 次)"
  159. )
  160. print(
  161. f" API范围:{START_DATE} ~ {request_end_date}"
  162. )
  163. response = session.get(
  164. URL,
  165. params=params,
  166. timeout=(15, 180)
  167. )
  168. response.raise_for_status()
  169. data = response.json()
  170. if data.get("error"):
  171. raise RuntimeError(
  172. data.get(
  173. "reason",
  174. "Open-Meteo API error"
  175. )
  176. )
  177. daily = data["daily"]
  178. # =================================================
  179. # 构造 DataFrame
  180. # =================================================
  181. df = pd.DataFrame({
  182. "date": daily["time"],
  183. "city": city,
  184. "mean_temp": daily[
  185. "temperature_2m_mean"
  186. ],
  187. "max_temp": daily[
  188. "temperature_2m_max"
  189. ],
  190. "min_temp": daily[
  191. "temperature_2m_min"
  192. ],
  193. })
  194. # =================================================
  195. # 重点:
  196. # 只保留用户真正要求的 END_DATE
  197. # =================================================
  198. df["date"] = pd.to_datetime(df["date"])
  199. target_end = pd.Timestamp(END_DATE)
  200. df = df[
  201. df["date"] <= target_end
  202. ].copy()
  203. # 日期重新排序
  204. df = df.sort_values(
  205. "date"
  206. ).reset_index(drop=True)
  207. # =================================================
  208. # 数据数量检查
  209. # =================================================
  210. if len(df) != expected_days:
  211. raise RuntimeError(
  212. f"数据数量异常:{len(df)} 条,"
  213. f"预期 {expected_days} 条"
  214. )
  215. # =================================================
  216. # 日期连续性检查
  217. # =================================================
  218. expected_dates = pd.date_range(
  219. start=START_DATE,
  220. end=END_DATE,
  221. freq="D"
  222. )
  223. actual_dates = pd.DatetimeIndex(
  224. df["date"]
  225. )
  226. if not actual_dates.equals(
  227. expected_dates
  228. ):
  229. raise RuntimeError(
  230. "日期不连续或存在缺失日期"
  231. )
  232. # =================================================
  233. # 温度数据缺失检查
  234. # =================================================
  235. temperature_columns = [
  236. "mean_temp",
  237. "max_temp",
  238. "min_temp"
  239. ]
  240. missing_count = (
  241. df[temperature_columns]
  242. .isna()
  243. .any(axis=1)
  244. .sum()
  245. )
  246. if missing_count > 0:
  247. missing_dates = df.loc[
  248. df[temperature_columns]
  249. .isna()
  250. .any(axis=1),
  251. "date"
  252. ].dt.strftime(
  253. "%Y-%m-%d"
  254. ).tolist()
  255. raise RuntimeError(
  256. f"存在 {missing_count} 天温度数据为空:"
  257. f"{missing_dates[-10:]}"
  258. )
  259. # =================================================
  260. # 保存单城市文件
  261. # =================================================
  262. df["date"] = df[
  263. "date"
  264. ].dt.strftime("%Y-%m-%d")
  265. df.to_csv(
  266. output_file,
  267. index=False,
  268. encoding="utf-8-sig"
  269. )
  270. print(
  271. f"✓ {city} 获取成功:"
  272. f"{len(df)} 天"
  273. )
  274. print(
  275. f" 数据范围:"
  276. f"{df['date'].iloc[0]} ~ "
  277. f"{df['date'].iloc[-1]}"
  278. )
  279. return df
  280. # =====================================================
  281. # 请求失败
  282. # =====================================================
  283. except Exception as e:
  284. print(
  285. f"✗ {city} 第 {attempt} 次失败:"
  286. f"{type(e).__name__}: {e}"
  287. )
  288. if attempt < max_retries:
  289. # 指数退避
  290. wait = min(
  291. 60,
  292. 5 * (2 ** (attempt - 1))
  293. )
  294. # 随机延迟
  295. wait += random.uniform(0, 3)
  296. print(
  297. f"等待 {wait:.1f} 秒后重试..."
  298. )
  299. time.sleep(wait)
  300. else:
  301. print(
  302. f"✗ {city} 最终失败,"
  303. f"后面会继续处理其他城市"
  304. )
  305. return None
  306. # ============================================================
  307. # 开始下载
  308. # ============================================================
  309. all_data = []
  310. failed = []
  311. for city, (lat, lon) in cities.items():
  312. df = get_city_weather(
  313. city,
  314. lat,
  315. lon
  316. )
  317. if df is not None:
  318. all_data.append(df)
  319. else:
  320. failed.append(city)
  321. # 城市之间稍微等待
  322. time.sleep(2)
  323. # ============================================================
  324. # 合并所有成功数据
  325. # ============================================================
  326. if all_data:
  327. result = pd.concat(
  328. all_data,
  329. ignore_index=True
  330. )
  331. result.to_csv(
  332. FINAL_FILE,
  333. index=False,
  334. encoding="utf-8-sig"
  335. )
  336. print("\n" + "=" * 60)
  337. print("下载完成")
  338. print("=" * 60)
  339. print(f"成功城市:{len(all_data)}")
  340. print(f"失败城市:{len(failed)}")
  341. print(f"总数据量:{len(result):,}")
  342. print(
  343. f"理论数据量:"
  344. f"{expected_days * len(cities):,}"
  345. )
  346. print(
  347. f"最终文件:{FINAL_FILE}"
  348. )
  349. if failed:
  350. print("\n失败城市:")
  351. for city in failed:
  352. print(" -", city)
  353. else:
  354. print(
  355. "没有成功获取任何城市数据"
  356. )
  357. # ============================================================
  358. # 数据检查
  359. # ============================================================
  360. print("\n数据预览:")
  361. if all_data:
  362. print(
  363. result.head(10)
  364. )
  365. print("\n每个城市数据量:")
  366. print(
  367. result.groupby("city").size()
  368. )
  369. print("\n缺失值检查:")
  370. print(
  371. result.isna().sum()
  372. )
  373. print("\n日期范围检查:")
  374. print(
  375. result.groupby("city")["date"]
  376. .agg(["min", "max", "count"])
  377. )