- radar_fetcher.py:呼叫 Function Server 抓 twpkinfo.com 資料後寫入 Redis (TTL 300s) - pokemon_radar_api.py:FastAPI /scan,從 Redis 讀取並依 request 即時二次篩選;Redis 不可用時回 200 空結果,不再 500 - pokemon_location_fetcher.py:舊版 CLI,仍由 fetch_and_upload.sh 使用 - 文件:README (參考手冊)、QUICKSTART (操作指南)、SUMMARY (故事線)、CLAUDE.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.7 KiB
SUMMARY
這份文件講為什麼長這樣:設計脈絡、演進、取捨、目前還沒處理乾淨的地方。要查指令看 README.md,要跑起來看 QUICKSTART.md。
一句話
把一個只能「給座標、即時回寶可夢清單」的網站(twpkinfo.com),變成「前端 / n8n 能隨時打 API 拿最新資料」的服務。
為什麼需要這個專案
twpkinfo.com 的雷達頁(/ipoke.aspx)有完整的物種 / IV / CP / 技能 / 座標資訊,但:
- 沒有 API,資料只在網頁 DOM 裡
- 有人機驗證與前端黑影遮罩,直接
curl拿不到 - 前端(地圖標點)和 n8n(通知高 IV 寶可夢)都需要結構化資料
所以這個專案做兩件事:把網頁變成 JSON、把 JSON 包成 API。
架構演進
這條 pipeline 不是一次設計出來的,是三個階段堆疊出來的,中間每個階段的產物都還在 repo 裡。
第一階段:CLI 一把梭
pokemon_location_fetcher.py + twpk_radar.mjs + fetch_and_upload.sh。
twpk_radar.mjs送到 Function Server(一台browserless/ 類似的 browser automation 服務)執行,繞 stealth、等黑影消退、解析資料回傳 JSONpokemon_location_fetcher.py是 Python CLI,讀 MJS、用 regex 注入座標和 state.json、POST 到 Function Server、對回來的資料做 IV%/remaining/species 篩選,最後輸出pokemon_data.json+pokemon_data.csv+screenshot.pngfetch_and_upload.sh用flock避免重入,跑完後scp把 JSON 丟到另一台 caddy 主機,由前端靜態讀取
為什麼 regex 注入:Function Server 的契約是「你丟一段 JS 過來,我幫你跑在無頭瀏覽器」。參數化只能在字串層做 — 所以 twpk_radar.mjs 裡的 const TARGET = {...};、const DO_SCREENSHOT = ...;、globalThis.__POKE_STATE__ = ...; 三個錨點變成 Python / JS 之間的介面。改 MJS 時若動到這些行的形狀,Python regex 會失效但不會報錯,只會靜默用錯誤參數跑。
這階段到今天仍在生產跑,因為 caddy 前端是靜態檔,沒理由為了它多引入 Redis。
第二階段:FastAPI v1(已不存在)
pokemon_radar_api.py_BACKUP_20251208225502(已清除)是第一版 API:每個 /scan request 都呼叫一次 Function Server。
問題:
- Function Server 單次 browser automation 要 30-60 秒,前端打一次就等一次
- 多人同時打會超過 Function Server 的 concurrency
- 資料源本身是頁面抓取,3 秒打一次跟 3 分鐘打一次拿到的結果幾乎一樣(寶可夢生成間隔長)
第三階段:Redis backed API(現行)
把 fetch 和 serve 拆開:
radar_fetcher.py當 writer,cron 定期跑,結果SETEX到 Redis,TTL 300spokemon_radar_api.py當 reader,每個 request 從 Redis 讀快取資料,依 request 參數(min_iv/perfect_only/species)做二次篩選後回傳- API 的
ScanRequest保留了lat / lng / zoom / shot欄位但忽略它們,純粹相容舊前端不必改 client
Reader 端沒有 breaking change、Writer 端可以獨立調速,Function Server 壓力從「每 request 一次」降到「每 TTL 一次」。
目前還沒處理乾淨的地方
兩條 pipeline 共存
radar_fetcher.py(新)和 pokemon_location_fetcher.py(舊)都能抓資料,篩選邏輯各寫了一份(_postprocess vs PokemonRadarClient.postprocess)。修改篩選規則要兩邊同步改。
合併不是單純 refactor:舊 CLI 還在 fetch_and_upload.sh 裡給前端生靜態檔,短期不會下掉。
API 重用舊 client 的 postprocess
pokemon_radar_api.py 為了不重寫一次篩選,import PokemonRadarClient 然後 _filter_helper = PokemonRadarClient(url="", token=""),只用它的 postprocess。這個 workaround 讓新 API 與舊 CLI 有了非必要的耦合。長期方向應該把 _postprocess 抽成獨立模組,兩邊共用。
.env 的環境耦合
radar_fetcher.py 預設 REDIS_HOST=localhost,pokemon_radar_api.py 和 reader.py 預設 192.168.42.211 — 因為 fetcher 歷史上跑在 Redis 本機,API 跑在遠端。實務上一定要透過 .env 顯式設定,不要依賴程式內預設。
state.json 的脆弱性
整條 pipeline 最常壞的點是 state 過期。過期時 Function Server 會正常 200,但回來的資料是空陣列或登入頁 DOM — pipeline 不會炸,只會靜默吐 0 筆。radar_fetcher._postprocess 有個診斷分支:當 raw_items 非空但篩完為 0 時會 log remaining_min / remaining_max / perfect 分佈,優先看這個判斷是過濾太嚴還是資料本身有問題。
最近處理過的問題
/scan 在 Redis 不可用時會 500
get_data_from_redis() 原本只 try/except json.JSONDecodeError,沒處理 redis.exceptions.RedisError。結果:
- Redis key 不存在 → 照原本設計回空陣列(OK)
- Redis 連不上 → 例外沒接,FastAPI 直接 500
修法:get_data_from_redis() 改回傳 (data, ttl, err) 三元組,整段 Redis 操作包 try/except RedisError;scan() 依 err 切換 source 為 redis (unreachable) 並把錯誤訊息放進 debug.note。三種情況(連線成功、key 不存在、連不上)都回 200,前端不會炸。
備份檔清理
repo 曾保留多份 *_BACKUP_<timestamp> 檔案當作版本保險,但有 git 就沒必要。已清除。
執行環境現況
- 本機開發:Docker 起
redis:7-alpine在127.0.0.1:6379,.env指本機、無密碼。Function Server 走 VPN 或同網段連192.168.42.124。 - 生產:Redis 在
192.168.42.211(有密碼)、API 在某台 8008 port、fetch_and_upload.sh由 cron 觸發並 scp 到192.168.42.104的 caddy。兩條 pipeline 並存。