ToolHub
View All Posts

URL 编码详解:什么是百分号编码

每次你点击链接、提交表单或输入网址时,你的浏览器都在幕后处理 URL 编码。你在 URL 中看到的那些由百分号和十六进制数字组成的神秘字符串并非随机;它们是一种精心设计的机制,使互联网可靠运行。理解 URL 编码对于 Web 开发者、API 设计者和任何从事 Web 技术工作的人来说都至关重要。本指南解释了 URL 编码是什么、为什么存在、如何工作以及你需要了解的安全影响。

什么是 URL 编码?

URL 编码,正式名称为百分号编码,是 RFC 3986 中定义的一种机制,将字符转换为可在统一资源标识符(URI)中安全传输的格式。URL 规范仅允许 ASCII 字符的子集直接出现在 URL 中。任何不在此允许集合中的字符必须编码为百分号(%)后跟两个十六进制数字,表示该字符的字节值。

< 变为 %3C,因为 < 的 ASCII 码是 60,十六进制为 3C。

为什么需要 URL 编码

URL 具有使用某些字符作为分隔符的特定语法。问号 ? 将路径与查询字符串分隔。& 符号分隔查询参数。等号 = 分隔参数名和值。正斜杠 / 分隔路径段。如果这些字符出现在传输的数据中,它们会被误解为 URL 结构而非内容。

考虑用户在网站上搜索 "rock & roll" 时会发生什么。不编码的话,URL 会是 /search?q=rock & roll。浏览器会将 & 解释为查询参数分隔符,将查询拆分为两个参数:q=rock 和名为 roll 的第二个参数(无值)。URL 编码修复了这个问题:/search?q=rock%20%26%20roll。

百分号编码的工作原理

编码过程遵循 URI 规范定义的简单算法:

  1. 识别非保留字符:字符 A-Z、a-z、0-9、-、.、_ 和 ~ 是非保留的。它们从不需要编码,可直接出现在 URL 中。
  2. 识别保留字符:字符 :、/、?、#、[、]、@、!、$、&、'、(、)、*、+、,、; 和 = 是为 URL 语法保留的。当它们用作数据而非分隔符时必须编码。
  3. 编码其他所有字符:任何不在非保留或保留集合中的字符必须进行百分号编码。这包括空格、控制字符、非 ASCII 字符和任何 ASCII 范围之外的字符。

非 ASCII 字符的编码过程

非 ASCII 字符(如重音字母、中日韩字符和表情符号)需要额外步骤。首先,将字符转换为其 UTF-8 字节序列。然后每个字节单独进行百分号编码。这意味着单个字符可能产生多个百分号编码三元组。

例如,欧元符号(€)的 UTF-8 字节序列为 E2 82 AC。URL 编码后变为 %E2%82%AC。汉字"中"的 UTF-8 序列为 E4 B8 AD,产生 %E4%B8%AD。表情符号😀(笑脸)有 4 字节 UTF-8 序列,编码为 %F0%9F%98%80。

常见的编码字符

下表显示了最常见的 URL 编码字符:

字符编码ASCII 码原因
空格%2032 (0x20)URL 中不允许
!%2133 (0x21)保留(子分隔符)
#%2335 (0x23)保留(片段分隔符)
$%2436 (0x24)保留(子分隔符)
&%2638 (0x26)保留(查询分隔符)
'%2739 (0x27)保留(子分隔符)
(%2840 (0x28)保留(子分隔符)
)%2941 (0x29)保留(子分隔符)
+%2B43 (0x2B)保留(查询中的空格)
,%2C44 (0x2C)保留(子分隔符)
/%2F47 (0x2F)保留(路径分隔符)
:%3A58 (0x3A)保留(协议分隔符)
;%3B59 (0x3B)保留(参数分隔符)
=%3D61 (0x3D)保留(值分隔符)
?%3F63 (0x3F)保留(查询分隔符)
@%4064 (0x40)保留(授权分隔符)
[%5B91 (0x5B)保留(IPv6 字面量)
]%5D93 (0x5D)保留(IPv6 字面量)

URL 编码与 HTML 编码

URL 编码和 HTML 实体编码经常被混淆,但它们的用途完全不同,运行在不同的上下文中。

URL 编码

URL 编码将字符转换为 %XX 格式,以便在 URL 中安全传输。它由 RFC 3986 规范,应用于 URL 组件如路径、查询字符串和片段。

HTML 编码

HTML 编码将字符转换为实体引用如 &、< 和 >,以便在 HTML 文档中安全渲染。它防止浏览器将内容解释为 HTML 标记。HTML 编码由 HTML 规范规范。

关键区别

方面URL 编码HTML 编码
上下文URL 和 URIHTML 文档
格式%XX(百分号 + 十六进制)或 &#NNN;
< 的示例%3C<
& 的示例%26" 的示例
%22"目的
安全的 URL 传输防止 HTML 注入规范
RFC 3986HTML Living Standard HTML Living Standard

(HTML 编码的 & 符号)。两者都有效,但 URL 编码 & 符号是更正确的做法,因为它保留了数据的预期含义。

不同编程语言中的 URL 编码

每种主要编程语言都提供 URL 编码和解码的内置函数。然而,确切的行为各不相同,理解差异对避免错误至关重要。

JavaScript

JavaScript 提供三个编码函数,每个行为不同:

// encodeURI - encodes a complete URL
// Preserves: :, /, ?, #, &, =, +, @, ;, ,, !, ~, *, ', (, )
const url = encodeURI("https://example.com/search?q=hello world");
// Result: "https://example.com/search?q=hello%20world"

// encodeURIComponent - encodes a URL component
// Encodes ALL special characters including URL delimiters
const param = encodeURIComponent("price=100&discount=20");
// Result: "price%3D100%26discount%3D20"

// Never use escape() - it is deprecated
// It does not handle Unicode correctly

关键区别在于 encodeURI 保留 URL 结构字符,而 encodeURIComponent 编码所有内容。当你有完整 URL 时使用 encodeURI,编码单个参数值时使用 encodeURIComponent。

Python

from urllib.parse import quote, quote_plus, urlencode

# quote - standard URL encoding (spaces as %20)
encoded = quote("hello world&more")
# Result: "hello%20world%26more"

# quote_plus - spaces become + instead of %20
encoded_plus = quote_plus("hello world")
# Result: "hello+world"

# urlencode - encodes a dictionary as a query string
params = {"q": "hello world", "lang": "en"}
query = urlencode(params)
# Result: "q=hello+world&lang=en"

PHP

// urlencode - spaces become +
$encoded = urlencode("hello world&more");
// Result: "hello+world%26more"

// rawurlencode - RFC 3986 compliant (spaces as %20)
$raw = rawurlencode("hello world&more");
// Result: "hello%20world%26more"

空格字符:%20 与 +

空格字符有两种常见编码,理解何时使用每种编码很重要:

这个区别很重要,因为将 + 解码为空格仅在 application/x-www-form-urlencoded 数据的上下文中是正确的。在 URL 路径组件中,+ 是字面加号,不是空格。大多数服务器端框架对查询字符串处理正确,但在编码路径或使用自定义 URL 方案时可能导致细微错误。

安全影响

URL 编码具有重要的安全影响,每个 Web 开发者都必须了解。

双重编码攻击

双重编码发生在数据被 URL 编码多次时。攻击者可能提交 %2527,它在第一次解码时变为 %27,第二次解码时变为单引号 '。如果安全过滤器只检查第一次解码,就会遗漏恶意字符。这可以绕过输入验证、跨站脚本(XSS)过滤器和 SQL 注入防护。