專項練習 · 8 分鐘閱讀

Python 分頁抓取練習:佇列、存取集與預算

分頁抓取本質上是帶預算的圖走訪。佇列決定下一頁,visited 集合防止繞圈,限制讓執行結束。這些練習先分別實現,再組合起來。

使用 FIFO 佇列

deque 的 append 和 popleft 提供廣度優先順序。分頁通常就需要這樣:先處理完目前列表層,再進入詳情頁。對很大的 frontier 使用 list.pop(0) 會退化為二次複雜度。

from collections import deque

queue = deque(["/page/1"])
seen = []
while queue:
    url = queue.popleft()
    seen.append(url)
    if url == "/page/1":
        queue.extend(["/page/2", "/page/3"])
print(seen)

先進先出

入隊時就標記已存取

如果等處理時才標記,兩個父頁面可能把同一個子連結都加入佇列。應在入隊時加入 visited,並在每次 append 前檢查。這樣集合和佇列對「待處理」的理解一致。

queue = ["/a"]
visited = {"/a"}
links = {"/a": ["/b", "/b", "/c"], "/b": ["/a"]}
order = []
while queue:
    page = queue.pop(0)
    order.append(page)
    for link in links.get(page, []):
        if link not in visited:
            visited.add(link)
            queue.append(link)
print(order)

不重複處理

消耗頁數預算

預算讓抓取可復現,也避免意外無限執行。是在解析前還是成功抓取後減一都可以,但要寫清楚。預算歸零時乾淨停止,並報告還有未完成的部分。

queue = ["/1", "/2", "/3", "/4"]
budget = 2
done = []
while queue and budget:
    done.append(queue.pop(0))
    budget -= 1
print(done)
print("remaining", len(queue))

有限的執行

顯式讀取下一頁連結

很多站點用 rel=next 而不是可預測的編號分頁。應解析這個連結並基於目前頁面補全。除非站點明確說明規則,否則不要從 /page/1 猜出 /page/2。

from html.parser import HTMLParser
from urllib.parse import urljoin

class Next(HTMLParser):
    def __init__(self):
        super().__init__()
        self.href = None
    def handle_starttag(self, tag, attrs):
        values = dict(attrs)
        if tag == "a" and values.get("rel") == "next" and self.href is None:
            self.href = values.get("href")

p = Next()
p.feed('<a rel="next" href="/page/2">Next</a>')
print(urljoin("https://example.com/page/1", p.href))

跟隨聲明的下一頁

讓輸出順序確定

集合適合判斷成員,不適合穩定輸出。寫報告時把 visited 轉成排序列表,或者另外保留插入順序列表。確定的輸出才能做有意義的 diff 和測試。

visited = {"/b", "/a", "/c"}
print(sorted(visited))
print(list(dict.fromkeys(["/b", "/a", "/b", "/c"])))

報告前先排序

你的練習清單

常見問題

廣度優先還是深度優先?

分頁通常用廣度優先,更符合站點結構,也能更早發現列表頁。只有需要完整處理一條分支時才選深度優先。

為什麼 visited 不用列表?

列表變大後成員檢查會變慢。集合提供常數時間尋找;需要順序時另存一個列表。

如何恢復中斷的抓取?

把 frontier、visited 和預算一起持久化。JSON Lines 或小型資料庫都可以,關鍵是原子儲存。

練習分頁抓取

12 道題,覆蓋佇列、visited 集合、預算、下一頁連結和穩定輸出。

打開練習

繼續閱讀: