专项练习 · 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 集合、预算、下一页链接和稳定输出。

打开练习

继续阅读: