URL 编解码 / 参数拆解
把一个链接拆成协议、主机、端口、路径、查询参数(中文域名会给出 punycode),或对整段文本做 percent 编码与解码,并指出非法转义的位置。全部在浏览器本地完成。
A · 拆链接
| 组成部分 | 值 |
|---|---|
| 协议 scheme | — |
| 认证段 username:password | — |
| 主机 hostname | — |
| 端口 port | — |
| 路径 pathname | — |
| 查询串 search | — |
| 片段 hash | — |
| 解析后的整串 href | — |
查询参数(键和值都已解码)
| # | 键 | 值 |
|---|
B · 整段文本 percent 编码 / 解码
四组编码好的对照
| 打进去 | 直接编码出来 | 勾上保留结构字符 | 勾上表单口径 |
|---|---|---|---|
a b | a%20b | a%20b | a+b |
a+b | a%2Bb | a+b | a%2Bb |
50% & 50% | 50%25%20%26%2050%25 | 50%25%20&%2050%25 | 50%25+%26+50%25 |
https://例子.com/文件 1 | https%3A%2F%2F%E4%BE%8B%E5%AD%90.com%2F%E6%96%87%E4%BB%B6%201 | https://%E4%BE%8B%E5%AD%90.com/%E6%96%87%E4%BB%B6%201 | https%3A%2F%2F%E4%BE%8B%E5%AD%90.com%2F%E6%96%87%E4%BB%B6+1 |
拆解与编码的口径
两类最容易做错的链接活
链接相关的活有两类特别容易做错:一类是"这条 URL 里哪一段是什么"——参数到底有几个、tag 出现了几次、# 后面那一截服务器看不看得到;另一类是"这串 %E4%B8%AD 到底是什么"——手工编码时既容易漏编(& 忘了转义,参数被从中间切断),也容易多编(把已经编好的一段再编一次,%20 变成 %2520)。
这里分成两段:A 段把一个链接拆成协议、认证段、主机、端口、路径、查询串、片段,并把查询串摊成一张参数表;B 段对任意一段文本做 percent 编码或解码,并且明确写出有多少处被转义、有几处是非法转义。
A 段的拆解口径
解析用的是浏览器自带的 URL 构造器(WHATWG URL 标准),不是我另写一套正则去猜。这一点决定了几个和直觉不符的行为,它们都会如实体现在结果里:
- 中文域名会变成 punycode。
https://例例.中国/a的 hostname 解出来是xn--fsqa.xn--fiqs8s,这是 IDNA 转换的结果,不是解析出错。比对域名、发请求时认这一行;给人看还是用原串。 - 默认端口会被抹掉。
https://a.com:443/p的 port 显示为"未显式写出",因为 443 本来就是 https 的默认端口;http://a.com:8080的 8080 会正常显示。 - 空格和控制字符会被处理。路径里的空格直接变成
%20,首尾空白被去掉,中间的制表符和换行会被删掉。从文档里粘出来的链接先在这儿看一眼,再决定要不要自己编码。 href那一行是规范化后的整串。它和输入不一样就说明链接被"整理"过了:.和..段被折叠、协议被转成小写、\在 http/https 这类协议里被当成/(https:\\a.com会变成https://a.com/)。要知道浏览器最终请求哪个地址,看这一行。- 没写协议会补
https://再解析。判断"有没有协议"只数冒号是不够的:example.com:8080/x/../y直接交给new URL(),它会把example.com当成协议名、把8080/x/../y当成路径,安静地给你一个错结果。所以这里只认"协议 +//“这一种写法,其余的(example.com/a?b=1、//cdn.example.com/x.js)一律补https://,并在提示里明说这次是补过的——补出来的协议和端口都是假设,别当成原链接的事实。mailto:、tel:、data:、javascript:这些本来就不带//的常见协议按原样解析,它们的 hostname 为空是正常的。 - 片段(
#后面)不会发给服务器。它是纯前端的东西。如果你发现某个"参数"在服务器日志里找不到,先看它是不是跑到#后面去了。
查询参数怎么分
问号后面按 & 切段,每段第一个 = 之前是键、之后是值(值里再出现 = 会留在值里)。三个细节经常和直觉不符:
- 没有等号的段算空值,不算无效。
?debug是一个参数,键debug、值空字符串。表格里这类行的键会加粗,并汇总计数。 - 分号不是分隔符。
?a=1;b=2在 WHATWG 里是一个参数,值是1;b=2。工具会提示"含分号”,因为不少老后端(PHP 的旧配置、早年的 Java 容器)确实按分号切——这种链接到底怎么算取决于服务器,不取决于这里怎么猜。 - 同名参数会重复出现。
?tag=1&tag=2是两个参数,逐条列出、不合并。要几个参数看「查询参数数」那一格。
键和值各自再做 percent 解码。默认勾选「把 + 当空格」,这是 application/x-www-form-urlencoded(也就是表单和大多数查询串)的口径;如果这串是静态资源路径那种用 %20 表示空格的场景,取消勾选,+ 就保持字面加号。
B 段的编码口径
编码是一个字符一个字符算出来的:把字符按 UTF-8 展开成字节,每个字节写成 % 加两个大写十六进制位。于是:
- 一个汉字变 9 个字符。它在 UTF-8 里占 3 字节,每字节 3 个字符 →
%E4%B8%AD。emoji 占 4 字节 → 12 个字符,而且它在 JS 里是两个字符位(代理对)。遍历按码点走,不会把代理对劈成两半分半编码。 - 不编码的字符集是
A-Za-z0-9-._~加上!'()*。前者是 RFC 3986 的 unreserved 集;后四个是 JavaScriptencodeURIComponent的历史遗留——它按更早的 RFC 2396 保留了!'()*。这里跟着encodeURIComponent走,是为了让输出结果和你在代码里写的encodeURIComponent(x)逐字一致,否则对不上号更难查。 - 「保留 URL 结构字符」这一勾等于
encodeURI口径::/?#[]@!$&'()*+,;=这 14 个不编码。它适合"整条链接里只想把空格和中文转掉"的场合;不适合编参数值——参数值里的&、=、#必须编掉,不然链接结构就被改写了。默认不勾,即encodeURIComponent口径。 +只在表单口径下代表空格。编码时勾上它,空格输出+;解码时勾上它,+还原成空格。路径里的+从来不是空格,它就是字面加号。
什么时候结果不能直接用
- 解码不抛错,但可能不干净。落单的
%(比如100%纯牛奶)不是错误,工具把它按字面%保留并计数。真正的问题是坏 UTF-8 序列:%C3%28这种两个字节配不上的情况,解码不报错,而是每个坏序列换成一个替换字符(U+FFFD)并计数。所以"解码完成"不等于"输入合法"——计数不为 0 就得回原串查。 - 双重编码要靠你自己判断。
%25E4%25B8%25AD解一次得到%E4%B8%AD,看着"还剩 %,再解一次"。工具解一次就是一次,不猜你要几次。要判断有没有被双重编码,看解码结果里是否还留着完整的%XX形态。 - A 段只管语法不管安全。
https://a.com/../../../../etc/passwd会被规范化成https://a.com/etc/passwd(折叠点段是 URL 规范的一部分),但这不代表这条链接安全——路径穿越是服务器和文件系统的事。端口越界(:99999)之类的非法写法会直接解析失败。 - 参数表只列前 200 个。上千个参数基本是拼接出错了,提示里会写明总数。
- 不解析相对链接。
./a/b?c=1、//cdn.example.com/x.js需要基准地址,请补成绝对地址再来,或者按你复制它的那个站点的上下文来判断。
常见问题
Q:为什么我编码出来的字符串和别的网站不一样?
先看两点。一是空格:别的站点可能输出 +,而这里默认输出 %20(勾上「表单口径」就一致了)。二是 !'()*:很多在线工具按 RFC 3986 把它们也编成 %21%27%28%29%2A,这里跟随 encodeURIComponent 不编。两种在服务端都能正确解回来,但如果你要和某个系统的输出逐字比对(比如做签名),必须固定一个口径。
Q:+ 到底该不该转义?
在查询串里:如果这串是表单编码出来的,+ 表示空格,字面加号该写成 %2B;如果来自现代 JS 的 encodeURIComponent,空格是 %20,+ 就是加号本身。两种写法都是合法 URL,区别只在服务端用哪种解码——这就是 + 被做成勾选而不是自动判断的原因。
Q:粘进来的链接会上传吗?
不会。解析用的是浏览器内的 URL 对象,编码解码都是浏览器端脚本里的循环,你粘的链接不会被送到别处,也就不会离开这台设备。
Q:为什么 A 段点了「拆解」才有结果,B 段不实时算?
链接没打完时中间状态一定是"错的"(https:// 后面还空着),实时报错只会刷屏;而 B 段是转换,不判对错,所以放在按钮上更清楚。A 段你一旦输入过完整链接,改字符时会跟着重算,不用再点。