跳到主内容
d.devtul.fun
EN
HTTP · 2026-08-30

URL 编码常见示例:一份开发者随手查的对照表

一份可以调试时一直开着的参考:同一个字符在三种编码器下分别长什么样。把这张表记熟,能省下十几回"我的参数为什么不对"的排查时间;而记住那几行在不同语言里结果不一样的字符,能帮你避开更隐蔽的一类 bug——同一段代码在服务端和浏览器里产出不同字符串。

三种语境

看表之前,先把这三种情况在脑子里分清楚。它们不是同一个工具,产出也不是同一个字符串:

  • encodeURIComponent——在 JavaScript 里编码单个值。它假设输入是数据,所以把所有保留字符都转义掉。
  • encodeURI——编码一个已经完整的 URL。它放过了结构性字符,让 URL 仍然可以跳转。
  • 表单提交(application/x-www-form-urlencoded,也就是 URLSearchParams 和 HTML 表单产出的格式)——三者里唯一会把空格变成字面 + 的。

前两个是 JavaScript 函数,第三个是线格式(wire format)。把它们混为一谈,就是下面大多数意外的根源。

完整对照表

这是值得收藏的表。按列读:同一个字符,仅仅因为被哪个编码器碰过,就可能变出不同的样子。

字符encodeURIComponentencodeURI表单提交说明
空格%20%20+表单用 +,其余用 %20
!%21!%21encodeURI 保留感叹号
"%22%22%22双引号
#%23#%23片段分隔符
$%24$%24encodeURI 保留
%%25%25%25百分号自身必须编码
&%26%26%26查询分隔符
'%27'%27单引号
( )%28 %29( )%28 %29encodeURI 保留括号
*%2A*%2AencodeURI 保留星号
+%2B+%2B字面加号必须编码
,%2C,%2CencodeURI 保留逗号
/%2F/%2F路径分隔符
:%3A:%3A协议分隔符
;%3B;%3BencodeURI 保留分号
=%3D%3D%3D键值分隔符
?%3F?%3F查询起始符
@%40@%40用户分隔符
[ ]%5B %5D%5B %5D%5B %5DIPv6 字面量用
~~~%7E非保留,但表单仍编码
你好%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BDUTF-8 六字节
😀%F0%9F%98%80%F0%9F%98%80%F0%9F%98%80四字节 emoji
换行%0A%0A%0A控制字符,必须编码
Tab%09%09%09控制字符

最让人意外的那几行是 ?、#、/、@、:,以及 ! $ ( ) * , ; 这些标点。encodeURI 放它们走,因为它们是结构性的;encodeURIComponent 不放,因为它默认输入是个值,而不是一个 URL。~ 那行是个安静的坑:它属于非保留集合,所以两个 JS 函数都原样放过,但表单编码仍然把它变成 %7E。

为什么空格那行不一样

空格在 URI 语法里属于不安全字符,所以两个 JS 函数都输出 %20。但表单媒体类型出现得比现代 URI 规范更早,它选了 + 来表示空格,好让查询串更可读。服务端的职责是解码表单格式,在那种格式里 + 就是空格。如果你用 encodeURIComponent 手写查询串,得到的是 %20,而符合标准的表单解码器仍然会把它读回空格——所以这两者通常能兼容。错配只发生在:一方期望表单的 + 约定,另一方不期望。

加号陷阱:a+b 与 a%2Bb

最常被报告的一个编码 bug 就是加号。在表单解码的查询串里,a+b 表示"a 空格 b"。只有 a%2Bb 表示"a 加 b"。如果你的数据里可能含有字面意义的加号,就必须把它编码掉,否则服务端会静默地把加号变成空格。

// 服务端收到 q="a b"  (加号被当成空格)
?q=a+b
// 服务端收到 q="a+b"  (加号被保留)
?q=a%2Bb

这个坑在 base64 和 URL 安全的令牌上咬人最狠:一个像 ab+cD/ef== 的 base64 值,过一遍服务端的表单解码,就变成了 ab cD/ef==,令牌就此校验失败。一定要用 encodeURIComponent 编码令牌(或者干脆用 - 和 _ 的 URL 安全 base64 字母表)。

不同语言里的编码差异

同一个逻辑操作,在不同语言里产出不同结果,而这正是跨系统 bug 藏身之处。下面几个值得背下来:

# Python:quote 保留斜杠,quote_plus 把斜杠变成 %2F 且用 +
from urllib.parse import quote, quote_plus
quote('a/b c')        # 'a/b%20c'
quote_plus('a/b c')   # 'a%2Fb+c'

// Go:QueryEscape 是表单风格,空格变成 +
import "net/url"
url.QueryEscape("a b")   // "a+b"

