cwadayi commited on
Commit
89f6652
·
verified ·
1 Parent(s): 595e888

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +103 -54
app.py CHANGED
@@ -1,22 +1,32 @@
1
  import os
2
  import io
3
  import tempfile
 
 
4
  import requests
5
  import pandas as pd
6
  import matplotlib.pyplot as plt
7
  import gradio as gr
8
- from datetime import datetime, timedelta, timezone
 
 
 
 
 
 
9
 
10
  # -----------------------------
11
  # 台北時區 (UTC+8)
12
  # -----------------------------
13
  TAIPEI_TZ = timezone(timedelta(hours=8))
14
 
 
15
  def _fmt(dt: datetime) -> str:
16
  return dt.strftime("%Y-%m-%dT%H:%M:%S")
17
 
 
18
  def set_time_range(hours=None, days=None):
19
- """依台北時間回傳 (timeFrom, timeTo)"""
20
  now = datetime.now(TAIPEI_TZ)
21
  if hours is not None:
22
  t_from = now - timedelta(hours=hours)
@@ -26,11 +36,13 @@ def set_time_range(hours=None, days=None):
26
  t_from = now - timedelta(days=3)
27
  return _fmt(t_from), _fmt(now)
28
 
 
29
  # -----------------------------
30
  # 取 API 資料
31
  # -----------------------------
32
  API_URL = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"
33
 
 
34
  def fetch_reports(time_from, time_to):
35
  api_key = os.getenv("CWA_API_KEY", "").strip()
36
  if not api_key:
@@ -40,6 +52,7 @@ def fetch_reports(time_from, time_to):
40
  r.raise_for_status()
41
  return r.json()
42
 
 
43
  # -----------------------------
44
  # 解析 JSON → DataFrame(彈性容錯)
45
  # -----------------------------
@@ -52,6 +65,7 @@ def _safe_get(d, *keys, default=None):
52
  return default
53
  return cur
54
 
 
55
  def _to_float(x):
56
  try:
57
  if x is None or str(x).strip() == "":
@@ -60,14 +74,14 @@ def _to_float(x):
60
  except Exception:
61
  return None
62
 
 
63
  def parse_ea0015(obj):
64
  """
65
- 支援 records/Records、earthquake/Earthquake 等大小寫差異與常見欄位。
66
  輸出欄位:OriginTime, Lat, Lon, Depth_km, Magnitude, Location, ReportURL
67
  """
68
  records = obj.get("records") or obj.get("Records") or {}
69
  quakes = records.get("earthquake") or records.get("Earthquake") or records.get("data") or []
70
-
71
  if not isinstance(quakes, list):
72
  quakes = []
73
 
@@ -79,14 +93,8 @@ def parse_ea0015(obj):
79
  or _safe_get(q, "earthquakeInfo", "originTime")
80
  or _safe_get(q, "EarthquakeInfo", "OriginTime")
81
  )
82
- lat = (
83
- _safe_get(q, "epicenter", "epicenterLat")
84
- or _safe_get(q, "Epicenter", "EpicenterLat")
85
- )
86
- lon = (
87
- _safe_get(q, "epicenter", "epicenterLon")
88
- or _safe_get(q, "Epicenter", "EpicenterLon")
89
- )
90
  depth = _safe_get(q, "depth") or _safe_get(q, "Depth")
91
  mag = (
92
  _safe_get(q, "magnitude", "magnitudeValue")
@@ -94,21 +102,20 @@ def parse_ea0015(obj):
94
  or _safe_get(q, "magnitude", "magnitude")
95
  or _safe_get(q, "Magnitude", "Magnitude")
96
  )
97
- loc = (
98
- _safe_get(q, "epicenter", "location")
99
- or _safe_get(q, "Epicenter", "Location")
100
- )
101
  url = _safe_get(q, "reportURL") or _safe_get(q, "ReportURL")
102
 
