Python 现代开发学习笔记

(八)从当前目录页找到详情与下一页

1. 将页面结构检查集中到解析函数

parsers.py 的第一部分负责目录。先找到 main,再逐张找到详情链接,最后读取 next:

# 项目文件:crawler_project/src/catalogcrawl/parsers.py
# 先在本张卡片或详情节点内取字段,缺失结构带着来源路径反馈。
from bs4 import BeautifulSoup

from bookdata.errors import RecordError
from bookdata.records import book_from_dict

from .errors import ParseError
from .models import CollectedBook
from .urls import normalized_url


def parse_catalog(html: str, page_url: str):
    soup = BeautifulSoup(html, "html.parser")
    catalog = soup.select_one("main#catalog")
    if catalog is None:
        raise ParseError(f"{page_url}:没有找到目录 main#catalog")
    cards = catalog.select("article.book-card")
    if not cards and catalog.select_one(".empty") is None:
        raise ParseError(f"{page_url}:目录没有卡片,也没有空目录标记")
    links = []
    for card in cards:
        anchor = card.select_one("a.detail[href]")
        if anchor is None:
            raise ParseError(f"{page_url}:某张卡片缺少详情链接")
        links.append(normalized_url(page_url, anchor["href"]))
    next_link = soup.select_one('a[rel~="next"][href]')
    next_url = normalized_url(page_url, next_link["href"]) if next_link else None
    return links, next_url

返回值由两个部分组成:本页全部详情链接,以及下一页 URL 或 None。当前页面有三张卡片时返回三项,其中可以出现相同链接;去重交给任务收集层,解析函数继续如实反映页面内容。

目录结构缺失和目录确实为空也分开处理。练习站把空目录写为 .empty;页面里连 main 都没有,或者卡片、空目录标记都没有时,就报告结构错误。这样服务器临时返回一张登录页时,不会被记录成“已经采完零本书”。

2. 顺着下一页链接循环推进

# 项目文件:demos/pages_sync.py
# 逐页发现下一条链接,用集合识别已经访问的目录,详情保留首次发现顺序。
from urllib.parse import urlsplit

import httpx
from crawlsite import running_site
from catalogcrawl.parsers import parse_catalog


if __name__ == "__main__":
    with running_site() as site, httpx.Client(timeout=1, trust_env=False) as client:
        url = site.base_url + "/catalog/?page=1"
        seen_pages, links, cards = set(), {}, 0
        while url is not None and url not in seen_pages:
            response = client.get(url)
            response.raise_for_status()
            found, next_url = parse_catalog(response.text, str(response.url))
            seen_pages.add(url)
            cards += len(found)
            for link in found:
                links.setdefault(link, None)
            print(
                "页", urlsplit(url).query, "卡片", len(found), "已发现详情", len(links)
            )
            url = next_url
        print("合计", len(seen_pages), "页", cards, "张卡片", len(links), "个详情")

输出过程为:

页 page=1 卡片 3 已发现详情 2
页 page=2 卡片 3 已发现详情 4
页 page=3 卡片 3 已发现详情 6
合计 3 页 9 张卡片 6 个详情

下一页地址来自当前响应,不通过猜一个很大的页数反复尝试。url is None 表示页面没有下一页;seen_pages 则记录已经处理的地址,遇到分页链接绕回第一页时停止重复读取。

第1页发现第2页,第2页发现第3页,最后next为空,每次同时更新页集合和详情集合

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