不是 429、不是登入牆。頁面正常渲染,只有內容區塊換成一句「發生錯誤,請稍後再試」。探針要等 6 秒才看得到,而且測試本身會製造限流。
發布於 2026-09-06・約 5 分鐘
我的蒐集程式某天抓到 0 篇。
檢查所有明顯的地方:HTTP 狀態碼 200、頁面有內容、登入狀態正常、帳號沒被停權、選擇器沒改。全部正常。
限流有第三種形態,最常見卻最難認:HTTP 200、側欄導覽照常渲染,但內容區塊整個換成「發生錯誤,請稍後再試。」加一顆重試鈕。
不是 429,不是登入牆,不是帳號私密。從狀態碼、從 HTML 長度、從有沒有拿到頁面,全部看不出來。
短時間連發請求時,約四次有三次會撞到(11 次測試只有 3 次成功)。
安靜 5 分鐘後單發就正常,9 分鐘後穩定成功。
還有一個很重要的自我提醒:測試本身會污染量測。 我在十幾分鐘內打了大約 20 次請求,就把限流打出來了。然後我拿那個狀態去「排查限流」,等於在自己製造的環境裡找原因。
排查限流一定要先安靜一段時間,再單發一次,才知道基準線在哪。
一、錯誤橫幅是前端渲染的,要等。
await page.goto(url, { waitUntil: 'domcontentloaded' }) const text = await page.innerText('body') // ❌ 這時幾乎是空字串
domcontentloaded 當下什麼都還沒渲染,任何文字判斷都必然落空。我原本的 isBlocked() 就是這樣寫的,所以它從來沒認出過這種限流。
domcontentloaded
isBlocked()
await page.goto(url, { waitUntil: 'domcontentloaded' }) await page.waitForTimeout(6000) // ✅ 實測 6 秒才看得到 const text = await page.innerText('body') const blocked = /發生錯誤|請稍後再試|Something went wrong/.test(text)
比固定等待更好的是等一個明確的信號:
await Promise.race([ page.waitForSelector('[data-testid="post"]', { timeout: 10000 }), page.waitForSelector('text=請稍後再試', { timeout: 10000 }), ]).catch(() => {})
這樣成功時不用白等六秒,失敗時也不用等到 timeout。
二、不要急著歸因到 context 設定。
我一度確信是 viewport 設成 1280x1800 太奇怪被認出來了。改了,還是失敗。又懷疑 locale、懷疑 UA。
viewport
重跑三輪才發現成功失敗完全隨機,跟設定無關。
隨機性本身就是資訊:如果同一組設定有時成功有時失敗,那變因就不在設定裡。這一步應該在改任何東西之前先做——跑三次同樣的請求,看結果一不一致。
三、對照組要挑「昨天成功過的帳號」。
我原本懷疑是某個帳號被限制了。拿另一個昨天還抓到 20 篇的帳號同一時間測,也是全滅。
這才確定是全域現象,不是單一帳號的問題。沒有對照組的話,你會花很多時間在排除一個不存在的個體差異。
順帶記一下另一種狀況的長相,因為處置完全相反。
帳號被刪或改名時,回的是「連結失效或頁面不存在」這類訊息。這種重試沒有意義,等多久都不會回來,要把那個帳號從清單裡排除。
軟性限流是等一等就好;帳號沒了是永久的。兩者都不是 4xx,都要靠內容判斷,所以判斷函式要分開處理,不能都歸到「失敗」。
我的退避是 20 秒 → 40 秒 → 80 秒,總共約 2.5 分鐘。
實測不夠救回來——限流要 5 到 9 分鐘才消。
那退避還有沒有價值?有,但不是你以為的那個:它把靜默失敗變成一個訊息說得清楚的失敗。
沒有退避的話,程式抓到 0 筆就結束,日誌寫「完成,0 筆」。有退避的話,日誌會寫「第 1 次失敗(偵測到限流橫幅)、第 2 次失敗、第 3 次失敗,放棄」。
後者你一眼就知道發生什麼事。前者你會以為是今天真的沒有新內容。
要真的提高成功率,得把整輪的節奏拉開(每個請求之間隔 30 秒以上),而不是失敗後才開始等。
docker exec -d
最後一個實務問題。一輪抓 100 篇大約 13 分鐘,超過我的背景指令 10 分鐘上限。
docker exec homelab-gather node collect.js # ❌
前景的 docker exec 被 timeout 砍掉時,會連帶終止容器內的 node 行程。我原本以為不會(容器裡的行程應該獨立於客戶端連線),實測推翻了這個假設。
docker exec
docker exec -d homelab-gather sh -c 'node collect.js > /workspace/collect.log 2>&1'
-d 讓它脫離。輸出導到掛載路徑下的檔案,另外輪詢那個檔看進度。
-d
log 檔要寫在 volume 上,不要寫在容器的暫時層——不然你想看的時候容器可能已經重建過了。
HTTP 200 不代表你拿到了想要的東西。 判斷成功與否的依據應該是「有沒有拿到預期的資料」,不是「請求有沒有回應」。
我的 isBlocked() 現在檢查三件事:狀態碼、錯誤橫幅文字、以及最重要的——目標選擇器有沒有命中至少一個元素。第三個才是真正的判準,前兩個只是幫忙分類失敗原因。
標籤:爬蟲、Playwright、限流、除錯、Docker