重复的 URL:一个页面为什么会有多个地址

2026-11-02 · 1215 词

一个页面从来不只有一个地址。客户端能构造出多少个都指向它的写法,它就有多少个地址;而每一种写法,在任何见过它的缓存、日志、统计报表和访问规则里,都是一个独立的键。很多人以为是"缓存问题"或"重复内容问题"的怪现象,源头都在这里。

规范对"哪些差异有意义、哪些只是写法不同"其实说得很清楚。麻烦的是现实:服务器、代理、客户端各自在不同环节做归一化,或者根本不做。

六种差异,各自会造成什么

尾斜杠。 对任何缓存和日志来说,example.com/about 与 example.com/about/ 是两个不同的 URL。它们是不是同一个资源,由服务器决定:它可以重定向到另一个、两个都返回同样内容、或者其中一个 404。规范里没有一条能替你定这件事,只有服务器的实际行为能。

协议与主机的大小写。 HTTPS://Example.COM/x 与 https://example.com/x 等价,主机名天生大小写不敏感。但路径大小写不是:/About 与 /about 可能是两个页面。所以归一化必须在主机边界处停下。

默认端口。 https://example.com:443/x 与 https://example.com/x 是同一个地址。端口规则是跟着协议走的 —— https 是 443,http 是 80 —— 所以"把所有端口都删掉"的归一化会把合法的非默认端口一起弄坏。

百分号编码的大小写。 %2F 与 %2f 是同一个字节:三元组里的十六进制数字大小写不敏感。但你的字符串比较敏感,于是浏览器认为相同的两个 URL,在你的缓存里会哈希成两个键。

过度编码。 %41 就是 A,而 A 从来不需要编码。给非保留字符编码,等于给同一个 URL 造出第二种拼法。RFC 3986 要求归一化时把这些解回去 —— 而同一条规则也要求:解出来是 . 的 %2E 不能因此改变路径结构,这两件事必须一起做,只做一半会造出路径穿越。

查询参数的顺序与重复。 抽象地说 ?a=1&b=2 与 ?b=2&a=1 等价,但没有哪条规范要求服务器这么认为,多数服务器也不这么认为。重排参数对缓存是安全的、对签名是不安全的 —— 这里没有"普遍正确"的答案,只有一个你定下来、然后到处都照做的决定。重复参数(?a=1&a=2)是第三种情况:规范什么都没说,于是每个框架自己选一条规则,"取第一个"和"取最后一个"是真会出 bug 的地方。

重复地址到底从哪来

多数重复 URL 不是用户手抖,而是软件自己造出来的:

最后一条代价最大。canonical 只是"我更希望是这种拼法"的提示,它拦不住其他拼法被抓取、被缓存、被计数。当站内链接和 canonical 不一致时,你等于一边告诉搜索引擎要哪一种,一边亲手生产另一种。

三层,各管一段

服务器管映射。 从非规范形态重定向到规范形态,是唯一真能把多余的那个键从缓存和浏览器里清掉的机制。301/308 是永久、302/307 是临时;选哪个,会影响旧形态还会不会被继续请求。

页面管偏好。 多个地址返回同样内容时,rel="canonical" 说明你希望哪一条被索引。它是建议性的。它和重定向一致时最有效,与之矛盾时最没用。

应用管签名。 如果你对 URL 签名 —— 上传、CDN 访问、重置密码链接 —— 归一化必须发生在签名之前,而且每个环节的归一化必须完全一致。给一个重排过的查询串签名,签名会在那个把参数排过序的服务器上失败,而错误信息只会说"签名无效",不会说"你把参数排序了"。

一组能落地的规则

  1. 每个资源只定一种形态,并写下来。 带不带尾斜杠、带不带 www、路径是否小写 —— 定一次,之后任何偏离都算 bug。
  2. 其余形态在边缘重定向掉。 一条 308 胜过五十个 canonical。本站自己就是这么做的:/x 一律重定向到 /x/,/x.html 重定向到 /x。代价在日志里看得见 —— 总有一小股 308,来自只听说过短写法的客户端。
  3. 比较和签名之前先归一化。 解掉非保留字符的编码、三元组统一大写、去掉默认端口,然后再哈希。如果你必须支持参数重排,那就在签名处和验证处都排序。
  4. 不要指望搜索引擎很快帮你合并。 它们确实会合并,但很慢。在合并之前,多出来的写法会累积展示、摊薄点击率,还会让你自己的报表失真。

出问题时的样子

症状很少是一个错误页面。更常见的是报表里同一篇文章出现两行,一行五十次展示、一行三十五次,而总数哪个都对不上;或者一个被反复请求的页面,缓存命中率只有 4%;或者一条访问规则挡住了 /admin/,而 /admin 照样能打开。

同一种纪律在更小的地方反复出现。token 在 & 处被截断,是因为那个分隔符没有编码 —— 详见哪些字符真的需要编码。Base64 走一趟 URL 之后就坏了,是因为 + 变成了空格 —— 详见 URL 编码与 Base64,以及那套把 + 和 / 换掉的 URL 安全字母表。症状不同,根因只有一个:同一串字节被写成了两种样子。

归一化就是"决定用哪一种,并且让每一层都同意"的纪律。

相关阅读

试用 Base64 转图片工具 →