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 集合、预算、下一页链接和稳定输出。
继续阅读: