URL 解析器与解码器,免费
将任意 URL 解析为其组件 · 协议、主机、端口、路径、查询参数和片段。
URL 的剖析:六个组成部分,一段漫长的历史
一个 URL 会被解析成六个概念上的部分:scheme://userinfo@host:port/path?query#fragment。scheme 告诉客户端使用哪种协议(https、http、ftp、mailto、file、data),它是唯一始终存在的部分。userinfo 组件(username:password@)在现代用法中很少见;浏览器通常会从显示的 URL 中把它去掉,因为自 1990 年代以来它一直是一种 phishing 媒介。host 是网络位置,一个注册的域名、一个 IP 地址(IPv4 点分四段或方括号包裹的 IPv6),或者像 localhost 这样的特殊名字。port 是 TCP/UDP 端口(HTTP 默认 80,HTTPS 默认 443,等等);省略时则使用 scheme 的默认端口。path 是用斜杠分隔的层级路径,标识 host 内的资源。query string(? 之后的所有内容)承载以 & 分隔的键值对,用于过滤、分页、跟踪、表单提交。fragment(# 之后的所有内容)是 URL 中唯一从不发送给服务器的部分,它完全在客户端由浏览器处理,用于滚动到特定段落,或者在单页应用中表示路由状态。
查询字符串本身的格式有一个分叉:传统的 ?key=value&key2=value2,值按 RFC 3986 做百分号编码;以及更早的表单编码 application/x-www-form-urlencoded 约定,其中 + 表示空格(最初是为 HTML 表单提交)。多数解析器对两者都能处理,但转换并不对称:%20 始终解码为空格;+ 仅在查询字符串内才解码为空格,在路径里从不。这是现实中最常见的 URL 解析 bug 之一。
URL 简史
URL(最初叫「Universal Document Identifier」,后来叫「Universal Resource Locator」)由 Tim Berners-Lee 在他 1989 年 3 月在 CERN 写下的《Information Management: A Proposal》备忘录(那一份被他的上司 Mike Sendall 批注「Vague but exciting」的)与 1991 年 8 月首批可被公开浏览的网页之间的时间里发明出来。最初的标志性 URL 是 http://info.cern.ch/hypertext/WWW/TheProject.html,于 1991 年 8 月 6 日发布。1992 年的 IETF 讨论将 UDI 改名为 URL,以避开一场词汇之争。RFC 1738(《Uniform Resource Locators》),由 Berners-Lee、Masinter 和 McCahill 撰写,于 1994 年 12 月作为第一份正式的 URL 语法发布。RFC 2396 于 1998 年 8 月跟进,把 URL 推广到更宽泛的 URI 概念中。当前的标志性规范是 RFC 3986(《URI Generic Syntax》),发布于 2005 年 1 月,由 Berners-Lee、Roy Fielding 和 Larry Masinter 编辑,它是 STD 66 Internet Standard,IETF 最高的成熟度等级。每一个 URL 解析器名义上都把 RFC 3986 当作目标。实际上现代浏览器在很多边角情况上偏离了 RFC 3986,这就是为什么 WHATWG 在 url.spec.whatwg.org 上单独维护一份 URL Living Standard 来描述浏览器实际的行为;WHATWG 规范明确以逐步取代 RFC 3986 与 RFC 3987 为目标,两者在末尾空白处理、百分号编码集和 Unicode 归一化等方面仍存在差异。
未保留字符、保留字符与百分号编码
RFC 3986 §2.3 定义了未保留字符,在任何 URI 组件中无需百分号编码就可保证安全的唯一一组字符:A-Z、a-z、0-9、连字符(-)、句点(.)、下划线(_)和波浪号(~)。共 66 个字符。其余的要么是某个组件中具有结构意义的保留字符(gen-delims(:/?#[]@)与 sub-delims(!$&'()*+,;=))要么属于「其他」,出现在 URI 中就必须做百分号编码。百分号编码(RFC 3986 §2.1)取一个字符的字节序列(默认是 UTF-8,除非 scheme 另有规定),把每一个字节替换为 %HH,其中 HH 是该字节的两位十六进制值。所以一个 UTF-8 编码的 é(字节 0xC3 0xA9)变成 %C3%A9;俄语单词 привет 变成 %D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82,每个字符 2 个字节,六个 %XX 三元组,六个西里尔字母对应 36 个百分号编码字符。
浏览器以两种方式显示百分号编码的路径:大多数现代浏览器(Chrome、Firefox、Safari)在编码是有效 UTF-8 时,会在地址栏中解码并渲染原本的 Unicode 字形,但在用户复制 URL 时,复制的是字面意义上的百分号编码形式。较老的浏览器与许多 Web 日志只显示百分号编码形式,这就是为什么「漂亮的 Unicode URL」可能具有迷惑性:它们在地址栏里看起来很美,在它们被分享到的任何文本中又显得很丑。RFC 3987(《Internationalized Resource Identifiers》,IRI)于 2005 年 1 月发布,把 Unicode URL 在未编码形式下规范化;Punycode(RFC 3492,2003 年 3 月)定义了国际化域名按标签逐一编码为供 DNS 使用的 ASCII 的方式:中文顶级标签 中国 变为 xn--fiqs8s,因此 example.中国 在 DNS 层解析为 example.xn--fiqs8s。最经典的演示是维基百科的 IRI URL:https://ja.wikipedia.org/wiki/東京 在任何现代浏览器中都能工作,尽管底层请求把路径编码成 /wiki/%E6%9D%B1%E4%BA%AC。
WHATWG URL 标准,浏览器实际在做的事
IETF 的 RFC 3986 说一回事;浏览器做的事略有不同。WHATWG(浏览器厂商组成的标准化机构)在 url.spec.whatwg.org 单独维护一份 URL Living Standard,描述了浏览器实际运行的算法状态机,包括对前导空白、控制字符、按组件而异的百分号编码集合,以及 Unicode 归一化的处理。WHATWG 规范是浏览器 URL 构造函数(new URL(input))所实现的内容,Node.js、Deno 与 Bun 的内置 URL 解析也都向其收敛。Ada URL 解析器(由 Yagiz Nizipli、Daniel Lemire 等人用 C++ 编写)成为符合 WHATWG 的解析器,自 Node.js 18.16.0(2023 年 4 月)起为 Node.js URL 解析提供支持,取代了较早的 url.parse() 路径;它在性能上可被测量地超过此前的每一种实现,在 2026 年是高性能 URL 解析事实上的标准。RFC 3986 与 WHATWG 规范至今仍未完全调和,历史性的分歧仍会出现在遗留代码路径与较旧的运行时版本里。
查询字符串,以及 URLSearchParams API
查询字符串在严格意义上不过是「? 之后、# 之前的所有内容」,规范并没有真正定义如何解释它。?key=value&key=value 加 & 分隔符这套约定只是约定,不是要求。在实践中,有两种查询字符串格式占主导:application/x-www-form-urlencoded(HTML 表单提交的默认格式,其中 + 表示空格)与标准 URI 查询约定(其中空格始终是 %20)。浏览器的 URLSearchParams API(WHATWG URL Living Standard 的一部分)在解析时透明处理两种格式,在转换为字符串时输出表单编码版本。重复键是合法的:?tag=red&tag=blue&tag=green 是有效的,URLSearchParams.getAll('tag') 返回 ['red', 'blue', 'green']。不同的 Web 框架处理重复键的方式不同:Rails 与 Express 把重复键收集为数组,而 PHP 在键不使用 name[] 方括号约定时会用后值覆盖前值;这是 API 集成中跨框架 bug 的长期来源。
常见的 URL 解析坑
- 双重编码。对一个已经编码过的 URL 再编码会产生
%2520(%自己被编码成%25)。这种情况会出现在 URL 经过多层(前端 → 后端 → 分析)的情形,每一层都「贴心地」再编码一次。修复办法是先一路解码到底,再只编码一次。 - 查询字符串中的 + 与 %20。在表单编码的查询字符串里
+表示空格,但在路径或 fragment 中+就是字面意义的+。把这两种约定混着用会产生很难调试的 bug:在查询里「John+Doe」变成 「John Doe」,但在路径里仍然是「John+Doe」。 - 大小写敏感。host 是大小写不敏感的(
EXAMPLE.com与example.com是同一个 host);path 在大多数服务器上(Linux/Unix)是大小写敏感的,但在另一些服务器上(默认的 Windows IIS、macOS 的 HFS+)是大小写不敏感的。这意味着同一个 URL 在不同服务器上可能解析到不同的内容。 - URL 中的 IPv6。IPv6 地址中含有冒号,会与 host:port 分隔符冲突。修复办法是把 IPv6 地址放在方括号里:
http://[2001:db8::1]:8080/path。许多 URL 解析器历史上在这一点上都失败过;现代浏览器与 WHATWG 解析器都能正确处理。 - OAuth fragment 令牌。OAuth 2.0 的 implicit grant 流程会把 access token 放在 URL fragment 里返回(
#access_token=...),这样它们就不会出现在服务器日志里。现代 OAuth 指南建议改用带 PKCE 的 authorization code,但遗留系统仍会输出 fragment 令牌。 - 往返非恒等。解析一个 URL 再把它转回字符串并不总是产生原始字符串,解析器会做归一化(对百分号编码的未保留字符做解码,把 host 转成小写,在某些实现里还会对查询参数排序)。不要假设
parse(url).toString() === url。
常见用法
- 调试 API 请求。带着长查询字符串的 REST 端点很难读;把它解析出来后,每个参数都会单独占一行。
- 检查 OAuth 回调。认证流程的 URL 携带着编码过的 state、code、scope 与 access token,需要解码以便调试,而不必把它们暴露给某个服务器。
- 追踪重定向链路。当一个 URL 经过多个中间 URL 重定向时,逐个解析有助于跟住链路、定位某个重定向是在哪一步坏掉的。
- UTM 标签审计。分析 URL(
?utm_source=...&utm_medium=...&utm_campaign=...)逐参数看比当成一堵查询字符串墙看要容易得多。 - 安全审计。查找 URL 参数里是否存在 SQL 注入模式或路径穿越序列;解析后每个值都被分别暴露出来供审查。
- 表单编码 body 解析。URL 查询字符串里使用的同一种格式,也用于
application/x-www-form-urlencoded的 POST body。 - 检查深度链接。移动应用的深度链接与 Web 应用的路由会把复杂状态编码进 path 或 query;解析能让结构可见。
- 审视 affiliate / 分享链接。邮件营销或联盟项目里的跟踪链接携带着编码后的重定向 URL 与跟踪 ID,把它们解码出来很有帮助。
隐私:URL 里其实藏着真正的秘密
URL 一般不被当作机密看待,但它们经常承载真正机密的数据。OAuth 回调 URL 包含 access token。Magic-link 登录 URL 包含一次性认证 token。密码重置链接包含 reset token。内部 API URL 包含暴露基础设施的内部主机名与路由路径。即使是普通应用 URL,也通过查询参数泄露用户行为,搜索词、筛选选项、profile ID、会话标识。Referer 头会把上一个 URL 泄露给每一个被链接到的站点,这一点由 2017 年作为 W3C Candidate Recommendation 引入的 Referrer-Policy 头部加以缓解(各浏览器的默认值仍有差异)。URL 最终会出现在服务器访问日志里、浏览器历史里、浏览器书签里、CDN 日志里、分析管道里、聊天应用的链接预览里。一个服务器端的 URL 解析器能看到每一个粘贴进来的 URL;一个仅在浏览器中运行的解析器看不到。对于内部 API URL、带 token 的 OAuth 回调、密码重置链接,或任何你不想复制到陌生人硬盘上的 URL,仅在浏览器中运行的解析器才是正确的架构。在解析时打开 DevTools 的 Network 面板自行验证,或者在页面加载完成后让它离线(飞行模式)再用。
常见问题
一个 URL 包含哪些部分?
六个概念部分:scheme(https、http、ftp、mailto)、userinfo(在现代用法里很少见,大多被浏览器作为 phishing 缓解策略剥离)、host(域名或 IP)、port(HTTP 默认 80,HTTPS 默认 443)、path(以斜杠分隔的层级)、query(? 之后的键值对)和 fragment(# 之后,从不发送给服务器)。完整文法见 RFC 3986 §3(2005 年 1 月,STD 66)与 WHATWG URL Living Standard。
我怎么解码 URL 编码的字符?
百分号编码用 % 加上字节的十六进制代码替换不安全字符:空格是 %20,冒号是 %3A,正斜杠是 %2F,与号是 %26,@ 符号是 %40。UTF-8 多字节字符按字节逐个编码,所以 é 变成 %C3%A9(两个字节)。解析器会自动把所有百分号编码的字符在显示结果中解码出来。标准的 JavaScript 函数是用于编码单个值的 encodeURIComponent() 与用于解码的 decodeURIComponent()。
什么是 URL fragment?
fragment(# 之后的所有内容)是 URL 中唯一完全在客户端处理的部分,它在 HTTP 请求里从不发送给 Web 服务器。最初的用途:把浏览器滚动到带有该 ID 的锚元素。现代用法包括单页应用的路由状态(#/dashboard/profile)、OAuth implicit 流的令牌(如今已不再推荐,改用带 PKCE 的 authorization code)以及 PDF 翻页(file.pdf#page=5)。因为 fragment 不会到达服务器,所以它是用来藏那些不该出现在服务器日志里的值的好地方。
为什么 + 有时是空格,有时又是 +?
存在两种编码约定。application/x-www-form-urlencoded(HTML 表单提交的默认格式)把空格编码为 +;标准百分号编码(按 RFC 3986)把空格编码为%20。两者在查询字符串里都有效;在路径与fragment 里只有 %20 是有效的。URLSearchParams 透明地处理两者。跨上下文 bug 出现在以下情况:代码用 encodeURIComponent(它把空格编码为 %20)处理某些查询参数,而服务器期望的是表单编码,反之亦然。
它能处理相对 URL 吗?
解析器期望一个带 scheme 的完整 URL。对于 /api/users 这种相对路径,在前面拼上一个基础 URL(https://example.com/api/users)再去解析。一些相对 URL 解析(像浏览器为 href 属性那样基于一个 base URL 解析)在路线图上,WHATWG URL 构造函数的双参数形式(new URL(relative, base))可以处理这一点,生产代码应当使用它。
我的 URL 会被发送到任何地方吗?
不会。解析完全通过 WHATWG URL 构造函数在你的浏览器中运行,你粘贴进来的 URL 永远不会离开你的设备。在你点击 Parse 时打开 DevTools 的 Network 面板自行验证,或者在页面加载完成后让它离线(飞行模式)。对于带 access token 的 OAuth 回调 URL、含一次性令牌的密码重置链接、暴露基础设施的内部 API URL,或者任何你不想复制到陌生人硬盘上的 URL,这都是安全的。