cwadayi commited on
Commit
13b5faf
·
verified ·
1 Parent(s): 25776c0

Update cwa_service.py

Browse files
Files changed (1) hide show
  1. cwa_service.py +167 -75
cwa_service.py CHANGED
@@ -1,87 +1,179 @@
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cwa_service.py (修正與優化版本)
2
 
 
3
  import requests
4
+ import re
5
+ import pandas as pd # 請確保 'pandas' 已加入 requirements.txt
6
+ from datetime import datetime, timedelta, timezone
7
+ from config import CWA_API_KEY, CWA_ALARM_API, CWA_SIGNIFICANT_API
8
 
9
+ # 定義台北時區 (UTC+8)
10
+ TAIPEI_TZ = timezone(timedelta(hours=8))
11
 
12
+ def _to_float(x) -> float | None:
13
+ """將輸入值穩健地轉換為浮點數,從字串中提取第一個數字。"""
14
+ if x is None: return None
15
+ s = str(x).strip()
16
+ m = re.search(r"[-+]?\d+(?:\.\d+)?", s)
17
+ return float(m.group()) if m else None
18
 
19
+ def _parse_cwa_time(s: str) -> tuple[str, str]:
 
 
 
20
  """
21
+ 穩健地解析 CWA API 可能回傳的兩種時間格式 (ISO 格式或本地時間格式)。
22
+ 返回 (台北時間字串, UTC時間字串)。
 
 
23
  """
24
+ if not s: return ("未知", "未知")
25
+ dt_utc = None
26
  try:
27
+ # 嘗試解析 ISO 8601 格式 (例如 "2025-08-22T14:06:15Z")
28
+ dt_utc = datetime.fromisoformat(s.replace("Z", "+00:00"))
29
+ except ValueError:
30
+ # 如果失敗,嘗試解析本地時間格式 (例如 "2025-08-22 14:06:15")
31
+ try:
32
+ dt_local = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
33
+ # 假設此格式為台北時間,並轉換為標準 UTC 時間
34
+ dt_local = dt_local.replace(tzinfo=TAIPEI_TZ)
35
+ dt_utc = dt_local.astimezone(timezone.utc)
36
+ except Exception:
37
+ # 如果兩種格式都失敗,直接返回原始字串
38
+ return (s, "未知")
39
+
40
+ if dt_utc:
41
+ tw_str = dt_utc.astimezone(TAIPEI_TZ).strftime("%Y-%m-%d %H:%M")
42
+ utc_str = dt_utc.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M")
43
+ return (tw_str, utc_str)
44
+
45
+ return (s, "未知")
46
 