103
- rows.append({
104
- "OriginTime": origin,
105
- "Lat": _to_float(lat),
106
- "Lon": _to_float(lon),
107
- "Depth_km": _to_float(depth),
108
- "Magnitude": _to_float(mag),
109
- "Location": loc,
110
- "ReportURL": url
111
- })
 
 
112
 
113
  df = pd.DataFrame(rows)
114
  if not df.empty:
@@ -116,10 +123,18 @@ def parse_ea0015(obj):
116
  df = df.sort_values("OriginTime", ascending=False, na_position="last").reset_index(drop=True)
117
  return df
118
 
 
119
  # -----------------------------
120
- # 視覺化
121
  # -----------------------------
122
- def plot_trend(df):
 
 
 
 
 
 
 
123
  if df.empty:
124
  return None
125
  fig, ax = plt.subplots(figsize=(6, 4))
@@ -128,13 +143,10 @@ def plot_trend(df):
128
  ax.set_ylabel("Magnitude")
129
  ax.grid(True, linestyle="--", alpha=0.4)
130
  fig.autofmt_xdate()
131
- buf = io.BytesIO()
132
- fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
133
- plt.close(fig)
134
- buf.seek(0)
135
- return buf.getvalue()
136
 
137
- def plot_map(df):
 
138
  if df.empty:
139
  return None
140
  lon_min, lon_max, lat_min, lat_max = 119, 123, 21, 26
@@ -148,17 +160,13 @@ def plot_map(df):
148
  ax.scatter([], [], s=((m + 2) ** 3), alpha=0.6, edgecolor="black", label=f"M {m}")
149
  ax.legend(title="Magnitude")
150
  ax.grid(True, linestyle="--", alpha=0.3)
151
- buf = io.BytesIO()
152
- fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
153
- plt.close(fig)
154
- buf.seek(0)
155
- return buf.getvalue()
156
 
157
  # -----------------------------
158
- # 主流程
159
  # -----------------------------
160
  def _format_taipei(series):
161
- """把時間 Series 以台北時間顯示;naive 視為台北時間,aware 轉為台北時間。"""
162
  try:
163
  if series.dt.tz is None:
164
  s = series.dt.tz_localize(TAIPEI_TZ)
@@ -168,6 +176,21 @@ def _format_taipei(series):
168
  except Exception:
169
  return series.astype(str)
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  def df_to_markdown(df, top_n=100):
172
  if df.empty:
173
  return "(查無資料)"
@@ -177,26 +200,40 @@ def df_to_markdown(df, top_n=100):
177
  if "OriginTime" in slim.columns:
178
  slim["OriginTime"] = _format_taipei(slim["OriginTime"])
179
  header = f"共 {len(df)} 筆,顯示前 {min(len(slim), top_n)} 筆\n\n"
180
- return header + slim.to_markdown(index=False)
 
 
 
 
 
181
 
 
 
 
182
  def query_and_render(time_from, time_to, sort_order):
183
  try:
184
  raw = fetch_reports(time_from, time_to)
185
  df = parse_ea0015(raw)
186
  if df.empty:
187
  return "(查無資料)", None, None, None
 
188
  if sort_order == "OriginTime (舊→新)":
189
  df = df.sort_values("OriginTime", ascending=True, na_position="last").reset_index(drop=True)
 
190
  md = df_to_markdown(df)
191
- trend_png = plot_trend(df)
192
- map_png = plot_map(df)
 
193
  csv_bytes = df.to_csv(index=False).encode("utf-8-sig")
194
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="CWA_E-A0015-001_")
195
- tmp.write(csv_bytes); tmp.flush(); tmp.close()
196
- return md, trend_png, map_png, tmp.name
 
 
197
  except Exception as e:
198
  return f"錯誤:{e}", None, None, None
199
 
 
200
  # -----------------------------
201
  # 介面
202
  # -----------------------------
@@ -207,28 +244,40 @@ with gr.Blocks(fill_height=True) as demo:
207
 
208
  with gr.Column():
209
  time_from = gr.Textbox(label="timeFrom yyyy-MM-ddTHH:mm:ss", value=default_from)
