URL 规范化

简而言之:URL 规范化会将指向同一资源的不同 URL 字符串转换为统一的规范形式,使爬虫不会通过不同地址多次抓取同一页面。

URL 规范化也称为 URL 标准化,是在 URL 进入爬虫队列之前对其进行标准化的过程。同一页面可能有数十种 URL 变体,这些变体在大小写、末尾斜杠、参数顺序或协议方面有所不同。如果不进行规范化,每个变体都会被视为不同的页面,从而增加抓取量、造成存储数据重复,甚至可能导致爬取永远无法结束。

标准规范化规则:

  1. 将协议和主机名转换为小写:HTTP://Example.COM/Page 变为 http://example.com/Page。路径仍区分大小写,因为许多服务器会以这种方式处理路径。
  2. 移除默认端口:http://example.com:80/page 变为 http://example.com/page,HTTPS 的 443 端口也同样如此。
  3. 解析点路径段:/a/b/../c 变为 /a/c。
  4. 解码非保留字符:%7Euser 变为 ~user,因为非保留字符可以选择不进行百分号编码。
  5. 对查询参数排序:?b=2&a=1 变为 ?a=1&b=2,从而使参数顺序不再重要。
  6. 移除片段标识符:# 之后的所有内容都属于客户端信息,永远不会发送到服务器,因此会被移除。

规范化前后对比:

原始 URL 规范化后的 URL
HTTPS://Example.com:443/Shoes/ https://example.com/Shoes/
https://example.com/shoes?color=red&size=9 https://example.com/shoes?color=red&size=9
https://example.com/shoes?size=9&color=red https://example.com/shoes?color=red&size=9
https://example.com/a/b/../shoes#reviews https://example.com/a/shoes
https://example.com/shoes?utm_source=email https://example.com/shoes

第二行和第三行最初是不同的字符串,最终会归一为相同的规范形式,而这正是规范化的意义所在。第五行假设跟踪参数会被移除,但这是一个可配置的选项,而不是通用规则。

可能造成问题的规则:

有些规范化步骤看似安全,实则不然。以下每项操作都会在至少部分网站上改变 URL 的含义。

移除末尾斜杠。在大多数服务器上,/path 和 /path/ 会返回相同内容,但有些服务器会返回重定向,还有一些则会返回完全不同的页面。将它们规范化为同一 URL,可能会在无提示的情况下合并两个不同的资源。

移除所有查询参数。对于 utm_source 或 fbclid 等跟踪参数而言是安全的,但对于 ?id=4417 或 ?page=3 等用于选择内容的参数,则会造成破坏性影响。

强制使用 HTTPS。对于现代网站而言通常是正确的做法,但如果主机从未实现 TLS,就会生成无效 URL。

将路径转换为小写。这会在区分大小写的服务器上导致错误,因为 /Products 和 /products 可能是两个不同的页面,也可能一个是有效页面,另一个返回 404。

安全的默认做法是,仅对 URL 规范保证等效的部分进行规范化,然后在观察目标网站的实际行为后添加网站特定规则。

实现:

from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode

TRACKING = {"utm_source", "utm_medium", "utm_campaign", "fbclid", "gclid"}
DEFAULT_PORTS = {"http": 80, "https": 443}

def normalize(url):
    parts = urlsplit(url)

    scheme = parts.scheme.lower()
    host = parts.hostname.lower() if parts.hostname else ""

    if parts.port and parts.port != DEFAULT_PORTS.get(scheme):
        host = f"{host}:{parts.port}"

    params = [(k, v) for k, v in parse_qsl(parts.query)
              if k.lower() not in TRACKING]
    query = urlencode(sorted(params))

    path = parts.path or "/"

    return urlunsplit((scheme, host, path, query, ""))

通过将空字符串作为最后一个组成部分传入来丢弃片段标识符。参数会经过排序,避免参数顺序产生错误的重复项,同时移除已知的跟踪参数键。

规范化与去重:

规范化是爬虫去重机制有效运行的基础。URL 待爬取队列会将每个新发现的 URL 与已发现 URL 集合进行比较,而只有当两边都以相同方式规范化时,这项检查才有意义。不一致的规范化工具会导致待爬取队列不断接受已抓取页面的各种变体。

这种影响在大型网站上会不断累积。假设一个产品页面可通过四个分类路径、两种排序方式和三个跟踪来源访问,那么规范化前会产生二十四个不同的 URL,规范化后则只有一个。对于包含 50,000 件产品的目录而言,这可能决定一次爬取任务是可控完成,还是将预算耗尽在重复内容上。

URL 规范化与规范标签:

这两种机制从不同方向解决相关问题。URL 规范化由爬虫工具执行,在抓取前应用通用规则。rel="canonical" 标签则由网站在 HTML 中声明,只有在抓取页面后才能看到。

规范标签更具权威性,因为网站所有者知道应优先使用哪个 URL,但发现这些标签时已经来不及节省请求。生产环境中的爬虫会同时使用这两种机制:通过规范化避免抓取明显的重复项,并通过规范标签合并只有在获取页面后才能发现的重复内容。

Bright Data 的爬虫 API会自动规范化新发现的 URL,并遵循规范声明,因此同一页面的重复变体不会消耗爬取预算。

准备好开始了吗?