摘要: Firecrawl 於 7 月 15 日的教學用每頁符合 schema 的 structured JSON,並以空陣列作為可驗證的分頁停止條件,不必預先知道總頁數。作者實測掃過 Hacker News 全部 61 頁,收進略低於 1,040 則 stories;文章也把 /crawl 留給整站遍歷,Interact 留給 URL 不變的介面。
讓 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,省去替每個網站客製解析器的功夫;「同一個迴圈適用任何分頁網站」這句話,正是站在這個基礎上才說得出口。
json format 有兩種強度:一種只給 prompt,欄位交給模型自行發揮;另一種再疊上 schema,讓每次回傳的形狀受到保證。目標既然是跑迴圈,答案只有 schema——單憑 prompt 的輸出,撐不住像「檢查陣列長度」那樣的程式化停止判定。
形狀由 schema 定死之後,停止判斷只剩一行檢查
在 Node SDK 底下,schema 以 Zod 撰寫,請求送出前會自動轉成 JSON Schema 當作 wire 格式。套到 Hacker News 上,這個形狀是一個 stories 陣列,每筆都帶 title、url、points、comments 四個欄位。送回的 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 從頭到尾沒被任何人設定過:迴圈持續前進,直到頭一頁交回空陣列才停止;那個數字就是停下之前被走完的最後一個仍有內容的頁面。
「總頁數是被發現的」——這一點值得專門停下來體會。任何流程只要在前幾頁就下結論說「後面大概沒了」,它做的事就是猜;對分頁網站來說,猜錯賠掉的往往不是零零星星幾筆,而是整整一段從未被看見的資料。唯有把完成的判定交給規則,完成證據才第一次變得經得起對質。
Offset 分頁:改的只是遞增的那個變數
並非每個網站都用頁碼分頁。老論壇與目錄型網站常見的是 offset pagination:URL 帶一個 start 參數,每次往後加一個 page size,例如 start=0、start=25、start=50。此時要動的程式碼少得可憐——把迴圈裡的 page++ 改成 start += PAGE_SIZE 即可,「回傳空結果就停」這條停止條件原封不動。
這裡浮現出教學更具普遍性的一面:迴圈本體對任何分頁網站都通用,需要替換的只有兩處——URL 怎麼拼、schema 放哪些欄位。停止條件則是各家分頁網站共用的資產,站與站之間的差異全被關進這兩個參數裡;這個模式之所以值得原樣搬進 production,理由就在這裡。
額度與適用邊界:這個迴圈何時派不上用場
先把搭配的額度規則弄明白。scrape endpoint 支援 keyless 使用:經由官方 client(SDK、CLI 或 MCP server)呼叫時不必備 API key,限制走的是 per-IP rate limit。相對地,crawl、extract、map 三者都需要 API key,到 dashboard 完成註冊即可解鎖這些端點並放寬限流。也就是說,這篇的單頁迴圈匿名就能執行;可一旦想升級到整站規模,註冊就成為繞不開的前置步驟。
比額度更要緊的是挑對入口。教學把三者分工說得一清二楚:目標是遍歷整個網站時,該用 /crawl——link discovery 與 traversal 屬於它內建的能力範圍,本就不該交給手動迴圈來做;碰上 URL 始終不變的分頁——infinite scroll 或點擊載入——該用 interact,它背後是真實的 browser session,資料必須靠操作才會浮現。本文這個迴圈鎖定的對象,是「URL 隨頁碼或 offset 改變」的列表頁面;三種情境各配一個入口,硬把迴圈套上另外兩種,是方向錯誤的設計。
