Firecrawl

抓到第一頁不等於完成:Firecrawl 用結構化輸出處理 Pagination

Agent 抓分頁網站常在第一頁三十筆就回報完成。本文拆解 Firecrawl 教學的解法:用 JSON Schema 固定每頁資料形狀、以空結果作為通用停止條件,從 Hacker News 全站 61 頁實測走到 offset 分頁變體,整理成可直接套用的迴圈模式與工具邊界。

抓到第一頁不等於完成:Firecrawl 用結構化輸出處理 Pagination — 文章封面
本頁內容7 個段落
  1. 第一頁不是完成:缺的是可驗證的停止條件
  2. 先把每頁變成結構化資料,而不是文字牆
  3. Schema 固定形狀後,停止條件縮成一行檢查
  4. Hacker News 實戰:61 頁、略低於 1,040 則
  5. Offset 分頁:換一個遞增變數而已
  6. 額度與邊界:什麼時候不該用這個迴圈
  7. 參考來源

請 agent 抓一個分頁網站,最常見的失敗不是解析出錯,而是它整理完第一頁的三十筆資料,就自信地回報「完成」。真正的難題有兩個:每一頁的結果怎樣以相同形狀合併,以及怎樣可靠地知道後面還有沒有頁。Firecrawl 在 7 月 15 日的教學用一個極簡迴圈同時回答這兩件事:每頁先轉成符合 schema 的 structured JSON,持續遞增頁碼,直到某頁回傳空陣列。作者以這個迴圈走完 Hacker News 全部 61 頁、收集略低於 1,040 則 stories——全程不需要預先知道總頁數。

第一頁不是完成:缺的是可驗證的停止條件

問題的本質是完成證據。只拿到「把所有頁面抓完」這種自然語言指令時,agent 沒有可靠的方法知道後面還有幾十頁;它看到的清單有內容、有長度,從表面上分不出「完成」與「第一頁」。Hacker News 是最典型的例子:每頁固定 30 則,對同一個 URL 呼叫單次 scrape,永遠只會拿到同樣的 30 筆。停止與否因此不是解析問題,而是流程問題——除非有一條程式可驗證的規則,否則 agent 只能停在第一頁。

Firecrawl 給的規則通用到近乎樸素:回傳零結果的那一頁,就是最後一頁。這條規則的價值在於不必預知總頁數——總頁數是迴圈走完才知道的輸出,不是事前要查的輸入。

先把每頁變成結構化資料,而不是文字牆

停止條件要能縮成一行檢查,前提是每頁輸出有固定形狀。scrape 在省略 formats 時預設回 markdown,對人眼閱讀方便,但整頁會混成一段文字牆:標題、分數、留言數黏在一起,程式沒有穩定的欄位可以取。更根本的差別是,Firecrawl 回 structured JSON 而非 raw HTML——不需要為每個網站寫 site-specific 的解析器,這是它能宣稱「同一個迴圈套任何分頁網站」的基礎。

json format 的彈性有兩層:可以只給 prompt,讓模型自行決定欄位;也可以加上 schema,保證每次回傳的形狀一致。要做迴圈,就用 schema——只有 prompt 的輸出,撐不起「檢查陣列長度」這種程式化的停止判斷。

Schema 固定形狀後,停止條件縮成一行檢查

在 Node SDK 裡,schema 用 Zod 定義,送出前自動轉成 JSON Schema 作為 wire 格式。以 Hacker News 為例,形狀是一個 stories 陣列,每筆含 titleurlpointscomments 四個欄位。回傳的 result.json 會精確符合這個 schema——不是大致像,而是欄位名稱與型別都對得上。

有了這個保證,資料收集與停止判斷就是同一個迴圈:

while (true) {
  const rows = (await scrapePage(page)).json.stories;
  if (rows.length === 0) break;
  stories.push(...rows.map((row) => ({ ...row, page })));
  page++;
}

每列在 push 前都標上來源頁碼的 page 欄位。這個小動作換來可稽核性:任何一筆資料都能回溯到它出現的頁面,事後要對帳或去重都有依據。迴圈結束後把 stories 寫進 stories.json,任務以一個檔案收尾。

Hacker News 實戰:61 頁、略低於 1,040 則

Hacker News 的分頁機制單純:URL 上的 ?p= 參數隨頁碼遞增,每頁固定 30 則。把上一節的迴圈接上這條規則,就是教學的完整實作。依作者的實測,迴圈走完全部 61 頁、收集略低於 1,040 則 stories——61 這個數字不是設定出來的:迴圈一路走到第一個回傳空陣列的頁面才停,而 61 是停下前走完的最後一個有內容的頁。

值得停下來看的是「頁數是被發現的」這件事。任何在前面幾頁就判斷「應該沒了」的流程都是在猜,而在分頁網站上,猜錯的代價不是零星幾筆,而是一整段沒被看見的資料。把完成判斷交給規則,agent 才第一次有了可以拿來對質的完成證據。

Offset 分頁:換一個遞增變數而已

不是所有網站都用頁碼參數。舊論壇與目錄網站常見 offset pagination:URL 上的 start 參數每次遞增一個 page size,例如 start=0start=25start=50。改寫幅度極小——迴圈裡把 page++ 換成 start += PAGE_SIZE,停止條件「空結果即停」完全不變。

這帶出教學更通用的一層:同一個迴圈可以套到任何分頁網站,要換的只有兩件事——URL 的組法與 schema 的欄位。停止條件是所有分頁網站共用的,網站之間的差異被隔離在這兩個參數裡,這也是這個模式值得直接搬進 production 的原因。

額度與邊界:什麼時候不該用這個迴圈

配套的額度規則要先弄清楚。scrape endpoint 可以 keyless 使用:透過官方 client(SDK、CLI 或 MCP server)不需要 API key,走 per-IP rate limit。但 crawlextractmap 需要 API key,在 dashboard 註冊可以解鎖這些端點並提高限流。換句話說,本篇的單頁迴圈可以匿名跑,但要往整站層級走,註冊是必要的前置。

更重要的是選對入口。教學把分工講得很清楚:要遍歷整個網站,用 /crawl——link discovery 與 traversal 是它內建的能力,本來就不是手動迴圈該做的事;遇到 URL 不變的分頁——infinite scroll 或點擊載入——用 interact,它維持真實的 browser session,資料要靠操作才會出現。這篇的迴圈適用的,是「URL 會隨頁碼或 offset 變化」的清單頁;三種情境對應三個入口,拿迴圈硬套另外兩種是錯誤的設計方向。

參考來源

本文由 AI 協助自上述來源整理,經人工審核後發布。

分享X電郵