技術實作
這頁寫給想知道「找回毛孩」怎麼做的人:資料從哪來、地址怎麼變成座標、隱私怎麼守、照片怎麼上地圖、刊登怎麼防濫用。數字都是 2026 年 8 月在正式站量到的。
整條流程(每 5 分鐘檢查一次,有新案件才跑)
- 探針:每 5 分鐘只抓官方清單最前面 200 筆(約 100KB、0.15 秒)。這個 API 是依「通報時間」新到舊排序的,新登記的案件一定在最前面,所以不用掃七萬多筆就知道有沒有新通報——沒有就到此為止
- 有新通報才往下:從開放資料 API 分頁抓完整份(7 萬多筆,21 秒)
- 去重、丟掉髒日期,只留近兩年
- 每筆地址切成「縣市/鄉鎮/路/巷/號」,對縣市門牌開放資料做離線地理編碼
- 去識別化:門牌、電話、LINE、姓名一律刪;座標加隨機偏移
- 探測官方照片是否還在,新照片抓回來做縮圖與中圖
- 打包成一份 GeoJSON 推到正式站,瀏覽器一次載完(gzip 後約 550 KB)
地址 → 座標:三層退而求其次
官方資料只有地址文字,沒有座標。線上地理編碼服務不是要錢就是有條款限制(Google 免費額度要綁信用卡、而且結果不能快取;OpenStreetMap 的 Nominatim 對台灣門牌覆蓋很差,抽樣測過大多只能到路名),所以改用各縣市政府公開的門牌座標檔自己建一張表:
- 16 個縣市的門牌 CSV,合計 478 萬筆門牌,建成一個 730 MB 的 SQLite(原始檔 850 MB)
- 座標是 TWD97(EPSG:3826,澎湖金門馬祖是 3825),用 pyproj 轉 WGS84;有些檔是 BIG5、有些欄位名不一樣、有些下載點只接受舊版 TLS,逐個縣市處理
- 查詢順序:門牌精準命中 → 找不到就用同一條巷所有門牌的平均座標(誤差約 100 公尺內)→ 再退到整條路的平均(可能差幾百公尺)
- 連路名都沒有(「住家附近」「公園旁」)的,就在該鄉鎮的邊界多邊形裡隨機撒點;鄉鎮有離島時只用面積最大的那塊,不然宜蘭頭城的狗會撒到釣魚台
- 地點只寫得出縣市(例如整欄就三個字「花蓮縣」)的,改在整個縣市範圍內撒點(依鄉鎮面積加權挑一塊再撒)。這種近兩年有 209 筆,本來整批被當成「地址不明」完全看不到
目前上地圖的 8,044 筆案件裡:定位到門牌或巷的 3,515 筆(44%)、只到路的 1,042 筆(13%)、鄉鎮撒點 3,279 筆(41%)、只到縣市 209 筆(3%)。另外 2,108 筆連縣市都解析不出來,只進地址不明清單。地圖上大圓=精準、虛線圈=撒點,一眼分得出來。
隱私:技術上做到「看得出區域、找不到住家」
- 座標偏移:精準案件公開的是「100 公尺內隨機偏移的點+300 公尺半徑的圓」。偏移用案件編號當種子,所以同一筆每天都在同一個位置,不會因為重建資料而抖動,也不能靠多次觀察平均回真實點
- 文字清洗:飼主常把門牌、手機、LINE ID、全名寫進「特徵」欄。建檔時用規則把門牌砍到路名(「長安街22巷27號」→「長安街」)、電話與 LINE 整段刪除、姓名只留姓氏;自行刊登的文字走同一套
- 真實座標與 Email 只留在後台,API 不吐;後台管理頁只接受區網來源 IP
- 目擊通知信:有人回報目擊時,寄一封信到飼主在官方啟事登記的 Email(自行刊登則是 Google 帳號的 Email)。Email 只在伺服器端用來寄這封信,不顯示給任何人;每案每天最多 3 封、每回報 IP 每天最多 5 封;信裡有一鍵退訂,退訂名單只存 Email 的雜湊
- 自行刊登者的聯絡方式(LINE/電話)由本人選填,只給登入者按下按鈕後看,每帳號每天 30 次,擋爬蟲整批撈
照片
官方圖片伺服器對外很慢又會限流,直接在地圖上引用會讓 2,800 張縮圖等到天荒地老,所以自己存:128px 縮圖(地圖用,2,816 張只有 12 MB)和 720px 中圖(資料卡用,約 200 MB)。官方「沒有照片」時回的是一個 12 bytes 的空檔,靠這個判斷。每天重新探測一次,官方撤下的照片本站一天內跟著刪;不存原圖。
地圖上的照片卡是自己排的:MapLibre 沒有「有空間才顯示、擠了就挑最新」這種功能,所以每次移動畫面時,把畫面內的案件依時間新到舊排序,逐張算矩形是否跟已放的卡重疊(AABB 碰撞),放得下才放,最多 120 張;放不下的退化成小圓點。這樣不管縮放到哪一層,永遠看得到照片,而且擠的地方是最新的案件優先。
前端
- 地圖用 MapLibre GL JS,底圖用 OpenFreeMap 的向量圖磚(Positron 淺色樣式),兩者都不用 API key、不綁信用卡、沒有用量帳單
- 案件資料一次載完(7,842 筆,gzip 後 553 KB),之後篩選縣市、物種、天數、新案全在瀏覽器裡做,零請求;資料量在這個規模不需要切圖磚
- 一週內的新案畫橘框+NEW 角標,超過 90 天淡化,撿到刊登綠框;貓狗其他用三種顏色的文字標籤而不是圖示(圖示在縮圖上根本分不出來)
- 海報產生器完全在瀏覽器的 canvas 上畫(A4 1240×1754),QR code 也是前端算,照片不會再上傳一次
自行刊登:不寄信、不建密碼
- 登入用 Google Identity Services:瀏覽器拿到 ID token,後端向 Google 驗一次簽章與 aud,只記 Google 的使用者 ID。沒有註冊表單、沒有驗證信、沒有密碼可以外洩
- 資料存 SQLite(跟站上其他工具一樣,不另外養資料庫)
- 每張上傳照片(刊登與目擊)先在伺服器本機過 NudeNet 裸露偵測,再去掉 EXIF(手機照片內含 GPS),只存 720px/128px 兩種尺寸
- 防洗版:每帳號每天 3 篇、每 IP 每天 5 篇、蜜罐欄位;同一張照片用感知雜湊(pHash)比對,最多出現在 2 則刊登
- 檢舉不用登入,3 個不同 IP 檢舉同一則就自動下架等人工複核;目擊情報每 IP 每天 10 則
- 90 天自動到期,可延長;「取消刊登」只問一句「找到了嗎」,找到了才進成功頁(問卷+自願贊助),沒找到就安靜下架
部署與成本
Flask 子應用掛在跟其他工具同一個容器裡,SQLite 讀寫,沒有顯示卡、沒有佇列。資料更新有兩支 systemd timer:每 5 分鐘一次的探針(只抓官方清單最前面 200 筆、約 100KB,比對有沒有新通報,有才跑整批),以及每天 04:40 的完整版:更新門牌表 → 抓官方資料 → 建檔 → 探測照片 → tar 過 SSH 推到正式站;失敗就留舊資料不上線。整個工具硬碟佔用約 1 GB(門牌表佔七成),每月流量在 Cloudflare 免費額度內。不放廣告在地圖與案件頁,因為上面有未複核的使用者內容。
難在哪
不是資料不夠,官方資料很齊。卡的是三件小事疊在一起:地址沒座標(要自己建門牌表)、照片伺服器慢(要自己存)、還有個資(原始資料裡到處是門牌電話,直接畫地圖會出事)。每一件都不難,但都要做完才能上線。