47
+ def fetch_cwa_alarm_list(limit: int = 5) -> str:
48
+ """獲取最新的地震預警列表。"""
49
+ try:
50
+ r = requests.get(CWA_ALARM_API, timeout=10)
51
+ r.raise_for_status()
52
+ payload = r.json()
53
+ except Exception as e:
54
+ return f"❌ 地震預警查詢失敗:{e}"
55
+
56
+ items = payload.get("data", [])
57
+ if not items: return "✅ 目前沒有地震預警。"
58
+
59
+ # 依據時間排序,確保最新的在最前面
60
+ def _key(it):
61
+ try: return datetime.fromisoformat(it.get("originTime", "").replace("Z", "+00:00"))
62
+ except: return datetime.min.replace(tzinfo=timezone.utc)
63
+ items = sorted(items, key=_key, reverse=True)
64
+
65
+ lines = ["🚨 地震預警(最新):", "-" * 20]
66
+ for it in items[:limit]:
67
+ mag = _to_float(it.get("magnitudeValue"))
68
+ depth = _to_float(it.get("depth"))
69
+ tw_str, _ = _parse_cwa_time(it.get("originTime", ""))
70
+
71
+ # [修正] 移除不必要的 .replace('{', '{{'),直接使用 str() 即可
72
+ identifier = str(it.get('identifier', '—'))
73
+ msg_type = str(it.get('msgType', '—'))
74
+ msg_no = str(it.get('msgNo', '—'))
75
+
76
+ location_desc_list = it.get('locationDesc')
77
+ areas = ", ".join(str(area) for area in location_desc_list) if isinstance(location_desc_list, list) and location_desc_list else "—"
78
+
79
+ mag_str = f"{mag:.1f}" if mag is not None else "—"
80
+ depth_str = f"{depth:.0f}" if depth is not None else "—"
81
+
82
+ lines.append(
83
+ f"事件: {identifier} | 類型: {msg_type}#{msg_no}\n"
84
+ f"規模/深度: M{mag_str} / {depth_str} km\n"
85
+ f"時間: {tw_str}(台灣)\n"
86
+ f"地點: {areas}"
87
+ )
88
+ return "\n\n".join(lines).strip()
89
+
90
+ def _parse_significant_earthquakes(obj: dict) -> pd.DataFrame:
91
+ """從 CWA API 的 JSON 回應中解析出顯著地震資料,並轉換為 Pandas DataFrame。"""
92
+ records = obj.get("records", {})
93
+ quakes = records.get("Earthquake", [])
94
+ rows = []
95
+ for q in quakes:
96
+ ei = q.get("EarthquakeInfo", {})
97
+
98
+ # 使用非常防禦性的方式取得資料,以應對 API 欄位名稱大小寫不一致或變動的問題
99
+ epic = ei.get("Epicenter") or ei.get("epicenter") or {}
100
+ mag_info = ei.get("Magnitude") or ei.get("magnitude") or {}
101
+ depth_raw = ei.get("FocalDepth") or ei.get("depth")
102
+ mag_raw = mag_info.get("MagnitudeValue") or mag_info.get("magnitudeValue")
103
+
104
+ rows.append({
105
+ "ID": q.get("EarthquakeNo"), "Time": ei.get("OriginTime"),
106
+ "Lat": _to_float(epic.get("EpicenterLatitude") or epic.get("epicenterLatitude")),
107
+ "Lon": _to_float(epic.get("EpicenterLongitude") or epic.get("epicenterLongitude")),
108
+ "Depth": _to_float(depth_raw),
109
+ "Magnitude": _to_float(mag_raw),
110
+ "Location": epic.get("Location") or epic.get("location"),
111
+ "URL": q.get("Web") or q.get("ReportURL"),
112
+ "ImageURL": q.get("ReportImageURI"), # 也順便解析圖片 URL
113
+ })
114
+
115
+ df = pd.DataFrame(rows)
116
+ if not df.empty and "Time" in df.columns:
117
+ # 使用 pandas 內建功能將時間欄位轉換為帶有時區的 datetime 物件
118
+ df["Time"] = pd.to_datetime(df["Time"], errors="coerce", utc=True).dt.tz_convert(TAIPEI_TZ)
119
+ return df
120
+
121
+ def fetch_significant_earthquakes(days: int = 7, limit: int = 5) -> str:
122
+ """獲取過去 N 天內的顯著地震列表。"""
123
+ if not CWA_API_KEY: return "❌ 顯著地震查詢失敗:管理者尚未設定 CWA_API_KEY。"
124
+
125
+ now = datetime.now(timezone.utc)
126
+ time_from = (now - timedelta(days=days)).strftime("%Y-%m-%d")
127
+ params = {"Authorization": CWA_API_KEY, "format": "JSON", "timeFrom": time_from}
128
+
129
+ try:
130
+ r = requests.get(CWA_SIGNIFICANT_API, params=params, timeout=15)
131
+ r.raise_for_status()
132
+ data = r.json()
133
+ df = _parse_significant_earthquakes(data)
134
+
135
+ if df.empty: return f"✅ 過去 {days} 天內沒有顯著有感地震報告。"
136
+
137
+ # 依時間排序並取前 N 筆
138
+ df = df.sort_values(by="Time", ascending=False).head(limit)
139
+
140
+ lines = [f"🚨 CWA 最新顯著有感地震 (近{days}天内):", "-" * 20]
141
+ for _, row in df.iterrows():
142
+ mag_str = f"{row['Magnitude']:.1f}" if pd.notna(row['Magnitude']) else "—"
143
+ depth_str = f"{row['Depth']:.0f}" if pd.notna(row['Depth']) else "—"
144
+ lines.append(
145
+ f"時間: {row['Time'].strftime('%Y-%m-%d %H:%M') if pd.notna(row['Time']) else '—'}\n"
146
+ f"地點: {row['Location'] or '—'}\n"
147
+ f"規模: M{mag_str} | 深度: {depth_str} km\n"
148
+ f"報告: {row['URL'] or '無'}"
149
  )
150
+ return "\n\n".join(lines)
 
 
 
 
 
 
 
 
 
 
151
  except Exception as e:
152
+ return f" 顯著地震查詢失敗:{e}"
 
153
 
154
+ def fetch_latest_significant_earthquake() -> dict | None:
155
+ """獲取最新的一筆顯著地震的詳細資料 (包含圖片)。"""
156
+ try:
157
+ if not CWA_API_KEY:
158
+ raise ValueError("錯誤:尚未設定 CWA_API_KEY Secret。")
159
+
160
+ params = {"Authorization": CWA_API_KEY, "format": "JSON", "limit": 1, "orderby": "OriginTime desc"}
161
+ r = requests.get(CWA_SIGNIFICANT_API, params=params, timeout=15)
162
+ r.raise_for_status()
163
+ data = r.json()
164
+ df = _parse_significant_earthquakes(data)
165
+
166
+ if df.empty:
167
+ return None
168
+
169
+ latest_eq_data = df.iloc[0].to_dict()
170
+
171
+ # 將 Time object 轉換為格式化的字串,方便後續使用
172
+ if pd.notna(latest_eq_data.get("Time")):
173
+ latest_eq_data["TimeStr"] = latest_eq_data["Time"].strftime('%Y-%m-%d %H:%M')
174
+
175
+ return latest_eq_data
176
+ except Exception as e:
177
+ # [優化] 發生錯誤時,印出日誌並回傳 None,避免程式崩潰
178
+ print(f"[錯誤] 獲取最新顯著地震失敗: {e}")
179
+ return None