cwadayi commited on
Commit
eaddbe2
·
verified ·
1 Parent(s): 3240b7c

Update cwa_service.py

Browse files
Files changed (1) hide show
  1. cwa_service.py +77 -137
cwa_service.py CHANGED
@@ -1,147 +1,87 @@
1
- # cwa_service.py (Final Defensive Parsing Version)
2
- import requests
3
- import re
4
- import pandas as pd
5
- from datetime import datetime, timedelta, timezone
6
- from config import CWA_API_KEY, CWA_ALARM_API, CWA_SIGNIFICANT_API
7
-
8
- TAIPEI_TZ = timezone(timedelta(hours=8))
9
 
10
- def _to_float(x):
11
- if x is None: return None
12
- s = str(x).strip()
13
- m = re.search(r"[-+]?\d+(?:\.\d+)?", s)
14
- return float(m.group()) if m else None
15
 
16
- def _parse_cwa_time(s: str) -> tuple[str, str]:
17
- if not s: return ("未知", "未知")
18
- dt_utc = None
19
- try:
20
- dt_utc = datetime.fromisoformat(s.replace("Z", "+00:00"))
21
- except ValueError:
22
- try:
23
- dt_local = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
24
- dt_local = dt_local.replace(tzinfo=TAIPEI_TZ)
25
- dt_utc = dt_local.astimezone(timezone.utc)
26
- except Exception:
27
- return (s, "未知")
28
- if dt_utc:
29
- tw_str = dt_utc.astimezone(TAIPEI_TZ).strftime("%Y-%m-%d %H:%M")
30
- utc_str = dt_utc.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M")
31
- return (tw_str, utc_str)
32
- return (s, "未知")
33
 
34
- def fetch_cwa_alarm_list(limit: int = 5) -> str:
35
- try:
36
- r = requests.get(CWA_ALARM_API, timeout=10)
37
- r.raise_for_status()
38
- payload = r.json()
39
- except Exception as e:
40
- return f"❌ 地震預警查詢失敗:{e}"
41
- items = payload.get("data", [])
42
- if not items: return "✅ 目前沒有地震預警。"
43
- def _key(it):
44
- try: return datetime.fromisoformat(it.get("originTime", "").replace("Z", "+00:00"))
45
- except: return datetime.min.replace(tzinfo=timezone.utc)
46
- items = sorted(items, key=_key, reverse=True)
47
- lines = ["🚨 地震預警(最新):", "-" * 20]
48
- for it in items[:limit]:
49
- mag = _to_float(it.get("magnitudeValue"))
50
- depth = _to_float(it.get("depth"))
51
- tw_str, _ = _parse_cwa_time(it.get("originTime", ""))
52
- identifier = str(it.get('identifier', '—')).replace('{', '{{').replace('}', '}}')
53
- msg_type = str(it.get('msgType', '—')).replace('{', '{{').replace('}', '}}')
54
- msg_no = str(it.get('msgNo', '—')).replace('{', '{{').replace('}', '}}')
55
- location_desc_list = it.get('locationDesc')
56
- areas_str = ", ".join(str(area) for area in location_desc_list) if isinstance(location_desc_list, list) and location_desc_list else "—"
57
- areas = areas_str.replace('{', '{{').replace('}', '}}')
58
- mag_str = f"{mag:.1f}" if mag is not None else "—"
59
- depth_str = f"{depth:.0f}" if depth is not None else "—"
60
- lines.append(
61
- f"事件: {identifier} | 類型: {msg_type}#{msg_no}\n"
62
- f"規模/深度: M{mag_str} / {depth_str} km\n"
63
- f"時間: {tw_str}(台灣)\n"
64
- f"地點: {areas}"
65
- )
66
- return "\n\n".join(lines).strip()
67
 