210
- time_to = gr.Textbox(label="timeTo yyyy-MM-ddTHH:mm:ss", value=default_to)
211
 
212
  with gr.Row():
213
  btn_12h = gr.Button("最近 12 小時")
214
  btn_24h = gr.Button("最近 24 小時")
215
- btn_3d = gr.Button("最近 3 天")
216
- btn_5d = gr.Button("最近 5 天")
 
 
 
 
 
 
217
 
218
- sort_dd = gr.Dropdown(choices=["OriginTime (新→舊)", "OriginTime (舊→新)"], value="OriginTime (新→舊)", label="排序")
219
  run_btn = gr.Button("查詢", variant="primary")
220
 
221
  table_out = gr.Markdown("(尚未查詢)")
222
- trend_out = gr.Image(label="趨勢圖", type="numpy")
223
- map_out = gr.Image(label="台灣範圍圖", type="numpy")
224
- dl_btn = gr.DownloadButton(label="下載 CSV") # 相容舊/新版
 
225
 
 
226
  btn_12h.click(lambda: set_time_range(hours=12), outputs=[time_from, time_to])
227
  btn_24h.click(lambda: set_time_range(hours=24), outputs=[time_from, time_to])
228
  btn_3d.click(lambda: set_time_range(days=3), outputs=[time_from, time_to])
229
  btn_5d.click(lambda: set_time_range(days=5), outputs=[time_from, time_to])
230
 
231
- run_btn.click(query_and_render, inputs=[time_from, time_to, sort_dd], outputs=[table_out, trend_out, map_out, dl_btn])
 
 
 
 
 
232
 
233
  if __name__ == "__main__":
234
  demo.launch()
 
1
  import os
2
  import io
3
  import tempfile
4
+ from datetime import datetime, timedelta, timezone
5
+
6
  import requests
7
  import pandas as pd
8
  import matplotlib.pyplot as plt
9
  import gradio as gr
10
+
11
+ # -------- tabulate(可選)偵測,沒裝也不會壞 --------
12
+ try:
13
+ import tabulate as _tabulate # noqa: F401
14
+ HAS_TABULATE = True
15
+ except Exception:
16
+ HAS_TABULATE = False
17
 
18
  # -----------------------------
19
  # 台北時區 (UTC+8)
20
  # -----------------------------
21
  TAIPEI_TZ = timezone(timedelta(hours=8))
22
 
23
+
24
  def _fmt(dt: datetime) -> str:
25
  return dt.strftime("%Y-%m-%dT%H:%M:%S")
26
 
27
+
28
  def set_time_range(hours=None, days=None):
29
+ """依台北時間回傳 (timeFrom, timeTo) 字串"""
30
  now = datetime.now(TAIPEI_TZ)
31
  if hours is not None:
32
  t_from = now - timedelta(hours=hours)
 
36
  t_from = now - timedelta(days=3)
37
  return _fmt(t_from), _fmt(now)
38
 
39
+
40
  # -----------------------------
41
  # 取 API 資料
42
  # -----------------------------
43
  API_URL = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"
44
 
45
+
46
  def fetch_reports(time_from, time_to):
47
  api_key = os.getenv("CWA_API_KEY", "").strip()
48
  if not api_key:
 
52
  r.raise_for_status()
53
  return r.json()
54
 
55
+
56
  # -----------------------------
57
  # 解析 JSON → DataFrame(彈性容錯)
58
  # -----------------------------
 
65
  return default
66
  return cur
67
 
68
+
69
  def _to_float(x):
70
  try:
71
  if x is None or str(x).strip() == "":
 
74
  except Exception:
75
  return None
76
 
77
+
78
  def parse_ea0015(obj):
79
  """
80
+ 支援 records/Records、earthquake/Earthquake 等大小寫差異。
81
  輸出欄位:OriginTime, Lat, Lon, Depth_km, Magnitude, Location, ReportURL
82
  """
83
  records = obj.get("records") or obj.get("Records") or {}
84
  quakes = records.get("earthquake") or records.get("Earthquake") or records.get("data") or []
 
85
  if not isinstance(quakes, list):
86
  quakes = []
87
 
 
93
  or _safe_get(q, "earthquakeInfo", "originTime")
94
  or _safe_get(q, "EarthquakeInfo", "OriginTime")
95
  )
96
+ lat = _safe_get(q, "epicenter", "epicenterLat") or _safe_get(q, "Epicenter", "EpicenterLat")
97
+ lon = _safe_get(q, "epicenter", "epicenterLon") or _safe_get(q, "Epicenter", "EpicenterLon")
 
 
 
 
 
 
98
  depth = _safe_get(q, "depth") or _safe_get(q, "Depth")
99
  mag = (
100
  _safe_get(q, "magnitude", "magnitudeValue")
 
102
  or _safe_get(q, "magnitude", "magnitude")
103
  or _safe_get(q, "Magnitude", "Magnitude")
104
  )
105
+ loc = _safe_get(q, "epicenter", "location") or _safe_get(q, "Epicenter", "Location")
 
 
 
106
  url = _safe_get(q, "reportURL") or _safe_get(q, "ReportURL")
107
 
108
+ rows.append(
109
+ {
110
+ "OriginTime": origin,
111
+ "Lat": _to_float(lat),
112
+ "Lon": _to_float(lon),
113
+ "Depth_km": _to_float(depth),
114
+ "Magnitude": _to_float(mag),
115
+ "Location": loc,
116
+ "ReportURL": url,
117
+ }
118
+ )
119
 
120
  df = pd.DataFrame(rows)
121
  if not df.empty:
 
123
  df = df.sort_values("OriginTime", ascending=False, na_position="last").reset_index(drop=True)
124
  return df
125
 
126
+
127
  # -----------------------------
128
+ # 視覺化(回傳「檔案路徑」以相容舊/新 Gradio)
129
  # -----------------------------
130
+ def _save_fig_to_tmp(fig, suffix=".png"):
131
+ buf_path = tempfile.NamedTemporaryFile(delete=False, suffix=suffix).name
132
+ fig.savefig(buf_path, format="png", dpi=160, bbox_inches="tight")
133
+ plt.close(fig)
134
+ return buf_path
135
+
136
+
137
+ def plot_trend_path(df):
138
  if df.empty:
139
  return None
140
  fig, ax = plt.subplots(figsize=(6, 4))
 
143
  ax.set_ylabel("Magnitude")
144
  ax.grid(True, linestyle="--", alpha=0.4)
145
  fig.autofmt_xdate()
146
+ return _save_fig_to_tmp(fig)
 
 
 
 
147
 
148
+
149
+ def plot_map_path(df):
150
  if df.empty:
151
  return None
152
  lon_min, lon_max, lat_min, lat_max = 119, 123, 21, 26
 
160
  ax.scatter([], [], s=((m + 2) ** 3), alpha=0.6, edgecolor="black", label=f"M {m}")
161
  ax.legend(title="Magnitude")
162
  ax.grid(True, linestyle="--", alpha=0.3)
163
+ return _save_fig_to_tmp(fig)
164
+
 
 
 
165
 
166
  # -----------------------------
167
+ # 表格輸出(tabulate 可選)
168
  # -----------------------------
169
  def _format_taipei(series):
 
170
  try:
171
  if series.dt.tz is None:
172
  s = series.dt.tz_localize(TAIPEI_TZ)
 
176
  except Exception:
177
  return series.astype(str)
178
 
179
+
180
+ def _to_simple_md_table(df: pd.DataFrame) -> str:
181
+ cols = list(df.columns)
182
+ header = "|" + "|".join(cols) + "|\n"
183
+ sep = "|" + "|".join(["---"] * len(cols)) + "|\n"
184
+ rows = []
185
+ for _, r in df.iterrows():
186
+ cells = []
187
+ for c in cols:
188
+ v = r.get(c, "")
189
+ cells.append("" if pd.isna(v) else str(v))
190
+ rows.append("|" + "|".join(cells) + "|")
191
+ return header + sep + "\n".join(rows)
192
+
193
+
194
  def df_to_markdown(df, top_n=100):
