(四)收齐所有页面再交出本批数据
1. 页面请求和字段提取
继续使用第九章的 HTTPX 与 Beautiful Soup。fetch() 最多发出三次 GET;传输失败或服务器暂时不可用时,等待 0.1、0.2 秒后继续。普通 4xx 会直接抛出错误,调用方能看到失败的页。
# 项目文件:catalog_project/src/cataloghub/collecting.py
import time
from urllib.parse import urldefrag, urljoin, urlsplit
from bs4 import BeautifulSoup
import httpx
def fetch(client, url):
# 只重试这份只读 GET;普通 4xx 直接作为本次失败交给调用方。
for attempt in range(3):
try:
response = client.get(url)
except httpx.TransportError:
if attempt == 2:
raise
else:
if response.status_code not in {429, 500, 502, 503, 504} or attempt == 2:
response.raise_for_status()
return response
time.sleep(0.1 * 2**attempt)
def parse_page(html, url):
soup = BeautifulSoup(html, "html.parser")
cards = []
for node in soup.select("article.book"):
title = node.select_one(".title")
cards.append(
{
"source_id": node.get("data-id", ""),
"title": title.get_text(" ", strip=True) if title else "",
"finished": node.get("data-finished", ""),
"tags": [
tag.get_text(" ", strip=True) for tag in node.select(".tags li")
],
"page": url,
}
)
next_link = soup.select_one("a.next")
return cards, next_link.get("href") if next_link else None解析阶段只提取字段,先保留空白和原始已读文本,清洗集中交给 clean_one()。page 跟随每张卡片,使拒收记录能追溯到对应网页。这里保存的是解析后的原始字段;需要留存完整 HTML 时,可以在 fetch() 返回后另存响应文本。
2. 追踪下一页与访问记录
# 续写同一文件:catalog_project/src/cataloghub/collecting.py
def collect(source, client):
# 分页正常结束后才交出整批卡片;请求异常直接向上传递。
start = urlsplit(source)
expected_origin = (start.scheme, start.netloc)
if start.scheme not in {"http", "https"}:
raise ValueError("采集入口需要 HTTP 或 HTTPS 地址")
url, visited, cards = urldefrag(source)[0], set(), []
while url:
parts = urlsplit(url)
if (parts.scheme, parts.netloc) != expected_origin:
raise ValueError("下一页离开了当前目录来源")
if url in visited or len(visited) >= 20:
raise ValueError("分页重复或页数超过二十,请检查目录链接")
visited.add(url)
response = fetch(client, url)
current, next_link = parse_page(response.text, url)
cards.extend(current)
url = urldefrag(urljoin(url, next_link))[0] if next_link else None
return cards, len(visited)urljoin() 把 ?page=2 接到当前地址,urldefrag() 去掉不影响服务器文档的片段标记。visited 检出重复分页;来源检查能发现下一页被误写成另一站点。只有循环正常结束,函数才返回完整的 cards。
第一页得到三条,第二页累计六条,第三页累计十条。第三页没有下一页,循环结束。第二页请求失败时,异常会离开 collect(),调用方不会收到“只抓到第一页”的成功结果。改请求间隔、重试或并发时,都应保留这个完整批次的交接方式。