前言:手動維護 53 種食物的極限
我正在開發一款健康追蹤 App,核心功能之一是飲食記錄。一開始,食物資料庫是手動維護的 JSON — 53 種台灣常見小吃,從滷肉飯到珍珠奶茶。
53 種夠用嗎?使用者搜尋「鮭魚」找不到、搜尋「優格」找不到、搜尋「oatmeal」更不用說。手動新增不可能跟上需求,我需要一條自動化管線,把全世界的食物營養資料拉進來。
這就是 ETL 管線登場的時機。
什麼是 ETL?用食物資料庫解釋
ETL 是 Extract(擷取)、Transform(轉換)、Load(載入) 的縮寫,是資料工程最基礎的設計模式。與其抽象解釋,直接用這個專案的食物資料庫來看:
四個來源的原始資料格式完全不同 — 台灣用中文欄位名(粗蛋白)、日本用 FAO 代碼(prot)、USDA 用數字編號(203)。Transform 階段把它們統一成 App 需要的 {id, name, emoji, category, nutrition} 結構。
為什麼不直接在 App 端串接 API?因為食物營養資料是靜態的 — 雞蛋的蛋白質含量不會每天變。離線 JSON 零延遲、不吃流量、無網路也能用。ETL 只在開發階段跑一次,產出的 JSON 跟著 App 一起發布。
USDA API 的格式陷阱:同一個服務,兩套規則
USDA FoodData Central 是美國農業部的食物營養資料庫,涵蓋近 8,000 種食物。我選擇 foods/list endpoint 批量下載 SR Legacy 和 Foundation Foods 資料。
然而,這個 API 有一個文件沒有明確說明的陷阱:foods/list 和 foods/search 兩個 endpoint 回傳的營養素格式完全不同。
同一個營養素(Energy/熱量),在 foods/list 的 key 是 number: "208",在 foods/search 是 nutrientId: 1008。不是大小寫差異、不是命名慣例不同 — 是完全不同的編號系統。
不處理這件事的後果?解析出來的營養素全部是零。程式不會報錯,只會靜悄悄地產出一堆「0 卡路里、0 蛋白質」的食物。
解法是維護兩套映射表(208 → calories 和 1008 → calories),在解析時先嘗試 number key,沒有的話再嘗試 nutrientId key,自動適應不同 endpoint 的回傳格式。
教訓:串接第三方 API 時,不要假設同一個服務的不同 endpoint 會用相同的資料結構。永遠先拿真實回傳值比對文件。
缺少分類欄位?用 NDB Number 反推
第二個坑:foods/list API 不回傳 foodCategory 欄位。
App 內部需要食物分類(蛋類、肉類、穀物…)來決定 emoji 和預設份量。foods/search 有 foodCategory,但 foods/list 沒有。換 API?不行 — foods/search 不支援批量下載,每次只能查一個關鍵字。
翻遍 API 文件後,我注意到每筆食物都有一個 ndbNumber(National Database Number)。這個編號的前兩碼對應 USDA 的食品群組分類:
但這裡有一個邊界案例:USDA 把蛋類和乳品合併在群組 01(“Dairy and Egg Products”)。光靠前兩碼會把雞蛋歸為 dairy。解法是對群組 01 加一層名稱關鍵字判斷 — 名稱包含 “egg” 就歸蛋類,否則歸乳品。
整個分類邏輯分三層優先順序:先看 NDB 群組代碼(最可靠),再看 foodCategory(若有),最後用名稱關鍵字兜底。三層 fallback 確保 8,000 種食物都能正確分類。
這種「從現有欄位推導缺失資訊」的手法在資料工程很常見。當 API 不給你需要的欄位時,不要急著換方案 — 先看看現有資料裡有沒有可利用的線索。
五來源合併去重:優先順序決定一切
最後一塊拼圖是合併。五個來源的資料格式已經在 Transform 階段統一了,但同一種食物可能在多個來源都出現(雞蛋在台灣食藥署、日本、USDA 都有)。
去重策略很簡單 — 先到先贏,按優先順序載入:
為什麼台灣小吃最優先?因為它是手動校正過的 — 「滷肉飯」的營養數據比 USDA 的 “Braised pork rice” 更精準,份量也更符合台灣的一碗標準。
去重用正規化名稱比對 — 移除括號內容(中英文括號),確保「雞蛋」和「雞蛋(生)」不會被視為不同食物。合併完成後,管線自動驗證三件事:無重複 ID、無重複名稱、所有食物都有完整 8 個營養素欄位。
| 優先順序 | 來源 | 處理後筆數 | 去重後新增 |
|---|---|---|---|
| 1 | 台灣小吃(手動) | 53 | 52 |
| 2 | 台灣食藥署 | 2,176 | 1,813 |
| 3 | 日本食品標準成分表 | 2,466 | 2,450 |
| 4 | 中國食物成分表 | 1,623 | 1,076 |
| 5 | USDA FoodData Central | 7,853 | 7,761 |
| 總計 | 13,152 |
結語:ETL 管線的長期價值
從 53 種到 13,152 種,這條管線讓食物資料庫擴充了 248 倍。但更重要的是可重複性 — 當台灣食藥署更新資料、或我想新增韓國來源時,只需要加一個 process_korea_food.py,跑一次 python run_pipeline.py --all,就能無痛擴充。
三個帶走的教訓:
- 同一個 API 的不同 endpoint 格式可能不同 — 永遠用真實回傳值驗證
- 缺少欄位不代表無解 — 從現有資料反推,NDB Number 就是藏在資料裡的分類密碼
- 合併優先順序比演算法重要 — 在地資料優先,品質勝過數量
