02-31.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import requests
  2. import mysql.connector
  3. from datetime import datetime
  4. import schedule
  5. import time
  6. import json
  7. import gzip
  8. import pytz
  9. from config import Config
  10. conf = Config()
  11. API_KEY = conf.weather_api_key
  12. city_codes = conf.weather_city_codes
  13. BASE_URL = conf.weather_base_url
  14. TZ = pytz.timezone('Asia/Shanghai')
  15. db_config = {
  16. "host": conf.host, "user": conf.user,
  17. "password": conf.password, "database": conf.database,
  18. "charset": "utf8mb4"
  19. }
  20. def connect_db():
  21. return mysql.connector.connect(**db_config)
  22. def fetch_weather_data(city, location):
  23. headers = {
  24. "X-QW-Api-Key": API_KEY,
  25. "Accept-Encoding": "gzip"
  26. }
  27. url = f"{BASE_URL}?location={location}"
  28. try:
  29. response = requests.get(url, headers=headers, timeout=10)
  30. response.raise_for_status()
  31. # requests 已自动解压响应,不要再次 gzip.decompress。
  32. return response.json()
  33. except requests.RequestException as e:
  34. print(f"请求 {city} 天气数据失败: {e}")
  35. return None
  36. except json.JSONDecodeError as e:
  37. print(f"{city} JSON 解析错误: {e}, 响应内容: {response.text[:500]}...")
  38. return None
  39. def get_latest_update_time(cursor, city):
  40. cursor.execute("SELECT MAX(update_time) FROM weather_data WHERE city = %s", (city,))
  41. result = cursor.fetchone()
  42. return result[0] if result[0] else None
  43. def should_update_data(latest_time, force_update=False):
  44. if force_update:
  45. return True
  46. if not latest_time:
  47. return True
  48. current_time = datetime.now(TZ)
  49. latest_time = latest_time.replace(tzinfo=TZ)
  50. return (current_time - latest_time).total_seconds() / 3600 >= 24
  51. def store_weather_data(conn, cursor, city, data):
  52. if not data or data.get("code") != "200":
  53. print(f"{city} 数据无效,跳过存储。")
  54. return
  55. daily_data = data.get("daily", [])
  56. update_time = datetime.fromisoformat(data.get("updateTime").replace("+08:00", "+08:00")).replace(tzinfo=TZ)
  57. for day in daily_data:
  58. fx_date = datetime.strptime(day["fxDate"], "%Y-%m-%d").date()
  59. values = (
  60. city, fx_date,
  61. day.get("sunrise"), day.get("sunset"),
  62. day.get("moonrise"), day.get("moonset"),
  63. day.get("moonPhase"), day.get("moonPhaseIcon"),
  64. day.get("tempMax"), day.get("tempMin"),
  65. day.get("iconDay"), day.get("textDay"),
  66. day.get("iconNight"), day.get("textNight"),
  67. day.get("wind360Day"), day.get("windDirDay"), day.get("windScaleDay"), day.get("windSpeedDay"),
  68. day.get("wind360Night"), day.get("windDirNight"), day.get("windScaleNight"), day.get("windSpeedNight"),
  69. day.get("precip"), day.get("uvIndex"),
  70. day.get("humidity"), day.get("pressure"),
  71. day.get("vis"), day.get("cloud"),
  72. update_time
  73. )
  74. insert_query = """
  75. INSERT INTO weather_data (
  76. city, fx_date, sunrise, sunset, moonrise, moonset, moon_phase, moon_phase_icon,
  77. temp_max, temp_min, icon_day, text_day, icon_night, text_night,
  78. wind360_day, wind_dir_day, wind_scale_day, wind_speed_day,
  79. wind360_night, wind_dir_night, wind_scale_night, wind_speed_night,
  80. precip, uv_index, humidity, pressure, vis, cloud, update_time
  81. ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
  82. ON DUPLICATE KEY UPDATE
  83. sunrise = VALUES(sunrise), sunset = VALUES(sunset), moonrise = VALUES(moonrise),
  84. moonset = VALUES(moonset), moon_phase = VALUES(moon_phase), moon_phase_icon = VALUES(moon_phase_icon),
  85. temp_max = VALUES(temp_max), temp_min = VALUES(temp_min), icon_day = VALUES(icon_day),
  86. text_day = VALUES(text_day), icon_night = VALUES(icon_night), text_night = VALUES(text_night),
  87. wind360_day = VALUES(wind360_day), wind_dir_day = VALUES(wind_dir_day), wind_scale_day = VALUES(wind_scale_day),
  88. wind_speed_day = VALUES(wind_speed_day), wind360_night = VALUES(wind360_night),
  89. wind_dir_night = VALUES(wind_dir_night), wind_scale_night = VALUES(wind_scale_night),
  90. wind_speed_night = VALUES(wind_speed_night), precip = VALUES(precip), uv_index = VALUES(uv_index),
  91. humidity = VALUES(humidity), pressure = VALUES(pressure), vis = VALUES(vis),
  92. cloud = VALUES(cloud), update_time = VALUES(update_time)
  93. """
  94. try:
  95. cursor.execute(insert_query, values)
  96. print(f"{city} {fx_date} 数据写入/更新成功: {day.get('textDay')}, 影响行数: {cursor.rowcount}")
  97. conn.commit()
  98. print(f"{city} 事务提交完成。")
  99. except mysql.connector.Error as e:
  100. print(f"{city} {fx_date} 数据库错误: {e}")
  101. conn.rollback()
  102. print(f"{city} 事务回滚。")
  103. def update_weather(force_update=False):
  104. conn = connect_db()
  105. cursor = conn.cursor()
  106. for city, location in city_codes.items():
  107. latest_time = get_latest_update_time(cursor, city)
  108. if should_update_data(latest_time, force_update):
  109. print(f"开始更新 {city} 天气数据...")
  110. data = fetch_weather_data(city, location)
  111. if data:
  112. store_weather_data(conn, cursor, city, data)
  113. else:
  114. print(f"{city} 数据已为最新,无需更新。最新更新时间: {latest_time}")
  115. cursor.close()
  116. conn.close()
  117. def setup_scheduler():
  118. # 北京时间 1:00 对应 PDT 前一天的 16:00(夏令时)
  119. schedule.every().day.at("16:00").do(update_weather)
  120. while True:
  121. schedule.run_pending()
  122. time.sleep(60)
  123. if __name__ == "__main__":
  124. # 初始检查和更新
  125. with mysql.connector.connect(**db_config) as conn:
  126. cursor = conn.cursor()
  127. cursor.execute("""
  128. CREATE TABLE IF NOT EXISTS weather_data (
  129. id INT AUTO_INCREMENT PRIMARY KEY,
  130. city VARCHAR(50) NOT NULL COMMENT '城市名称',
  131. fx_date DATE NOT NULL COMMENT '预报日期',
  132. sunrise TIME COMMENT '日出时间',
  133. sunset TIME COMMENT '日落时间',
  134. moonrise TIME COMMENT '月升时间',
  135. moonset TIME COMMENT '月落时间',
  136. moon_phase VARCHAR(20) COMMENT '月相名称',
  137. moon_phase_icon VARCHAR(10) COMMENT '月相图标代码',
  138. temp_max INT COMMENT '最高温度',
  139. temp_min INT COMMENT '最低温度',
  140. icon_day VARCHAR(10) COMMENT '白天天气图标代码',
  141. text_day VARCHAR(20) COMMENT '白天天气描述',
  142. icon_night VARCHAR(10) COMMENT '夜间天气图标代码',
  143. text_night VARCHAR(20) COMMENT '夜间天气描述',
  144. wind360_day INT COMMENT '白天风向360角度',
  145. wind_dir_day VARCHAR(20) COMMENT '白天风向',
  146. wind_scale_day VARCHAR(10) COMMENT '白天风力等级',
  147. wind_speed_day INT COMMENT '白天风速 (km/h)',
  148. wind360_night INT COMMENT '夜间风向360角度',
  149. wind_dir_night VARCHAR(20) COMMENT '夜间风向',
  150. wind_scale_night VARCHAR(10) COMMENT '夜间风力等级',
  151. wind_speed_night INT COMMENT '夜间风速 (km/h)',
  152. precip DECIMAL(5,1) COMMENT '降水量 (mm)',
  153. uv_index INT COMMENT '紫外线指数',
  154. humidity INT COMMENT '相对湿度 (%)',
  155. pressure INT COMMENT '大气压强 (hPa)',
  156. vis INT COMMENT '能见度 (km)',
  157. cloud INT COMMENT '云量 (%)',
  158. update_time DATETIME COMMENT '数据更新时间',
  159. UNIQUE KEY unique_city_date (city, fx_date)
  160. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='天气数据表'
  161. """)
  162. conn.commit()
  163. # 立即执行一次更新
  164. update_weather()
  165. # 启动定时任务
  166. setup_scheduler()