URL 编码器 / 解码器

按规范做百分号编码:RFC 3986 的保留字符与非保留字符

URI 标准 RFC 3986 把字符精确地分成两个有意义的集合。非保留集合 — 字母、数字,加上四个符号 - . _ ~ — 可以不经编码出现在 URL 的任何位置;对它们编码合法但毫无意义:%41 和 A 标识同一个东西,URL 归一化器还应把它们解码回来。设计的精髓在保留集合:gen-delims 的 : / ? # [ ] @ 标记 URL 各组件之间的边界,sub-delims 的 ! $ & ' ( ) * + , ; = 在组件内部承载含义。其余一切 — 空格、引号、尖括号、所有非 ASCII 字符 — 根本不能以原样出现,必须按字节百分号编码为 %HH。 关键的微妙之处在于:对保留字符而言,编码形式与原始形式并不等价。原始的 / 分隔路径段;编码后的 %2F 只是长得像斜杠的数据。因此一个字符"是否需要编码"完全取决于它所在的组件:/ 在路径里没问题,但在恰好包含另一个 URL 的查询参数值里必须变成 %2F;? 第一次出现时是结构,之后就只是普通数据。 这也解释了 URL 构造的头号实务规则:永远不要对拼装完成的整个 URL 做编码,因为那根本无法做对 — 一趟处理要么漏掉数据字符,要么摧毁结构字符。先分别编码每个动态值,再拼装。

encodeURIComponent、encodeURI 与 URLSearchParams

JavaScript 内置了三代编码 API,选错是前端代码里最常见的 URL 缺陷。encodeURI 把输入当作完整 URL:它转义那些永远不能出现的字符(空格、引号、非 ASCII),但保留所有结构分隔符 — : / ? # & = 原样通过。这让它对编码"值"毫无用处:喂给它一个含 & 的搜索词,& 会存活下来,把你的参数劈成两半。encodeURIComponent 才是值编码器:除了非保留字符和 ! ' ( ) * 之外全部转义,于是 &、=、/、? 统统变安全。经验法则很简单:你几乎总是需要 encodeURIComponent,并在拼进 URL 之前对每个值分别调用。 两个函数都不是完美的 RFC 3986 公民。encodeURIComponent 把 RFC 归为 sub-delims 的 ! ' ( ) * 留作原样;OAuth 1.0 签名等要求字节级精确的协议,必须再用一趟 replace 补上这五个字符。远古的 escape() 函数根本不该再用 — 它诞生于 UTF-8 普及之前,会毁掉非拉丁文本。而 decodeURIComponent 遇到畸形序列(孤立的 %、%ZZ)会抛出 URIError,所以解码不可信输入要放进 try/catch。 现代答案是彻底跳过手动编码:URL 和 URLSearchParams 类按组件解析、构建、序列化 URL,并自动以正确规则编码每一部分。
const q = 'fish & chips?';

encodeURI(q)             // 'fish%20&%20chips?'   & and ? survive!
encodeURIComponent(q)    // 'fish%20%26%20chips%3F'  safe as a value

// The failure mode:
'https://x.io/s?q=' + encodeURI(q)
// → ?q=fish%20&%20chips?   server sees TWO params: q and ' chips?'

// The modern way — encoding handled per component:
const u = new URL('https://x.io/s');
u.searchParams.set('q', q);
u.toString()             // 'https://x.io/s?q=fish+%26+chips%3F'

+ 与 %20:一个空格,两套标准

空格有两种编码,因为两份不同的规范都认领了它。RFC 3986 的百分号编码规定:空格在任何地方都是 %20。但 HTML 表单早于这条规则:application/x-www-form-urlencoded 格式在 1990 年代初为表单提交而定义,至今仍由 WHATWG URL 标准规定,它把空格编码为 +(把字面加号编码为 %2B)。两种编码在各自语境里都是正确的:表单提交和 URLSearchParams 生成的查询串用 +;路径以及一切用 encodeURIComponent 编码的内容用 %20。 缺陷来自语境交叉。经典受害者是合法含有加号的数据:国际格式电话号码(+8210...)和 base64 字符串。不加编码地发送 phone=+82101234567,服务器的表单解码器会把 + 变成空格,得到 ' 82101234567' — 这种污染常常一路活到数据库里才被发现。加号必须以 %2B 发送。反方向上,JavaScript 的 decodeURIComponent 对表单编码一无所知:它把 + 留作字面加号,用它去解码表单编码的查询串,所有空格都会悄然丢失。URLSearchParams 则能正确解码 +。 各语言库把这种分裂刻进了 API:PHP 有 urlencode(表单风格,+)和 rawurlencode(RFC 3986,%20);Python 的 urllib.parse.quote 用 %20,quote_plus 用 +;Java 那个名字误导人的 URLEncoder 是表单编码器,输出 + — 拿它来构造路径是众所周知的错误。拿不准时,输出 %20 更安全:表单解码器两种都接受,路径解析器只认 %20。
// The international phone number bug
fetch('/api/sms?to=+821012345678')
// server-side form decoding: '+' → ' '
// to = ' 821012345678'        ← corrupted, silently

fetch('/api/sms?to=%2B821012345678')
// to = '+821012345678'        ← correct

// Decoding mismatch:
decodeURIComponent('a+b%20c')            // 'a+b c'  (+ kept)
new URLSearchParams('x=a+b%20c').get('x') // 'a b c'  (+ = space)

双重编码:%2520 缺陷及其安全孪生兄弟

