URL 编码指南:百分号编码、RFC 3986 与常见错误
了解 URL 编码的工作原理、哪些字符需要百分号编码,以及常见陷阱。涵盖 RFC 3986 规范、encodeURIComponent 与 encodeURI 的区别、重复编码问题,以及七种编程语言的编码示例。
URL 编码指南:百分号编码、RFC 3986 与常见错误
如果你曾在网址中看到 %20,或者疑惑为何在查询参数中加入 & 后 API 请求就失败了,那你已经遇到了 URL 编码。URL 编码也称为百分号编码,是一种让特殊字符能够安全出现在 URL 中而不破坏 Web 功能的机制。
本指南涵盖开发者需要了解的 URL 编码全貌:哪些字符需要编码、算法如何运作、会破坏应用的常见错误,以及七种编程语言中的编码函数。欢迎使用我们的免费 URL 编码与解码工具 亲身体验百分号编码。
什么是 URL 编码?
URL 编码(在 RFC 3986 中正式称为百分号编码)是将字符转换为可安全嵌入 URL 的格式的过程。在 URL 语法中具有特殊含义的字符,或超出允许 ASCII 范围的字符,会被替换为 % 符号加上对应的两位十六进制值。
举例如下:
- 空格 →
%20 - 与号
&→%26 - 等号
=→%3D - 斜杠
/→%2F - 欧元符号
€→%E2%82%AC(三个 UTF-8 字节)
为什么需要 URL 编码
URL 的严格语法定义于 RFC 3986。某些字符充当结构分隔符:
https://example.com:8080/path/to/page?key=value&other=data#section
___ ___________ ____ ___________ _____________________ _______
scheme host port path query string fragment
每个分隔符字符都有明确的作用:
| 字符 | URL 中的作用 | 未编码时的后果 |
|---|---|---|
: | 分隔协议与主机、主机与端口 | key=time:30 会使解析器混乱 |
/ | 分隔路径段 | /search/cats/dogs 看起来像 3 个路径段 |
? | 开始查询字符串 | ?query=what? 含义不明确 |
# | 开始片段标识符 | key=C# 会在 # 处截断 |
& | 分隔查询参数 | name=Tom&Jerry 会被解析为 2 个参数 |
= | 分隔键与值 | equation=2+2=4 含有两个等号 |
@ | 分隔用户信息与主机 | email=user@host 看起来像认证信息 |
+ | 表单编码中的空格 | 含义不明确:加号还是空格? |
当这些字符作为数据而非分隔符出现时,必须进行百分号编码。
哪些字符需要编码?
RFC 3986 将字符分为三类:
非保留字符(永远不需要编码)
这些字符可以在 URL 中的任意位置出现,无需编码:
A-Z a-z 0-9 - _ . ~
共计 66 个字符。
保留字符(作为数据时需编码)
这些字符在 URL 语法中具有特殊含义,当作为数据值使用时必须编码:
| 字符 | 十六进制代码 | 作用 |
|---|---|---|
: | %3A | 协议/端口分隔符 |
/ | %2F | 路径分隔符 |
? | %3F | 查询字符串起始 |
# | %23 | 片段起始 |
[ | %5B | IPv6 括号 |
] | %5D | IPv6 括号 |
@ | %40 | 用户信息分隔符 |
! | %21 | 子分隔符 |
$ | %24 | 子分隔符 |
& | %26 | 参数分隔符 |
' | %27 | 子分隔符 |
( | %28 | 子分隔符 |
) | %29 | 子分隔符 |
* | %2A | 子分隔符 |
+ | %2B | 子分隔符 / 空格 |
, | %2C | 子分隔符 |
; | %3B | 子分隔符 |
= | %3D | 键值分隔符 |
其他所有字符(始终编码)
所有其他字符——包括空格、非 ASCII 字符和控制字符——必须始终编码:
| 字符 | 编码 | 备注 |
|---|---|---|
| 空格 | %20 | 最常见的编码 |
" | %22 | 双引号 |
< | %3C | 小于号 |
> | %3E | 大于号 |
{ | %7B | 左花括号 |
} | %7D | 右花括号 |
| ` | ` | %7C |
\ | %5C | 反斜杠 |
^ | %5E | 脱字符 |
` | %60 | 反引号 |
| 非 ASCII | 多字节 | UTF-8 编码后再百分号编码 |
百分号编码的工作原理
编码算法非常直观:
- 判断字符是否需要编码(不属于非保留字符,或是作为数据使用的保留字符)
- 将字符转换为 UTF-8 字节序列
- 将每个字节表示为
%XX,其中 XX 是对应的十六进制值
示例:编码「café」
| 字符 | UTF-8 字节 | 百分号编码 |
|---|---|---|
| c | 63 | c(非保留字符) |
| a | 61 | a(非保留字符) |
| f | 66 | f(非保留字符) |
| é | C3 A9 | %C3%A9 |
结果:caf%C3%A9
示例:编码完整查询字符串
原始 URL:
https://example.com/search?q=hello world&lang=en&tag=c#
正确编码后:
https://example.com/search?q=hello%20world&lang=en&tag=c%23
注意:& 和 = 保持未编码状态,因为它们在此处承担结构分隔作用。只有数据值(hello world、c#)才需要编码。
空格:%20 还是 +(加号)
空格的两种编码方式是无休止混乱的根源:
| 格式 | 空格编码 | 标准 | 使用场景 |
|---|---|---|---|
| 百分号编码 | %20 | RFC 3986 | URL 路径,通用场景 |
| 表单编码 | + | HTML 规范(application/x-www-form-urlencoded) | HTML 表单提交 |
最佳实践:URL 路径中的空格使用 %20。在查询字符串中,%20 和 + 都被广泛接受,但 %20 的兼容性更强。
encodeURI 与 encodeURIComponent
JavaScript 提供了两个 URL 编码函数,其行为存在关键差异:
encodeURI()
对完整 URI 进行编码,保留在 URL 中具有结构含义的字符:
encodeURI("https://example.com/path with spaces?q=hello world")
// "https://example.com/path%20with%20spaces?q=hello%20world"
不编码:; , / ? : @ & = + $ - _ . ! ~ * ' ( ) #
encodeURIComponent()
对 URI 组成部分(如单个查询参数值)进行编码,对非保留字符以外的所有内容编码:
encodeURIComponent("hello world & goodbye")
// "hello%20world%20%26%20goodbye"
不编码:A-Z a-z 0-9 - _ . ! ~ * ' ( )
如何选择
| 使用场景 | 函数 | 示例 |
|---|---|---|
| 编码完整 URL | encodeURI() | encodeURI(fullUrl) |
| 编码查询参数值 | encodeURIComponent() | ?q=${encodeURIComponent(searchTerm)} |
| 编码路径段 | encodeURIComponent() | /users/${encodeURIComponent(username)} |
| 将跳转 URL 作为参数编码 | encodeURIComponent() | ?redirect=${encodeURIComponent(returnUrl)} |
简单原则:如果值中可能包含 &、=、? 或 /,使用 encodeURIComponent()。
常见错误
1. 重复编码
最常见的 URL 编码错误。当对已经编码过的字符串再次编码时就会出现:
原始字符串: hello world
第一次编码: hello%20world
重复编码: hello%2520world ← 错误
%20 中的 % 被编码成 %25,从而产生 %2520。服务器收到的是字面量 %20 而非空格。
预防方法:重新编码前始终先解码,或追踪字符串是否已被编码。
2. 未对查询参数值编码
// 错误 —— 若 searchTerm 包含 & 或 = 则会出问题
const url = `/search?q=${searchTerm}`;
// 正确
const url = `/search?q=${encodeURIComponent(searchTerm)}`;
如果 searchTerm 是 cats & dogs,错误写法会生成 /search?q=cats & dogs,这会被拆成两个参数:q=cats 和 dogs(或更糟糕的结果)。
3. 对参数值使用 encodeURI()
// 错误 —— encodeURI 保留 & 和 =
const url = `/api?data=${encodeURI("a=1&b=2")}`;
// 结果:/api?data=a=1&b=2 ← 变成 3 个参数而非 1 个
// 正确
const url = `/api?data=${encodeURIComponent("a=1&b=2")}`;
// 结果:/api?data=a%3D1%26b%3D2 ← 1 个参数
4. 忘记对非 ASCII 字符编码
包含非 ASCII 字符(重音字母、CJK 字符、emoji)的 URL 必须编码:
❌ https://example.com/café
✅ https://example.com/caf%C3%A9
现代浏览器在地址栏中显示解码后的版本,但实际 HTTP 请求使用的是百分号编码。
各编程语言中的 URL 编码
| 语言 | 编码 | 解码 |
|---|---|---|
| JavaScript | encodeURIComponent(str) | decodeURIComponent(str) |
| Python | urllib.parse.quote(str) | urllib.parse.unquote(str) |
| Java | URLEncoder.encode(str, "UTF-8") | URLDecoder.decode(str, "UTF-8") |
| PHP | rawurlencode($str) | rawurldecode($str) |
| C# | Uri.EscapeDataString(str) | Uri.UnescapeDataString(str) |
| Go | url.QueryEscape(str) | url.QueryUnescape(str) |
| Ruby | CGI.escape(str) | CGI.unescape(str) |
注意:Java 的 URLEncoder.encode() 和 PHP 的 urlencode() 使用 + 表示空格(表单编码)。在 PHP 中若要符合 RFC 3986 规范、以 %20 表示空格,应使用 rawurlencode()。
URL 编码与 HTML 编码
这两种编码系统经常被混淆:
| 特性 | URL 编码 | HTML 编码 |
|---|---|---|
| 目的 | 在 URL 中安全表示字符 | 在 HTML 中安全表示字符 |
| 格式 | %XX(十六进制字节) | &name; 或 &#number; |
| 空格 | %20 | 或直接空格 |
& | %26 | & |
< | %3C | < |
" | %22 | " |
| 标准 | RFC 3986 | HTML 规范 |
| 适用场景 | URL 查询字符串、路径 | HTML 属性、内容 |
两者用途完全不同,不可互换。
常见问题
URL 中的 %20 是什么意思?
%20 是空格字符的百分号编码表示。% 表示这是一个编码序列,20 是空格字符的十六进制值(十进制为 32)。当浏览器或服务器在 URL 中遇到 %20 时,会将其替换为空格。
URL 编码区分大小写吗?
根据 RFC 3986,百分号编码中的十六进制数字(如 %3A 与 %3a)不区分大小写,但标准建议使用大写。大多数实现两者都接受。不过,URL 路径的其余部分是否区分大小写取决于服务器(Linux 服务器通常区分大小写,Windows 服务器则不区分)。
国际化域名是如何工作的?
国际化域名(IDN)使用一种名为 Punycode 的系统,将 Unicode 域名转换为 ASCII。例如,münchen.de 会变成 xn--mnchen-3ya.de。路径和查询组成部分则使用标准的百分号编码来处理非 ASCII 字符。
准备好编码或解码 URL 了吗?使用我们的 URL 编码与解码工具,即时完成百分号编码并查看字符分解。如需其他编码工具,请查看我们的 Base64 编码器 和十六进制转文本工具。