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"])))報告前先排序
你的練習清單
- 用 deque.popleft 實現廣度優先 frontier。
- URL 入隊時就加入 visited。
- 設定並報告頁數或請求預算。
- 把 rel=next 基於目前頁面補全。
- 輸出前排序或保留插入順序。
常見問題
廣度優先還是深度優先?
分頁通常用廣度優先,更符合站點結構,也能更早發現列表頁。只有需要完整處理一條分支時才選深度優先。
為什麼 visited 不用列表?
列表變大後成員檢查會變慢。集合提供常數時間尋找;需要順序時另存一個列表。
如何恢復中斷的抓取?
把 frontier、visited 和預算一起持久化。JSON Lines 或小型資料庫都可以,關鍵是原子儲存。
練習分頁抓取
12 道題,覆蓋佇列、visited 集合、預算、下一頁連結和穩定輸出。
繼續閱讀: