iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Build on Google AI

30 天用 Google AI 打造台灣防災速報 App系列 第 5

Day 05|1,074 筆示警、161 組重複 id:被真實資料推翻的去除重複設計

  • 分享至 

  • xImage
  •  

系列:30 天用 Google AI 打造臺灣防災速報 App(Day 5/30)

Day 2 的資料下載程式只是「能動」,今天把它變成「能用」:去除重複資料、追蹤示警狀態、統一分類名稱。原本以為是例行工程,結果真實資料推翻了我原本的設計,這篇記錄整個過程。

原始計畫:三個要解決的問題

1. 重複與更新:同一事件會發多次示警(颱風警報每 3 小時更新一次),不能當成新事件重複推播。

2. 生命週期:Day 2 觀察到火災示警會走到「已結案」。每筆示警要標上狀態,前端只顯示生效中的,結案的轉入歷史區。

3. 統一分類名稱:示警資料自帶的分類有粗有細,例如「淹水」是一類、「淹水感測」又是另一類,各單位的分法不一致;要建一張對照表,把這些分類整理成自訂的六大類:氣象、地震海嘯、水利、火災事故、交通、民生

第一個問題:id 根本不唯一

去除重複資料的第一版設計很直覺: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 列出來看:

單位 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)是把一段文字轉成固定長度代碼的方法,內容只要有一點不同,代碼就會不同,拿來辨認兩筆資料是否完全相同很方便。

  • 去除重複時只剔除完全相同的示警(id、時間、內容全同才算),寧可多顯示、不可漏警報。把 updated 放進雜湊是刻意的:同一事件的每次更新都會得到新的 uid、被當成新的一筆,這在資料下載層是正確的保守做法,代價是前端可能看到同一事件的多個版本並列。
  • 更新鏈合併延後到 CAP 電文階段,用官方的 references 欄位做,不自己猜。這一步目前尚未實作,之後接 Cloud Functions 時處理;在那之前,「哪幾筆是同一事件」這個問題系統不回答。
  • 雜湊只取前 8 碼是為了 uid 好讀,兩筆不同資料得到相同代碼的機率極低但不是零;正式版存進 Firestore(Google 的雲端資料庫)時會改用完整雜湊當文件 ID,8 碼只留給人看。
  • 分類對照表按實測補到 21 種分類全覆蓋,沒對照到的進「其他」並輸出警告,之後逐步補;另外正式示警資料會混入 ncdrSystemTest 系統測試訊息,在這一層直接過濾。

實測結果(2026-09-16)

原始 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 則,其餘大多已經過期。所以這一節的生效中筆數,以及用它算出來的各類比例,都高估了。當時存下來的資料沒有保留到期時間,無法重新計算,在這裡照實更正;修正的過程之後會說明。

今日小結

  • 兩個實測教訓:id 不唯一(161 組重複、同 id 可載不同內容甚至不同警戒層級)、id 格式無通則,「同 id 取最新」的直覺設計會蓋掉紅色警戒。
  • 修正設計:內容雜湊當唯一鍵、只剔除完全相同的示警、更新鏈留給 CAP references

明天 Day 6 接上第二個資料源:中央氣象署開放資料平台的地震報告與天氣特報。


上一篇
Day 04|第一次呼叫 Gemini:它讀懂了示警,還自己多加一句提醒
下一篇
Day 06|接上中央氣象署開放資料:授權碼沒錯,卻被憑證檢查擋在門外
系列文
30 天用 Google AI 打造台灣防災速報 App9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言