百分号编码不是幂等的:跑两遍会得到不同的输出,因为 % 字符本身会被编码成 %25。把空格编码一次得到 %20;把结果再编码一次,% 变成 %25,得到 %2520。当用户最终在页面或邮件主题里看到 "New%20York",说明管线中某处把值编码了两次、却只解码了一次。常见嫌疑人:手写代码先对值调用了 encodeURIComponent,然后框架、HTTP 客户端或模板引擎又编码了一遍 — 或者重定向链上的每一跳都对已编码的 next-URL 参数重新编码。 镜像缺陷更危险。只要任何一层解码超过一次,%2520 就塌缩成 %20,再塌缩成真正的空格 — 攻击者恰恰把这一点武器化。检查路径中 ../ 序列的安全过滤器可以被 %252e%252e%252f 绕过:过滤器检查解码一次后的字符串,看到人畜无害的 %2e%2e%2f 便放行;后面的组件再解码一次,目录穿越就复活了。代理、应用服务器、框架中都有这种形态的真实 CVE,因此安全准则说得很直白:在信任边界恰好解码一次,然后校验解码后的值,校验之后绝不再解码。 同时防住两个方向的工程纪律是"所有权":恰好一层拥有编码权,在 URL 离开你的代码前的最后一刻执行;恰好一层拥有解码权,在请求进入的那一刻执行。如果你在一个本不该嵌套的 URL 里看到 %25 后面跟两位十六进制数,几乎可以断定这是双重编码缺陷 — 逐步解码,数一数要几遍才回到明文。
'New York'
→ encode once:  'New%20York'      correct
→ encode twice: 'New%2520York'    bug: % became %25

// Path traversal hidden by double encoding:
'../etc/passwd'
→ once:  '%2E%2E%2Fetc%2Fpasswd'   filter catches %2E%2E%2F
→ twice: '%252E%252E%252Fetc...'   filter sees nothing;
                                   second decode revives ../

// Diagnosis: decode until stable, count the passes
decodeURIComponent('New%2520York') // 'New%20York'  (pass 1)
decodeURIComponent('New%20York')   // 'New York'    (pass 2 → double-encoded)

URL 中的非 ASCII:UTF-8 转义、乱码与 Punycode

百分号编码作用于字节而非字符,所以非 ASCII 字符在编码前必须先序列化成字节 — 而选哪种字节编码,正是历史留下烂摊子的地方。现代规则由 RFC 3986 阐明、由浏览器实现的 WHATWG URL 标准强制执行:用 UTF-8。韩文音节"한"变成三个字节 ED 95 9C,于是编码为 %ED%95%9C;一个 emoji 可能膨胀成十几个百分号转义。浏览器对用户隐藏这一切 — 地址栏显示解码后的 Unicode,而线上请求携带的是转义序列。 乱码(mojibake)发生在两端对字节序列化意见不一时。用 EUC-KR 或 Latin-1 字节编码、却按 UTF-8 解码的值,会产生垃圾字符或直接解码报错;这在 UTF-8 普及前的网络上是流行病,如今仍会在遗留系统和默认 URI 字符集为 ISO-8859-1 的老 Java Servlet 容器里冒头。更隐蔽的变种是 Unicode 归一化:café 可以序列化为一个预组合码点(NFC),也可以是 e 加组合重音(NFD),两种形式百分号编码出不同的字节序列,字符串比较时无法匹配。编码前先归一化为 NFC。 主机名遵循完全不同的规则手册:host 组件里根本不允许百分号编码。国际化域名走的是 IDNA,把 Unicode 标签转换为 ASCII 兼容的 Punycode 形式 — 한글.example 变成 xn--bj0bj06e.example。证书或日志里的 xn-- 前缀不是数据损坏,而是 Unicode 域名的 DNS 安全拼写;也正因近似字符钓鱼(IDN 同形异义攻击)的存在,浏览器会把可疑的混合文字主机名以 Punycode 形式显示。
最后更新:

关于此工具

URL 编码器把 URL 中不安全的字符(空格、&、=、?、/ 与非 ASCII 等)转为 %xx 百分号编码。不编码会破坏 URL 结构。Web 框架通常自动处理,但用字符串拼接 URL 时仍需手动处理。

使用方法

  1. Choose Encode to escape characters, or Decode to recover the original.
  2. Paste the URL or query parameter into the input box.
  3. The output updates as you type — no submit button.
  4. Copy the encoded string into your URL builder, fetch call, or query string.
  5. When in doubt, encode each query parameter individually rather than the whole URL.

常见用例

  • Building a search URL where the query contains spaces or special characters.
  • Generating a redirect URL that passes another URL as a parameter.
  • Constructing a deep link with non-ASCII text (Korean, Japanese, emoji) in the path.
  • Decoding a webhook payload where parameters are URL-encoded twice.
  • Sanity-checking that a third-party callback URL is properly escaped.
  • Encoding a base64 string before placing it in a URL — the + and / are unsafe.

常见问题

Q. What is the difference between encodeURI and encodeURIComponent?

A. encodeURI keeps reserved URL characters (: / ? & =) intact — use it for whole URLs. encodeURIComponent escapes them too — use it for individual parameter values.

Q. Why is a space sometimes encoded as + and other times as %20?

A. + is the legacy x-www-form-urlencoded form (still used in query strings), %20 is the standard percent-encoding used in URL paths. Servers normally accept either in query strings.

Q. Do I need to encode non-ASCII characters?

A. Yes, for compatibility. Modern browsers display Unicode in the URL bar, but the underlying request encodes them as UTF-8 percent-escapes.

Q. Why does decoding produce strange characters?

A. Usually a charset mismatch — the URL was encoded with one encoding (e.g., latin-1) but decoded as another (UTF-8). Fix the source if you can.