cwadayi commited on
Commit
31af63f
·
verified ·
1 Parent(s): fe4a3b8

Update cwa_service.py

Browse files
Files changed (1) hide show
  1. cwa_service.py +25 -57
cwa_service.py CHANGED
@@ -1,84 +1,62 @@
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"
@@ -88,55 +66,48 @@ def fetch_cwa_alarm_list(limit: int = 5) -> str:
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 "—"
@@ -152,28 +123,25 @@ def fetch_significant_earthquakes(days: int = 7, limit: int = 5) -> str:
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
 
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"
 
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 "—"
 
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
+