(十)取得详情并还原完整 Book
1. 从页面属性逐步得到字段
继续把下面部分写入同一个 parsers.py。这次先确定详情根节点,再读取编号、书名、状态和标签:
# 续写同一文件:crawler_project/src/catalogcrawl/parsers.py
def parse_detail(html: str, source_url: str) -> CollectedBook:
soup = BeautifulSoup(html, "html.parser")
root = soup.select_one("article#book")
if root is None:
raise ParseError(f"{source_url}:没有找到详情 article#book")
title = root.select_one("h1")
source_id = root.get("data-id")
flag = root.get("data-finished")
if title is None or not isinstance(source_id, str) or not source_id.strip():
raise ParseError(f"{source_url}:缺少书名或源站编号")
if flag not in {"true", "false"}:
raise ParseError(f"{source_url}:data-finished 不是 true 或 false")
record = {
"title": title.get_text(" ", strip=True),
"finished": flag == "true",
"tags": [tag.get_text(" ", strip=True) for tag in root.select("ul.tags > li")],
}
try:
book = book_from_dict(record)
except RecordError as cause:
raise ParseError(f"{source_url}:{cause}") from cause
return CollectedBook(source_id.strip(), book, source_url)data-finished="false" 先得到字符串 false,比较 flag == "true" 才得到 False;标签节点被分别提取成列表。book_from_dict 再检查书名、布尔类型和标签,并清理重复标签。网络请求和记录检查之间保留了解析这一步,出错时可以知道具体停在哪里。
2. 两个地址读到同一本书
# 项目文件:demos/details_sync.py
# 两个不同地址最终指向同一本书,URL 集合之外还要按源站编号识别记录。
import httpx
from crawlsite import running_site
from catalogcrawl.parsers import parse_detail
if __name__ == "__main__":
with (
running_site() as site,
httpx.Client(timeout=1, trust_env=False, follow_redirects=True) as client,
):
items = {}
for path in ["/books/b101", "/alias/b101", "/books/b102"]:
response = client.get(site.base_url + path)
response.raise_for_status()
item = parse_detail(response.text, str(response.url))
items.setdefault(item.source_id, item)
for item in items.values():
print(item.source_id, item.as_record())程序请求正常详情、它的跳转别名,以及第二本书,最后只留下 b101 和 b102 两条记录。b101 的 finished 为 False,标签是 ['基础', '语法'];b102 为 True,标签是 ['数值']。按编号执行 setdefault 保留第一次取得的记录。
实际采集时,来源 URL 也跟着记录保存。之后发现某个标题提取错了,可以按这个地址重新查看正文,而不是从最终 CSV 里的几个字倒猜出错页面。