系列:30 天用 Google AI 打造臺灣防災速報 App(Day 5/30)
Day 2 的資料下載程式只是「能動」,今天把它變成「能用」:去除重複資料、追蹤示警狀態、統一分類名稱。原本以為是例行工程,結果真實資料推翻了我原本的設計,這篇記錄整個過程。
1. 重複與更新:同一事件會發多次示警(颱風警報每 3 小時更新一次),不能當成新事件重複推播。
2. 生命週期:Day 2 觀察到火災示警會走到「已結案」。每筆示警要標上狀態,前端只顯示生效中的,結案的轉入歷史區。
3. 統一分類名稱:示警資料自帶的分類有粗有細,例如「淹水」是一類、「淹水感測」又是另一類,各單位的分法不一致;要建一張對照表,把這些分類整理成自訂的六大類:氣象、地震海嘯、水利、火災事故、交通、民生。
去除重複資料的第一版設計很直覺:CAP(Common Alerting Protocol,各國政府發布示警共用的標準格式)規定每則示警都要有識別碼,那就拿示警資料的 id 欄位當作辨認每一筆資料的依據,同一個 id 只留最新一筆就好。執行的結果:
1,074 筆示警中,完整 id 重複的有 161 組(2026-09-16)
(Day 4 同一天清晨取得的是 1,076 筆;這次下載的時間較晚,示警有增有減,所以總數略有不同。)
抽一組出來看:台灣自來水公司的 TWC_water_202609150850 底下有 6 筆內容完全不同的停水通知(龜山、豐原、田寮……各在不同縣市),id 只精確到分鐘,同一分鐘發出的通知全部共用一個 id。更危險的例子是 8 月 23 日大雨那天看到的:水保署同一個 ardswc.gov.tw_debrisFlowLandSlide_202608230630 底下有 5 筆警戒層級不同的示警:
「計 2 條土石流潛勢溪流達紅色警戒…」
「計 32 條土石流潛勢溪流、2 處大規模崩塌潛勢區達紅色警戒…」
「計 28 條土石流潛勢溪流、1 處大規模崩塌潛勢區達黃色警戒…」
水保署把不同層級、不同範圍的警戒發在同一個 id 底下。如果照原設計「同 id 取最新」,紅色警戒會被黃色警戒蓋掉,使用者就看不到比較嚴重的那一則。
那改成「去掉尾端流水號、取事件碼」呢?把各發布單位的 id 列出來看:
| 單位 | id 樣式 |
|---|---|
| 水利署 | WRA_floodSensor_20260823062700_0000(尾綴流水號) |
| 中央氣象署 | CWA-Weather_heat_202608191138001(尾綴數字,不帶底線) |
| 台灣自來水公司 | TWC_water_202609150850(id 只到分鐘,同一分鐘的多則通知共用) |
| 消防署 | A2026082207251839044644460(純數字流水) |
| 水保署 | 見上,同 id 多內容 |
結論:不存在通用的「事件碼切法」。CAP 標準統一了電文格式,但 id 的命名紀律是各單位自己的事。想真正把「同一事件的更新鏈」串起來,得下載每筆示警的完整內容,使用裡面的 references 欄位(用來指向同一事件的前一版示警),這部分之後接上 Cloud Functions(Google 的雲端自動執行服務)時再處理。
資料下載這一層只處理確定不會出錯的部分:
from hashlib import sha1
def normalize(raw: dict) -> Alert:
summary = raw.get("summary") or ""
# id 不可信,改用 id+時間+內容雜湊當唯一鍵
digest = sha1(f"{raw['id']}|{raw['updated']}|{summary}".encode()).hexdigest()[:8]
return Alert(
uid=f"{raw['id']}#{digest}",
group=CATEGORY_MAP.get(raw["category"], "其他"),
status="closed" if "已結案" in summary else "active",
...
)
雜湊(hash)是把一段文字轉成固定長度代碼的方法,內容只要有一點不同,代碼就會不同,拿來辨認兩筆資料是否完全相同很方便。
updated 放進雜湊是刻意的:同一事件的每次更新都會得到新的 uid、被當成新的一筆,這在資料下載層是正確的保守做法,代價是前端可能看到同一事件的多個版本並列。references 欄位做,不自己猜。這一步目前尚未實作,之後接 Cloud Functions 時處理;在那之前,「哪幾筆是同一事件」這個問題系統不回答。uid 好讀,兩筆不同資料得到相同代碼的機率極低但不是零;正式版存進 Firestore(Google 的雲端資料庫)時會改用完整雜湊當文件 ID,8 碼只留給人看。ncdrSystemTest 系統測試訊息,在這一層直接過濾。原始 1,074 筆 → 去除完全重複後 1,059 筆 → 生效中 1,043 筆
六大類分布:民生 583、水利 260、火災事故 125、氣象 63、交通 9、地震海嘯 3
兩個觀察:完全相同的示警只有 15 筆,來源資料本身相當乾淨;以生效中的筆數計算,平常日的主角是民生類(停水)佔 56%;而 8 月 23 日大雨那天執行同一支程式,水利類(水庫放流、淹水感測、土石流)佔了 45%(生效中 1,432 筆、水利 650 筆)。兩種天氣下警報的組成完全不同,這對之後設計推播分級,以及警報大量湧入時怎麼降低推播頻率,都是第一手依據。
更正(2026-09-20):上面的「生效中」只排除了摘要裡有「已結案」的示警,沒有檢查到期時間。後來發現 NCDR 的原始資料裡有
expires(到期時間)欄位,補上這項檢查之後,9/20 下載的 922 則示警裡,真正還在生效的只有 30 則,其餘大多已經過期。所以這一節的生效中筆數,以及用它算出來的各類比例,都高估了。當時存下來的資料沒有保留到期時間,無法重新計算,在這裡照實更正;修正的過程之後會說明。
references。明天 Day 6 接上第二個資料源:中央氣象署開放資料平台的地震報告與天氣特報。