跳转至

URL 处理:相对链接、规范化与去重

网页中的 href 往往不是完整 URL。正确处理 URL 能避免抓错域名、重复抓取同一页面,或把锚点链接误当成新页面。

1. 相对链接必须基于当前页面解析

不要手工用字符串拼接链接。使用 urllib.parse.urljoin()

from urllib.parse import urljoin

page_url = "https://example.com/news/list?page=2"
print(urljoin(page_url, "/about"))       # https://example.com/about
print(urljoin(page_url, "detail/1"))     # https://example.com/news/detail/1
print(urljoin(page_url, "../help"))      # https://example.com/help

urljoin() 的第二个参数若本身是绝对 URL,会直接替换原站点。因此,对不可信的页面内容或任务只允许同站 URL 时,必须在拼接后再次校验域名。

2. 只抓取允许的协议和域名

from urllib.parse import urlparse


def is_allowed_url(url: str, allowed_host: str) -> bool:
    parsed = urlparse(url)
    return (
        parsed.scheme in {"http", "https"}
        and parsed.hostname == allowed_host
    )

这会排除 mailto:javascript:data: 等非网页链接,也避免任务无意扩展到其他域名。实际任务若允许子域名,应显式写出允许规则,不要使用宽泛的字符串包含判断。

3. 片段标识不代表新的 HTTP 资源

https://example.com/article#comments 中的 #comments 是 fragment。它由客户端用于定位文档内部位置,不会发送给 HTTP 服务器。对“页面级”去重,通常应去掉 fragment:

from urllib.parse import urldefrag

canonical, fragment = urldefrag("https://example.com/article#comments")
print(canonical)  # https://example.com/article

不要擅自删除查询参数。?page=2?id=10 等参数可能改变资源内容;只有在理解目标站点 URL 语义后,才能制定参数规范化规则。

4. 去重的最小实现

seen: set[str] = set()

def enqueue(url: str) -> bool:
    url, _ = urldefrag(url)
    if url in seen:
        return False
    seen.add(url)
    return True

seen 只能保证本次运行内的 URL 不重复。跨运行去重需要持久化 URL 或内容指纹,并明确保留策略。

参考

评论