URL 编解码 / 参数拆解

把一个链接拆成协议、主机、端口、路径、查询参数(中文域名会给出 punycode),或对整段文本做 percent 编码与解码,并指出非法转义的位置。全部在浏览器本地完成。

A · 拆链接

查询参数(键和值都已解码)

B · 整段文本 percent 编码 / 解码

四组编码好的对照

打进去直接编码出来勾上保留结构字符勾上表单口径
a ba%20ba%20ba+b
a+ba%2Bba+ba%2Bb
50% & 50%50%25%20%26%2050%2550%25%20&%2050%2550%25+%26+50%25
https://例子.com/文件 1https%3A%2F%2F%E4%BE%8B%E5%AD%90.com%2F%E6%96%87%E4%BB%B6%201https://%E4%BE%8B%E5%AD%90.com/%E6%96%87%E4%BB%B6%201https%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 为空是正常的。
  • 片段(# 后面)不会发给服务器。它是纯前端的东西。如果你发现某个"参数"在服务器日志里找不到,先看它是不是跑到 # 后面去了。

查询参数怎么分

问号后面按 & 切段,每段第一个 = 之前是键、之后是值(值里再出现 = 会留在值里)。三个细节经常和直觉不符:

  1. 没有等号的段算空值,不算无效。?debug 是一个参数,键 debug、值空字符串。表格里这类行的键会加粗,并汇总计数。
  2. 分号不是分隔符。?a=1;b=2 在 WHATWG 里是一个参数,值是 1;b=2。工具会提示"含分号”,因为不少老后端(PHP 的旧配置、早年的 Java 容器)确实按分号切——这种链接到底怎么算取决于服务器,不取决于这里怎么猜。
  3. 同名参数会重复出现。?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 集;后四个是 JavaScript encodeURIComponent 的历史遗留——它按更早的 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 段你一旦输入过完整链接,改字符时会跟着重算,不用再点。

微信
微信
支付宝
支付宝