TOC

Urlencode

URL / URI 的设计中规定允许的字符,如果出现了其他字符就需要转义,这套规则被称之为百分号编码(Percent-encoding)。

WWW 的设计中就使用了这套规则,比如 URL 地址、和表单提交(application/x-www-form-urlencoded)等场景,所以这个编码规则也被称之为 URL 编码(URL encoding)。
编程时,编码解码操作一般就叫 urlencode,urldecode。

编码的方式非常简单,就是把字节用 16 进制的方式表示,然后每个字节前面加一个百分号。

应用场景

  1. URL / URI
  2. MIME application/x-www-form-urlencoded

合法字符

参考:URI 基础语法

ASCII 中 66 个非保留字符和 18 个保留字符。

非保留字符(66)

  1. 字母和数字:A-Z + a-z + 0-9
  2. 再加上四个标点:-, _, ., ~

注意,非保留字符也可以按照规则编码,比如字母 A 按 ASCII / UTF-8 可以编码成 %41。但是协议反对这种做法。

Update @ 2022-11-22:

近期遇到一个问题:邮件中的链接,如果 path 部分包含 ~,会被客户端解析成 %7E,按理来说这是非保留字符,不需要转义的。
谷歌浏览器在访问相关地址的时候会重新反转义成 ~,但是苹果的 Safari 就不会,导致没有做兼容处理的服务器端无法处理苹果浏览器访问的 %7E 路径而报错。
根据有限的资料,这可能和 WWW 生态中,部分中间服务的安全策略有关,带波浪线的可能有安全风险。这个问题待查明!
至少,以后要小心链接中使用波浪线。

Update @ 2022-11-24 (PHP: urlencode() vs. rawurlencode()):

PHP 的 urlencode, rawurlencode 的重要区别就是空格和波浪线的编码方式:

  1. urlencode 编码空格为加号(+),rawurlencode 编码空格为 %20
  2. urlencode 编码波浪线为加号(%7E),rawurlencode 不编码波浪线

PHP 5.3.4 changelog 中有提到:

Fixed bug #53248 (rawurlencode RFC 3986 EBCDIC support misses tilde char).

看来,之前都是对波浪线进行编码,但是 RFC 3986 中,波浪线是非保留字符。
PHP 中的 rawurlencode 的设计是按照 URI RFC 实现,所以做这个调整,取消了对波浪线的编码。

再往前倒,RFC 1738 Uniform Resource Locators (URL) 中,波浪线不属于非保留字符。

safe           = "$" | "-" | "_" | "." | "+"
extra          = "!" | "*" | "'" | "(" | ")" | ","
unreserved     = alpha | digit | safe | extra

保留字符(18)

!   #   $   &   '   (   )   *   +   ,   /   :   ;   =   ?   @   [   ]

保留字符是指 URI 语法中有特俗含义的一组字符。比如 http://user:pass@host:port/path?query#fragment,其密码部分包含 /

保留字符又分成两组,: / ? # [ ] @ (7) 为通用保留字符,! $ & ' ( ) * + , ; = (11) 为实现相关保留字符。
比如,URL 中,&, + 都是有特俗含义的,就是保留字符。
PS:RPC 中似乎是把前一组叫做 gen-delims,后一组叫做 sub-delims。

URL / x-www-form-urlencode

The encoding used by default is based on an early version of the general URI percent-encoding rules, with a number of modifications such as newline normalization and replacing spaces with + instead of %20.

在 Web 开发领域中 URL 编码其实和 URI 相关 RPC 的 Percent-Encoding 章节描述的规则可能有一些不一样。

比如:空格一般不转义成 %20,而是 +

print(repr(string.printable))
for index, i in enumerate(string.printable, 1):
    a, b = parse.quote(i), parse.quote_plus(i)
    if a != b:
        print('%2d. %r %5s %5s' % (index, i, a, b))
# 77. '/'     /   %2F
# 95. ' '   %20     +

参考资料与拓展阅读