68
- def _parse_significant_earthquakes(obj: dict) -> pd.DataFrame:
69
- records = obj.get("records", {})
70
- quakes = records.get("Earthquake", [])
71
- rows = []
72
- for q in quakes:
73
- # [偵錯] 如果需要,可以取消下面這行的註解,它會在 Log 中印出最原始的資料
74
- # print(f"原始地震資料: {q}")
75
-
76
- ei = q.get("EarthquakeInfo", {})
77
-
78
- # [修正] 使用更穩健的方式取得所有資料,檢查所有已知的大小寫和備用名稱
79
- epic = ei.get("Epicenter") or ei.get("epicenter") or {}
80
- mag_info = ei.get("Magnitude") or ei.get("magnitude") or ei.get("EarthquakeMagnitude") or {}
81
- depth_raw = ei.get("FocalDepth") or ei.get("depth") or ei.get("Depth")
82
- mag_raw = mag_info.get("MagnitudeValue") or mag_info.get("magnitudeValue") or mag_info.get("Value") or mag_info.get("value")
83
-
84
- rows.append({
85
- "ID": q.get("EarthquakeNo"), "Time": ei.get("OriginTime"),
86
- "Lat": _to_float(epic.get("EpicenterLatitude") or epic.get("epicenterLatitude")),
87
- "Lon": _to_float(epic.get("EpicenterLongitude") or epic.get("epicenterLongitude")),
88
- "Depth": _to_float(depth_raw),
89
- "Magnitude": _to_float(mag_raw),
90
- "Location": epic.get("Location") or epic.get("location"),
91
- "URL": q.get("Web") or q.get("ReportURL"),
92
- })
93
-
94
- df = pd.DataFrame(rows)
95
- if not df.empty and "Time" in df.columns:
96
- df["Time"] = pd.to_datetime(df["Time"], errors="coerce", utc=True).dt.tz_convert(TAIPEI_TZ)
97
- return df
98
 
99
- def fetch_significant_earthquakes(days: int = 7, limit: int = 5) -> str:
100
- if not CWA_API_KEY: return "❌ 顯著地震查詢失敗:管理者尚未設定 CWA_API_KEY。"
101
- now = datetime.now(timezone.utc)
102
- time_from = (now - timedelta(days=days)).strftime("%Y-%m-%d")
103
- params = {"Authorization": CWA_API_KEY, "format": "JSON", "timeFrom": time_from}
104
- try:
105
- r = requests.get(CWA_SIGNIFICANT_API, params=params, timeout=15)
106
- r.raise_for_status()
107
- data = r.json()
108
- df = _parse_significant_earthquakes(data)
109
- if df.empty: return f"✅ 過去 {days} 天內沒有顯著有感地震報告。"
110
- df = df.sort_values(by="Time", ascending=False).head(limit)
111
- lines = [f"🚨 CWA 最新顯著有感地震 (近{days}天内):", "-" * 20]
112
- for _, row in df.iterrows():
113
- mag_str = f"{row['Magnitude']:.1f}" if pd.notna(row['Magnitude']) else "—"
114
- depth_str = f"{row['Depth']:.0f}" if pd.notna(row['Depth']) else "—"
115
- lines.append(
116
- f"時間: {row['Time'].strftime('%Y-%m-%d %H:%M') if pd.notna(row['Time']) else '—'}\n"
117
- f"地點: {row['Location'] or '—'}\n"
118
- f"規模: M{mag_str} | 深度: {depth_str} km\n"
119
- f"報告: {row['URL'] or '無'}"
120
- )
121
- return "\n\n".join(lines)
122
- except Exception as e:
123
- return f"❌ 顯著地震查詢失敗:{e}"
124
 
125
- def fetch_latest_significant_earthquake() -> dict | None:
 
 
126
  try:
127
- if not CWA_API_KEY: raise ValueError("錯誤:尚未設定 CWA_API_KEY Secret。")
128
- params = {"Authorization": CWA_API_KEY, "format": "JSON", "limit": 1, "orderby": "OriginTime desc"}
129
- r = requests.get(CWA_SIGNIFICANT_API, params=params, timeout=15)
130
- r.raise_for_status()
131
- data = r.json()
132
- df = _parse_significant_earthquakes(data)
133
- if df.empty: return None
134
 
135
- latest_eq_data = df.iloc[0].to_dict()
136
-
137
- quakes = data.get("records", {}).get("Earthquake", [])
138
- if quakes:
139
- latest_eq_data["ImageURL"] = quakes[0].get("ReportImageURI")
140
-
141
- if pd.notna(latest_eq_data.get("Time")):
142
- latest_eq_data["TimeStr"] = latest_eq_data["Time"].strftime('%Y-%m-%d %H:%M')
143
-
144
- return latest_eq_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  except Exception as e:
146
- raise e
 
