iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
佛心分享-SideProject30

營養師想做一個飲食建議產品系列 第 22 篇

Day22 - 資料怎麼蒐集?爬蟲、Firecrawl、官方 API 該怎麼選

  • 分享至 

  • xImage
  •  

真實食物資料是怎麼一筆一筆蒐集回來的?

Day21 把 FoodItem 的欄位定好了,接下來是真正花時間的部分:把各品牌官網上「給人看」的營養標示,變成程式能用的結構化資料。這篇整理實際用到的蒐集方式(MOS、Subway、全家各用了不同的做法),以及過程中踩到的雷。

🧱 地基概念

資料蒐集在做什麼

網頁上的營養標示是排好版給人看的,程式要用的話,得先把它「抽」出來,整理成固定格式(也就是 Day21 的 FoodItem)。這件事就叫資料蒐集,也常被叫做爬蟲。

傳統做法:指定「第幾個表格的第幾格」

傳統的做法就是「自己寫爬蟲」。先理解一件事:網頁其實是一份 HTML 文字檔,瀏覽器把它畫成漂亮的畫面給人看。爬蟲程式做的事情是:

  1. 先把這份 HTML 下載下來
  2. 用「規則」告訴程式資料在哪裡。常見有兩種:CSS selector(用網頁的標籤和名稱定位,例如「找出那張營養表格的第 1 列第 2 格」),或正則表達式(直接在文字裡找固定樣式,例如「找到『熱量』後面的數字」)
  3. 把抓到的字串轉成數字,存起來

用一個簡化的例子(示意):網頁背後的 HTML 長這樣:

<table class="nutrition">
  <tr><td>熱量</td><td>433.9</td></tr>
  <tr><td>蛋白質</td><td>17.8</td></tr>
</table>

程式就要踏穩地寫「去找 class 叫 nutrition 的表格、第 1 列的第 2 格」:

document.querySelector('.nutrition tr:nth-child(1) td:nth-child(2)').textContent  // "433.9"

優點:免費、速度快、結果固定(同一頁每次抓到的值都一樣)、數字可以精確保留。

缺點有三個:

  • 位置綁死:網站一改版,「第幾列第幾格」就跑掉,整套解析規則就壞了,要自己回頭維護。
  • 每個網站都要重寫一套:MOS、Subway、全家的網頁結構完全不同,規則沒辦法共用。
  • 有些網頁下載下來沒有資料:網頁的內容是「載入後再用 JavaScript 填進去」的話,直接下載 HTML 會發現裡面是空的,得先用工具模擬瀏覽器才拿得到,門檻更高。

新做法:讓 AI 讀懂網頁的意思

Firecrawl 是一個雲端服務:你把網址交給它,它幫你把網頁抓下來(包括需要 JavaScript 才會顯示內容的頁面),再回傳整理好的結果。這個專案用到它的兩種模式:

  • 模式一:轉成 markdown。 把雜亂的 HTML 變成乾淨的純文字,例如營養表格會變成「熱量 | 433.9」這樣的一行行文字。之後怎麼解析,還是我們自己寫。
  • 模式二:AI 語意抽取。 不用指定位置,只要告訴它「我要哪些欄位」,再附上一份 JSON schema(鎖死回傳格式),AI 就會讀懂網頁內容,直接回傳符合格式的 JSON。

實際傳給 Firecrawl 的請求長這樣(節錄自專案腳本):

// 模式一:只要乾淨的文字
{ url, formats: ['markdown'], onlyMainContent: true }

// 模式二:讓 AI 抽取,附上 prompt(要哪些分類)和 schema(回傳格式)
{ url, formats: ['json'], onlyMainContent: true, jsonOptions: { prompt: extractPrompt, schema: MULTI_ITEM_SCHEMA } }

它幫你省下的是「下載網頁、處理 JavaScript 載入」這一層,模式二還能省下「找位置」這一層。不過它也有代價:免費方案有次數限制(實測約每分鐘 11 次,超過就會收到 429 錯誤),所以腳本裡要每次請求隔幾秒、遇到錯誤自動重試。

三種做法對照看看:

