什么是百分号编码?(用最简单的方式讲清楚 RFC 3986)
百分号编码是 RFC 3986 中定义的一种机制,用于对 URI 中的特殊字符进行编码:将它们替换为一个百分号(%)加上两位十六进制数字。它确保 URL 只包含可以在互联网上安全传输的合法 ASCII 字符。
什么是百分号编码?
百分号编码,也称为 URL 编码,是在 URI(统一资源标识符)中表示那些不被允许或具有特殊含义的字符的标准方式。它的工作原理是:将每个需要编码的字符替换为一个百分号(%)加上两位十六进制数字,这两位数字表示该字符的字节值。
例如,空格字符(字节值 0x20)被编码为 %20。at 符号 @(字节值 0x40)被编码为 %40。这一机制确保任何数据都能安全地嵌入到 URI 中,而不会与 URI 语法产生冲突。
“百分号编码”这个术语来源于用百分号作为转义前缀的做法。它的正式规范定义在 RFC 3986 中,该文档由互联网工程任务组(IETF)于 2005 年 1 月发布,作者是 Tim Berners-Lee、Roy Fielding 和 Larry Masinter。
百分号编码是如何工作的
编码过程遵循以下步骤:首先,使用某种字符编码(在现代 Web 中几乎总是 UTF-8)将字符转换为它的字节表示。然后,每个字节被表示为一个百分号加上两位十六进制数字(按惯例使用大写的 A-F,不过解码器应当同时接受小写形式)。
// 逐步演示空格字符的编码过程
// 字符:' '(空格)
// ASCII/UTF-8 字节值:0x20(十进制 32)
// 百分号编码结果:%20
// 逐步演示一个多字节字符的编码过程:带尖音符的 e
// 字符:带尖音符的 e(U+00E9)
// UTF-8 字节:0xC3 0xA9
// 百分号编码结果:%C3%A9
// 逐步演示一个 3 字节字符的编码过程
// 字符:CJK 表意文字(U+4E2D)
// UTF-8 字节:0xE4 0xB8 0xAD
// 百分号编码结果:%E4%B8%AD
十六进制数字必须始终成对出现。单独的一个百分号,或者百分号后面跟着非十六进制字符,都是非法的,会导致解码错误。这也是 JavaScript 中“URI malformed”错误的一个常见来源。
百分号编码始终是可逆的。解码器读取百分号,把接下来的两个字符当作一个十六进制字节值,再将其转换回原始字符。对于多字节的 UTF-8 字符,连续的多个编码字节会被组合在一起并一同解码。
哪些字符必须被编码?
并非所有字符都需要百分号编码。RFC 3986 定义了两类字符:从不需要编码的非保留字符(unreserved characters),以及在不用于其保留用途时必须被编码的保留字符(reserved characters)。所有其他字符(包括空格、非 ASCII 字符和控制字符)都必须始终被编码。
不需要编码的字符(非保留字符):大写字母(A-Z)、小写字母(a-z)、数字(0-9)、连字符(-)、句点(.)、下划线(_)以及波浪号(~)。这 66 个字符可以出现在 URI 的任何位置而无需编码。
有时需要编码的字符(保留字符):: / ? # [ ] @ ! $ & ' ( ) * + , ; =。这些字符在 URI 中具有特殊的语法含义。只有当它们被当作数据而非分隔符使用时才需要编码。
始终需要编码的字符:空格、非 ASCII 字符(任何码值大于 127 的字符)、控制字符(0-31 以及 127),以及像 < > { } | \ ^ ` " 这样的不安全字符。
RFC 3986 中的保留字符与非保留字符
保留字符与非保留字符之间的区分对于理解 URI 的工作方式至关重要。保留字符充当分隔符,用于定义 URI 的结构。例如,:// 将 scheme 与 authority 分隔开,/ 分隔各个路径片段,? 标志查询字符串的开始,而 # 标志片段(fragment)的开始。
| 类别 | 字符 | 何时需要编码 |
|---|---|---|
| 非保留字符 | A-Z a-z 0-9 - . _ ~ |
从不 |
| 通用分隔符 | : / ? # [ ] @ |
当被当作数据而非分隔符使用时 |
| 子分隔符 | ! $ & ' ( ) * + , ; = |
当在赋予其特定含义的组成部分中被当作数据使用时 |
| 所有其他字符 | 空格、非 ASCII 字符、控制字符等 | 始终 |
RFC 3986 中一条关键原则是:仅在某个保留字符是被百分号编码还是以字面形式出现这一点上存在差异的 URI,并不等价。例如,/path/to 和 /path%2Fto 是不同的 URI,尽管 %2F 解码后就是 /。前者有两个路径片段;后者只有一个路径片段,其中包含一个字面的斜杠。
百分号编码与 URL 编码:它们是一回事吗?
“百分号编码”和“URL 编码”经常被交替使用,在大多数语境下它们指的是同一件事。不过,两者之间存在一个细微的历史差异。“URL 编码”有时可能指代更早的、由 HTML 表单使用的 application/x-www-form-urlencoded 格式,该格式将空格编码为 + 而不是 %20。
application/x-www-form-urlencoded 格式定义在 HTML 规范中,早于 RFC 3986。它的规则略有不同:空格会变成 +,且不被编码的字符集合也稍有区别。如今,“URL 编码”这个术语几乎总是指 RFC 3986 的百分号编码,其中空格被编码为 %20。
在实践中,对于所有 URI 场景(路径片段、REST API 中的查询参数、片段标识符),你都应当使用 RFC 3986 的百分号编码。仅在处理 HTML 表单提交,或某个特定 API 有明确要求时,才使用 application/x-www-form-urlencoded 格式。
// RFC 3986 百分号编码(推荐用于 URI)
encodeURIComponent('hello world') // "hello%20world"
// application/x-www-form-urlencoded(HTML 表单)
new URLSearchParams({q: 'hello world'}).toString() // "q=hello+world"
// Python 中的等价写法
from urllib.parse import quote, quote_plus
quote('hello world', safe='') # "hello%20world"
quote_plus('hello world') # "hello+world"