147
 
 
 
 
 
 
 
 
 
1
+ # cwa_service.py
 
 
 
 
 
 
 
2
 
3
+ import os
4
+ import requests
5
+ from datetime import datetime, timezone, timedelta
6
+ from dotenv import load_dotenv
 
7
 
8
+ # .env 文件加載環境變數
9
+ load_dotenv()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ # 從環境變數中獲取 CWA 授權碼
12
+ CWA_API_KEY = os.getenv('CWA_API_KEY')
13
+ if not CWA_API_KEY:
14
+ raise ValueError("CWA_API_KEY not found in environment variables.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ # CWA API 端點
17
+ CWA_API_URL = f"https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001?Authorization={CWA_API_KEY}&format=JSON"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ def get_latest_earthquake():
20
+ """
21
+ CWA API 獲取最新的有感地震資訊。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ Returns:
24
+ tuple: 包含地震詳細資訊的訊息和報告圖片的 URL。如果出錯則返回錯誤訊息和 None。
25
+ """
26
  try:
27
+ response = requests.get(CWA_API_URL)
28
+ response.raise_for_status() # 如果請求失敗 (狀態碼不是 2xx),則引發異常
29
+ data = response.json()
 
 
 
 
30
 
31
+ if data.get('success') == 'true' and data['records']['Earthquake']:
32
+ latest_earthquake = data['records']['Earthquake'][0]
33
+
34
+ # --- 時間處理 ---
35
+ # 1. 取得 API 回傳的 UTC 時間字串 (格式: YYYY-MM-DDTHH:MM:SSZ)
36
+ origin_time_utc_str = latest_earthquake['EarthquakeInfo']['OriginTime']
37
+
38
+ # 2. 將 UTC 字串轉換為帶有時區資訊的 datetime 物件
39
+ # .replace(tzinfo=timezone.utc) 是為了明確告知這個時間是 UTC
40
+ utc_time = datetime.strptime(origin_time_utc_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
41
+
42
+ # 3. 定義台北時區 (UTC+8)
43
+ taipei_tz = timezone(timedelta(hours=8))
44
+
45
+ # 4. 將 UTC 時間轉換為台北時間
46
+ taipei_time = utc_time.astimezone(taipei_tz)
47
+
48
+ # 5. 格式化為易於閱讀的字串
49
+ formatted_time = taipei_time.strftime('%Y-%m-%d %H:%M:%S')
50
+
51
+ # --- 其他資訊處理 ---
52
+ location = latest_earthquake['EarthquakeInfo']['Epicenter']['Location']
53
+ magnitude_value = latest_earthquake['EarthquakeInfo']['Magnitude']['MagnitudeValue']
54
+ depth_value = latest_earthquake['EarthquakeInfo']['FocalDepth']
55
+ report_url = latest_earthquake.get('Web', '#') # 如果 Web 欄位不存在,給予一個預設值
56
+ report_image_url = latest_earthquake.get('ReportImageURI', None)
57
+
58
+ # 組合訊息
59
+ message = (
60
+ f"🚨 CWA 最新顯著有感地震\n\n"
61
+ f"時間: {formatted_time}\n"
62
+ f"地點: {location}\n"
63
+ f"規模: M{magnitude_value} | 深度: {depth_value} km\n"
64
+ f"報告: {report_url}"
65
+ )
66
+
67
+ return message, report_image_url
68
+ else:
69
+ return "目前無法獲取最新的地震資訊。", None
70
+
71
+ except requests.exceptions.RequestException as e:
72
+ print(f"Error fetching data from CWA API: {e}")
73
+ return f"連接中央氣象署 API 時發生錯誤: {e}", None
74
+ except (KeyError, IndexError) as e:
75
+ print(f"Error parsing CWA API response: {e}")
76
+ return "解析地震資料時發生錯誤,可能是資料格式有變。", None
77
  except Exception as e:
78
+ print(f"An unexpected error occurred: {e}")
79
+ return f"發生未知錯誤: {e}", None
80
 
81
+ # 你可以在本地端測試這個函式
82
+ if __name__ == '__main__':
83
+ message, image_url = get_latest_earthquake()
84
+ print("--- 訊息 ---")
85
+ print(message)
86
+ print("\n--- 圖片 URL ---")
87
+ print(image_url)