Python 现代开发学习笔记

(四)收齐所有页面再交出本批数据

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(),调用方不会收到“只抓到第一页”的成功结果。改请求间隔、重试或并发时,都应保留这个完整批次的交接方式。

依次请求三页,显示 visited、卡片累计数和下一页地址,直到十条原始记录全部收齐

笔记正文与全部题解 · Ctrl / ⌘ K