(八)从当前目录页找到详情与下一页
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 则记录已经处理的地址,遇到分页链接绕回第一页时停止重复读取。