做法 誰負責找資料的位置 優點 代價
傳統爬蟲(自己寫 selector 或正則) 我們自己寫規則 免費、快、精確、結果固定 改版就壞、每站要重寫、動態頁面門檻高
Firecrawl 轉 markdown,再自己正則解析 我們自己寫規則(但不用處理下載與 HTML) 省下下載與前置處理,數字仍精確 還是要自己寫和維護正則
Firecrawl AI 抽取 AI 自己讀懂網頁 不用寫規則,這類表格混雜的頁面特別快 數字可能被四捨五入、每次結果不一定完全一致

官方 API:有時候不用爬網頁

有些網站的網頁背後,其實是呼叫一支 API 取得資料。如果能直接呼叫那支 API,就能拿到最乾淨的資料,連解析網頁這一步都省了。

🔧 實際操作

整條蒐集流程長這樣

  1. 找資料來源
  2. 抓取資料(Firecrawl 或直接呼叫 API)
  3. 整理成 FoodItem 的格式
  4. 驗證熱量公式合理性(避免抓到明顯錯誤的數字)
  5. 存進 S3(存放的原因在 Day23 說明)

專案裡對應的腳本在 backend/scripts/(ingest-food-data.mjs 處理 MOS 與 Subway,ingest-familymart-data.mjs 處理全家)。Firecrawl 省掉的是「解析 HTML」這一層,不是整條流程,還是需要一支可以重複執行的腳本,把上面五步串起來。

方式一:Firecrawl 轉 markdown,再用正則表達式解析(MOS)

MOS 每個商品都有一頁獨立的詳細頁,版面統一。所以流程是:用 Firecrawl 把頁面轉成 markdown,再用正則表達式從文字裡抓出「熱量、蛋白質、脂肪」的數字(節錄):

const caloriesMatch = markdown.match(/熱量\s*\|\s*([\d.]+)/);

這段正則的意思是:找到「熱量」兩個字,後面接一道表格的豎線,再抓出後面的數字。這個方式的好處是數字保留原始小數位、同樣輸入結果一樣,所以資料的 confidence 標為 high。也因為解析是純函式,比再打一次 AI API 更快、更省錢、也更容易寫測試。

方式二:Firecrawl AI 抽取(Subway)

實測發現它的語意判斷是可信的:抓到沒有營養資料的頁面時,AI 會老實回填 0,不會亂猜一個數字。Subway 的官網是一頁裡混了多個表格,用正則表達式解析太脆弱,所以改用 Firecrawl 的 JSON schema 抽取模式,先把欄位結構鎖死,避免 AI 每次自己取的欄位名稱都不一樣(實測過一次叫 food_items、一次叫 items)。代價是 AI 抽取會把小數點四捨五入(例如 6.3 變 6),精確度比正則解析低一點,所以這類資料的 confidence 標為 medium。

方式三:直接呼叫官方 REST API(全家)

全家的「食在購安心」網站,表面上是要互動搜尋的網頁,但用瀏覽器的開發者工具攔截網路請求後,發現背後其實是一支沒有防護的官方 API,可以直接用程式呼叫,連 Firecrawl 都不用。這是這次蒐集裡最意外的發現,也讓全家一次拿到 1750 筆資料,整個資料庫因此來到 1915 筆(MOS 133 筆、Subway 32 筆、全家 1750 筆)。

真實踩過的雷

  • 整批抓完才存檔,中途被中止就全部白抓:
    改成每抓一筆就存進本機的檢查點檔案(checkpoint),重跑時自動跳過已經抓過的
  • 分類不能只看網址或官網的標籤名稱:
    MOS 的 soup.aspx 其實是咖啡系列頁面,真正的濃湯混在別的分類裡;全家把「小菜、滷味、湯品」全部併在同一類。這種坑踩了兩次,變成一個通則:拿到新資料來源時,一定要核對頁面實際內容,不能只信網址或分類標籤

Claude Code 在這裡幫了什麼

這幾支腳本是在 Claude Code 的協助下,邊寫邊除錯完成的。分工上,需要判斷的部分留給自己(找資料規律、決定用哪種蒐集方式、設計驗證規則),重複性高的腳本骨架與除錯交給 AI 加速。

小結

這篇的重點是:選蒐集方式,先看資料藏在哪裡。有整合頁面就讓 AI 抽取、網頁背後有 API 就直接呼叫。資料蒐集回來之後,下一篇 Day23 要處理的是:資料要放在哪裡?


上一篇
Day21 - Food Database 要長什麼樣子?
系列文
營養師想做一個飲食建議產品 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言