(七)补全链接并确定 URL 去重键
1. 链接相对于当前页面解析
目录的当前地址为 /catalog/?page=1,详情 href 为 ../books/b101?from=catalog#intro。urljoin 先按当前页面补全路径,得到 /books/b101;from 记录链接来自哪里,片段 intro 留给客户端。分页中的 ?page=2 则继续选择另一份目录,不能一起删除。
# 项目文件:crawler_project/src/catalogcrawl/urls.py
# 先补全相对链接,只移除练习站的跟踪参数 from 与客户端片段。
from urllib.parse import parse_qsl, urlencode, urljoin, urlsplit, urlunsplit
from .errors import CrawlError
def normalized_url(page_url: str, href: str) -> str:
base = urlsplit(page_url)
target = urlsplit(urljoin(page_url, href))
if target.scheme not in {"http", "https"}:
raise CrawlError(f"链接不是 HTTP 地址:{href}")
if (target.scheme, target.hostname, target.port) != (
base.scheme,
base.hostname,
base.port,
):
raise CrawlError(f"链接离开本次目录所在站点:{href}")
if target.username is not None or target.password is not None:
raise CrawlError("目录链接不使用 URL 中的用户名和密码")
query = [
(key, value)
for key, value in parse_qsl(target.query, keep_blank_values=True)
if key != "from"
]
return urlunsplit(
(target.scheme, target.netloc, target.path or "/", urlencode(query), "")
)这段函数只移除练习站用作来源跟踪的 from,以及客户端片段,其他查询继续保留。两个 edition 不同的查询可能代表不同版本,若把所有查询都删掉,就会把它们错误合并。parse_qsl 交回按次序排列的键值对,keep_blank_values=True 保留空值,再用 urlencode 重新编码。
本次任务沿同一个目录站点推进。发现链接指向另一个站点,或使用 javascript 这样的协议时,函数会把该链接反馈给调用者,避免把页面里的每种链接都当作目录任务。
# 项目文件:demos/url_keys.py
# from 和片段消失,真正选择资源的 page 查询仍保留。
from catalogcrawl.urls import normalized_url
base = "http://127.0.0.1:8766/catalog/?page=1"
for href in ["../books/b101?from=top#intro", "../books/b101?from=bottom", "?page=2"]:
print(normalized_url(base, href))前两行得到相同的 /books/b101 地址,第三行仍然包含 /catalog/?page=2。后面把这种补全后的 URL 作为详情任务表的唯一键。