195
  if df.empty:
196
  return "(查無資料)"
 
200
  if "OriginTime" in slim.columns:
201
  slim["OriginTime"] = _format_taipei(slim["OriginTime"])
202
  header = f"共 {len(df)} 筆,顯示前 {min(len(slim), top_n)} 筆\n\n"
203
+ if HAS_TABULATE:
204
+ table = slim.to_markdown(index=False)
205
+ else:
206
+ table = _to_simple_md_table(slim.reset_index(drop=True))
207
+ return header + table
208
+
209
 
210
+ # -----------------------------
211
+ # 主流程
212
+ # -----------------------------
213
  def query_and_render(time_from, time_to, sort_order):
214
  try:
215
  raw = fetch_reports(time_from, time_to)
216
  df = parse_ea0015(raw)
217
  if df.empty:
218
  return "(查無資料)", None, None, None
219
+
220
  if sort_order == "OriginTime (舊→新)":
221
  df = df.sort_values("OriginTime", ascending=True, na_position="last").reset_index(drop=True)
222
+
223
  md = df_to_markdown(df)
224
+ trend_path = plot_trend_path(df)
225
+ map_path = plot_map_path(df)
226
+
227
  csv_bytes = df.to_csv(index=False).encode("utf-8-sig")
228
+ csv_path = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="CWA_E-A0015-001_").name
229
+ with open(csv_path, "wb") as f:
230
+ f.write(csv_bytes)
231
+
232
+ return md, trend_path, map_path, csv_path
233
  except Exception as e:
234
  return f"錯誤:{e}", None, None, None
235
 
236
+
237
  # -----------------------------
238
  # 介面
239
  # -----------------------------
 
244
 
245
  with gr.Column():
246
  time_from = gr.Textbox(label="timeFrom yyyy-MM-ddTHH:mm:ss", value=default_from)
247
+ time_to = gr.Textbox(label="timeTo yyyy-MM-ddTHH:mm:ss", value=default_to)
248
 
249
  with gr.Row():
250
  btn_12h = gr.Button("最近 12 小時")
251
  btn_24h = gr.Button("最近 24 小時")
252
+ btn_3d = gr.Button("最近 3 天")
253
+ btn_5d = gr.Button("最近 5 天")
254
+
255
+ sort_dd = gr.Dropdown(
256
+ choices=["OriginTime (新→舊)", "OriginTime (舊→新)"],
257
+ value="OriginTime (新→舊)",
258
+ label="排序",
259
+ )
260
 
 
261
  run_btn = gr.Button("查詢", variant="primary")
262
 
263
  table_out = gr.Markdown("(尚未查詢)")
264
+ # 回傳檔案路徑 Image 用 filepath 模式最穩
265
+ trend_out = gr.Image(label="趨勢圖", type="filepath")
266
+ map_out = gr.Image(label="台灣範圍圖", type="filepath")
267
+ dl_btn = gr.DownloadButton(label="下載 CSV") # ���使用 file_name 參數,傳路徑即可
268
 
269
+ # 快速鍵
270
  btn_12h.click(lambda: set_time_range(hours=12), outputs=[time_from, time_to])
271
  btn_24h.click(lambda: set_time_range(hours=24), outputs=[time_from, time_to])
272
  btn_3d.click(lambda: set_time_range(days=3), outputs=[time_from, time_to])
273
  btn_5d.click(lambda: set_time_range(days=5), outputs=[time_from, time_to])
274
 
275
+ # 查詢
276
+ run_btn.click(
277
+ query_and_render,
278
+ inputs=[time_from, time_to, sort_dd],
279
+ outputs=[table_out, trend_out, map_out, dl_btn],
280
+ )
281
 
282
  if __name__ == "__main__":
283
  demo.launch()