// Java:URLEncoder 是表单风格,空格变 +,斜杠变 %2F
URLEncoder.encode("a/b c", "UTF-8")   // "a%2Fb%2Bc"

# Ruby:URI.encode_www_form_component 是表单风格(空格 -> +)
require 'uri'
URI.encode_www_form_component('a b')   # "a+b"

结论:在 Python、Go、Java、Ruby 里,"默认"编码器都是表单编码器,产出 + 并且会动斜杠。JavaScript 的 encodeURIComponent 是个异类,它产出 %20,而且只有当你拿它去编码整条路径时才会把斜杠保留成 %2F。当你的前端用 encodeURIComponent 编码、后端用表单解码器解码时,一个错配就已经在等你了。

双重编码事故

这是典型的"能跑但值不对" bug。一层编码完,另一层又把已经编码过的字符串再编码一次。一个被正确编成 %20 的空格,变成了 %2520。服务端解码一次,看到的是字面字符串 %20,把它当作数据——于是你想要的空格没了,用户屏幕上看到的是 "%20"。

// 前端已经编码过值
const sent = 'q=' + encodeURIComponent('a b');   // "q=a%20b"

// 错误:某个代理或框架在发送前又编码了一次
const double = encodeURIComponent(sent);          // "q%3Da%2520b"
// 服务端解码一次:q=a%20b  -> 值变成了字面的 "a%20b",而不是 "a b"

修复办法是纪律:只在恰好一层做编码。如果你的框架或 HTTP 客户端已经编码了参数,就别在应用代码里再编码一遍。如果必须预编码,就把客户端的自动编码关掉。

"该编码却没编码"的事故

镜像的另一面:一个带有保留字符的值,被人当成"不过就是个字符串"裸丢进了 URL,因为以为不用编码。解析器随后在错误的分隔符处把值切开了。

// 错误:值里的 & 被当成了新的查询参数
const name = 'Tom & Jerry';
const url = 'https://example.com/search?q=' + name;
// 结果:https://example.com/search?q=Tom & Jerry
// 服务端解析出两个参数:q="Tom " 和 Jerry=""(Jerry 现在成键了!)

// 正确:编码值
const url2 = 'https://example.com/search?q=' + encodeURIComponent(name);
// https://example.com/search?q=Tom%20%26%20Jerry

同样的隐患也适用于 #(它后面的内容被丢进片段)、?(开启第二段查询)、/(开启一段路径)。任何这些字符出现在未编码的用户输入里,都会重塑整个 URL。

编码非 ASCII 与 emoji

百分号编码针对的是字节,不是字符。非 ASCII 文本先被转成 UTF-8,再对每个字节做 %XX 三连码。这就是为什么 你好 是 %E4%BD%A0%E5%A5%BD(六个字节、九个可见字符),而那个咧嘴 emoji 😀 是 %F0%9F%98%80(四个字节)。同一个字符只有在源用了不同字符集时才会编出不同结果——所以你应该在所有地方钉死 UTF-8,绝对别让框架回退到 Latin-1。

百分号自身与残缺序列

一个后面没有跟着两个十六进制数字的孤零零 % 是非法的。如果你拼接的字符串里本来就含有百分号(日志、哈希、像 50% 这种格式化数字)却没编码,就会产出一个严格解码器会拒绝的序列。永远把 % 当成数据,在它进 URL 之前编码成 %25。

实战场景:拼一个安全的搜索 URL

把这些串起来。用户输入一段自由文本查询,里面可能有空格、&、中文和 emoji。正确的构造方式只编码"值"那几块:

function buildSearch(term, page) {
  const p = new URLSearchParams();
  p.set('q', term);          // URLSearchParams 会编码每个值
  p.set('page', String(page));
  return 'https://example.com/search?' + p.toString();
}
// term = "C++ & 你好"  ->  "...?q=C%2B%2B+%26+%E4%BD%A0%E5%A5%BD&page=2"
// 注意:空格变成了 +(表单风格),+ 和 & 被编码,中文是 UTF-8 字节

如果服务端期望严格的 %20 而不是 +,就把 URLSearchParams 换成手写的 encodeURIComponent 拼接。

什么时候该用工具

如果你正盯着一长串编码后的文本、想搞清楚它到底说了什么,直接粘进 URL 编码工具 解码就行。除了很短的值,手写编码正是错误溜进来的地方;而解码器是确认一个字符串究竟包含什么的最快办法。

小结

一个字符的编码结果,不是关于这个字符的事实,而是关于"哪个编码器碰过它"的事实。把三种语境分开,把表收藏好,并记住 + 只在表单编码里才表示空格。每个值只编码恰好一次,而当服务端和客户端对不上时,上面那张表就是你想明白"为什么"的地方。

继续阅读