Spaces:
Sleeping
Sleeping
File size: 9,310 Bytes
139ddee 071beff 139ddee 071beff 139ddee 417ae3a 139ddee 97ce441 139ddee 97ce441 139ddee 97ce441 139ddee 97ce441 139ddee 97ce441 139ddee 97ce441 139ddee 97ce441 139ddee 97ce441 139ddee |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# cwa_service.py
# -*- coding: utf-8 -*-
from __future__ import annotations
import requests
import re
import pandas as pd
from datetime import datetime, timedelta, timezone
from config import CWA_API_KEY, CWA_ALARM_API, CWA_SIGNIFICANT_API, CWA_LOCAL_EQ_API
TAIPEI_TZ = timezone(timedelta(hours=8))
# --- Helper Functions ---
def _to_float(x):
if x is None: return None
s = str(x).strip()
m = re.search(r"[-+]?\d+(?:\.\d+)?", s)
return float(m.group()) if m else None
def _parse_cwa_time(s: str) -> tuple[str, str]:
if not s: return ("未知", "未知")
try:
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
tw = dt.astimezone(TAIPEI_TZ).strftime("%Y-%m-%d %H:%M")
utc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M")
return (tw, utc)
except Exception:
return (s, "未知")
def _normalize_cwa_area_name(area_name: str) -> str:
"""自動校正縣市名稱,以符合 CWA API 查詢需求"""
area_name = area_name.replace("台", "臺")
if area_name.endswith("市") or area_name.endswith("縣"):
return area_name
major_cities = "臺北,新北,基隆,桃園,新竹,臺中,嘉義,臺南,高雄".split(',')
if any(city in area_name for city in major_cities):
return f"{area_name}市"
else:
return f"{area_name}縣"
# --- 地震預警 (CWA_ALARM_API) ---
def fetch_cwa_alarm_list(limit: int = 5) -> str:
"""抓 CWA 地震預警並格式化輸出。"""
try:
r = requests.get(CWA_ALARM_API, timeout=10)
r.raise_for_status()
payload = r.json()
except Exception as e:
return f"❌ 地震預警查詢失敗:{e}"
items = payload.get("data", [])
if not items:
return "✅ 目前沒有地震預警。"
def _key(it):
try:
return datetime.fromisoformat(it.get("originTime", "").replace("Z", "+00:00"))
except:
return datetime.min.replace(tzinfo=timezone.utc)
items = sorted(items, key=_key, reverse=True)
lines = ["🚨 地震預警(最新):", "-" * 20]
for it in items[:limit]:
mag = _to_float(it.get("magnitudeValue"))
depth = _to_float(it.get("depth"))
tw_str, _ = _parse_cwa_time(it.get("originTime", ""))
identifier = str(it.get('identifier', '—')).replace('{', '{{').replace('}', '}}')
msg_type = str(it.get('msgType', '—')).replace('{', '{{').replace('}', '}}')
msg_no = str(it.get('msgNo', '—')).replace('{', '{{').replace('}', '}}')
areas = str(it.get('alertAreas') or '—').replace('{', '{{').replace('}', '}}')
mag_str = f"{mag:.1f}" if mag is not None else "—"
depth_str = f"{depth:.0f}" if depth is not None else "—"
lines.append(
f"事件: {identifier} | 類型: {msg_type}#{msg_no}\n"
f"規模/深度: M{mag_str} / {depth_str} km\n"
f"時間: {tw_str}(台灣)\n"
f"預警地區: {areas}"
)
return "\n\n".join(lines).strip()
# --- 顯著有感地震 (E-A0015-001) ---
def _parse_significant_earthquakes(obj: dict) -> pd.DataFrame:
"""最穩健的解析邏輯,供 /significant 和 /latest 共用"""
records = obj.get("records") or obj.get("Records") or {}
quakes = records.get("earthquake") or records.get("Earthquake") or []
rows = []
for q in quakes:
ei = q.get("EarthquakeInfo") or q.get("earthquakeInfo") or {}
epic = ei.get("Epicenter") or ei.get("epicenter") or {}
mag_info = ei.get("Magnitude") or ei.get("magnitude") or ei.get("EarthquakeMagnitude") or {}
depth_raw = ei.get("FocalDepth") or ei.get("depth") or ei.get("Depth")
mag_raw = mag_info.get("MagnitudeValue") or mag_info.get("magnitudeValue") or mag_info.get("Value") or mag_info.get("value")
rows.append({
"ID": q.get("EarthquakeNo"), "Time": ei.get("OriginTime"),
"Lat": _to_float(epic.get("EpicenterLatitude") or epic.get("epicenterLatitude")),
"Lon": _to_float(epic.get("EpicenterLongitude") or epic.get("epicenterLongitude")),
"Depth": _to_float(depth_raw), "Magnitude": _to_float(mag_raw),
"Location": epic.get("Location") or epic.get("location"),
"URL": q.get("Web") or q.get("ReportURL"),
})
df = pd.DataFrame(rows)
if not df.empty and "Time" in df.columns:
time_series = pd.to_datetime(df["Time"], errors="coerce")
if pd.api.types.is_datetime64_any_dtype(time_series):
df["Time"] = time_series.dt.tz_localize("UTC").dt.tz_convert(TAIPEI_TZ)
return df
def fetch_significant_earthquakes(days: int = 7, limit: int = 5) -> str:
if not CWA_API_KEY: return "❌ 顯著地震查詢失敗:管理者尚未設定 CWA_API_KEY。"
now = datetime.now(timezone.utc)
time_from = (now - timedelta(days=days)).strftime("%Y-%m-%d")
params = {"Authorization": CWA_API_KEY, "format": "JSON", "timeFrom": time_from}
try:
r = requests.get(CWA_SIGNIFICANT_API, params=params, timeout=15)
r.raise_for_status()
data = r.json()
df = _parse_significant_earthquakes(data)
if df.empty: return f"✅ 過去 {days} 天內沒有顯著有感地震報告。"
df = df.sort_values(by="Time", ascending=False).head(limit)
lines = [f"🚨 CWA 最新顯著有感地震 (近{days}天內):", "-" * 20]
for _, row in df.iterrows():
mag_str = f"{row['Magnitude']:.1f}" if pd.notna(row['Magnitude']) else "—"
depth_str = f"{row['Depth']:.0f}" if pd.notna(row['Depth']) else "—"
lines.append(
f"時間: {row['Time'].strftime('%Y-%m-%d %H:%M') if pd.notna(row['Time']) else '—'}\n"
f"地點: {row['Location'] or '—'}\n"
f"規模: M{mag_str} | 深度: {depth_str} km\n"
f"報告: {row['URL'] or '無'}"
)
return "\n\n".join(lines)
except Exception as e:
return f"❌ 顯著地震查詢失敗:{e}"
# --- 最新一筆顯著地震 ---
def fetch_latest_significant_earthquake() -> dict | None:
"""從 CWA 獲取最新一筆顯著地震,並重用現有的解析邏輯"""
if not CWA_API_KEY:
raise ValueError("錯誤:尚未設定 CWA_API_KEY Secret。")
now = datetime.now(timezone.utc)
time_from = (now - timedelta(days=2)).strftime("%Y-%m-%dT%H:%M:%S")
params = {"Authorization": CWA_API_KEY, "format": "JSON", "limit": 1}
r = requests.get(CWA_SIGNIFICANT_API, params=params, timeout=15)
r.raise_for_status()
data = r.json()
df = _parse_significant_earthquakes(data)
if df.empty:
return None
latest_eq_data = df.sort_values(by="Time", ascending=False).iloc[0].to_dict()
quakes = data.get("records", {}).get("Earthquake", [])
if quakes:
latest_eq_data["ImageURL"] = quakes[0].get("ReportImageURI")
if pd.notna(latest_eq_data.get("Time")):
latest_eq_data["TimeStr"] = latest_eq_data["Time"].strftime('%Y-%m-%d %H:%M')
return latest_eq_data
# --- 小區域有感地震 (E-A0016-001) ---
def fetch_local_earthquakes(area_name: str = "", limit: int = 5) -> str:
"""從 CWA 獲取小區域有感地震報告。如果 area_name 為空,則查詢全台灣。"""
if not CWA_API_KEY: return "❌ 查詢失敗:管理者尚未設定 CWA_API_KEY。"
params = {"Authorization": CWA_API_KEY, "format": "JSON", "limit": limit}
title = "🇹🇼 台灣近期小區域有感地震:"
if area_name:
normalized_area = _normalize_cwa_area_name(area_name)
params["AreaName"] = normalized_area
title = f"🚨 「{normalized_area}」近期小區域有感地震:"
try:
r = requests.get(CWA_LOCAL_EQ_API, params=params, timeout=15)
r.raise_for_status()
data = r.json()
earthquakes = data.get("records", {}).get("Earthquake", [])
if not earthquakes:
msg = f"✅ 在「{area_name}」" if area_name else "✅ 台灣"
return f"{msg}近期沒有小區域有感地震報告。"
lines = [title, "-" * 20]
for eq in earthquakes:
info = eq.get("earthquakeInfo", {})
epi = info.get("epicenter", {})
mag = info.get("magnitude", {})
depth = info.get("depth", {})
intensity_areas = eq.get("intensity", {}).get("shakingArea", [])
area_strs = [f"{area.get('areaDesc')} {area.get('areaIntensity')}級" for area in intensity_areas if area.get("areaIntensity") and float(area.get("areaIntensity", 0)) > 0]
intensity_str = "、".join(area_strs) if area_strs else "無具體震度回報"
lines.append(
f"報告: {eq.get('reportContent', '—')}\n"
f"時間: {info.get('originTime', '—')}\n"
f"地點: {epi.get('location', '—')}\n"
f"規模: M{mag.get('magnitudeValue', '—')} | 深度: {depth.get('value', '—')} km\n"
f"主要影響區域: {intensity_str}"
)
return "\n\n".join(lines)
except Exception as e:
return f"❌ 小區域地震查詢失敗:{e}"
|