[CODE]

四個 job 抓同一份資料:把 per-site 趨勢爬取收成一份市場快取

恆春九家民宿共用同一個觀光市場。Google Trends 管線卻跑四個一模一樣的爬取,對一個會限流的 API 打四次,產出四份完全相同的資料。修法是認清這份資料從來就不是 per-site 的。

1 min read AI 生成
typescript github-actions python data-pipeline automation

每個禮拜天晚上,四個 GitHub Actions job 對 Google Trends 各打一次,一個客戶站台一個。抓的關鍵字都一樣——墾丁包棟、恆春民宿、墾丁住宿——同一個台灣地區、同一個 18 個月視窗。四個 job、四份 trends-state.json、四次被限流的機會。而這四份資料,照定義來說,完全相同。

這篇講的是我注意到這件事的那一刻,以及清理它實際上要動哪些地方。

這管線屬於哪個系統

每家民宿都有一份自動週報。週報裡其中一塊是墾丁/恆春觀光市場的搜尋需求年增率——市場本身是漲還是跌,跟單一站台的流量無關?這個脈絡能避免週報把整個市場的下滑誤判成某一站的問題。

來源是 Google Trends。但 Trends 限流很兇,週報跑的時候吃到 429 會卡死整份報告。所以 Trends 爬取被拆成獨立的 workflow,禮拜天台灣時間 01:00 跑——比禮拜一報告 cron 早約八小時——commit 一份快取 JSON,報告只讀快取。如果 Trends 限流,快照 job exit 0,舊快取保留,每份報告照跑。這個解耦本來就是對的。

不對的是那個 matrix。

這份資料從來就不是 per-site 的

快照 workflow 跑了一個四站的 matrix。每個站台在 site-config.yaml 裡有自己的 trends.keyword_groups,每次爬取寫進 sites/<site>/trends-state.json,每個站台的報告讀自己那份。

問題在於:四家全在恆春。它們競爭的就是同一個市場。它們的關鍵字組互相是複本——墾丁包棟、恆春民宿、墾丁住宿——因為「找恆春民宿的人」這件事,能用的搜尋字就那一組。Per-site 的結構暗示資料會因站台而異。它不會。我為了產出四份位元組完全相同的檔案,付了四倍的限流風險。

對的模型是市場層級:墾丁/恆春市場爬一次,寫進 data/trends-kenting-hengchun.json,這個市場裡的每個站台讀同一份快取。

這次收斂動了哪些地方

這次重構行數不多、但牽連面很廣——在修正「資料歸屬錯誤」而不是加功能時,通常就是這個樣子。

  • Python 爬取腳本多了一個 --output 參數,可以寫到共用的市場路徑,而不是寫死的 sites/<site>/trends-state.json。預設還是寫 per-site 路徑,所以工具保持通用——以後多一個市場就多加一個 step,帶自己的 fetch 站台和輸出路徑。
  • 有一個站台(cometrue-bnb)保留完整的 keyword_groups 定義,當觸發爬取的那個站。另外三站的設定整段刪掉重複的關鍵字區塊,只留一個 data_path 指向共用檔。render 層在 data_path 存在時讀它。
  • workflow 拿掉 matrix——四個平行 job 變一個。原本抓同一件事抓四次的 four-way fan-out 沒了。

這個不對稱——一個站擁有關鍵字定義、其他只引用快取——是刻意的。它把關鍵字清單放在唯一一個地方。市場相關的搜尋字變了,只有一份設定要改,不是四份要同步。同步這件事本來就不可能可靠做到;那份重複本身就是一個遲早會發作的 drift bug。

為什麼行數之外這件事重要

單站專案,四對一的平行 job 是雜訊。但九家民宿、五個 codebase——每多一個客戶原則上就可能多一個 Trends 爬取——per-site matrix 是一個朝錯方向擴張的結構。每加一個站台,就多一次對限流 API 的多餘打點,去產出一份對整個市場的人都一樣的資料複本。

市場快取模型朝對的方向擴張:恆春多一家民宿,讀現有的檔,爬取負載加零。真正新的市場——比方說台中的品牌——加剛好一個爬取 step。成本現在跟著「不同市場的數量」走,這本來就是它該跟的東西,而不是跟著站台數量走。

這些都不光鮮。這是那種你得停下來問「這份資料到底是什麼」、而不是問「目錄結構說它是什麼」才會浮現的修正。目錄說 per-site。資料說 per-market。資料是對的。