URI 是「标识资源」的通用写法,URL 只是它的一种(定位型的那种)。 中文在 URI 里膨胀 9 倍:一个汉字 3 字节,每个字节变成 3 个字符。
这一节把四类 URI 逐段拆开,再量一遍百分号编码的膨胀。
最后说清 quote 那个 safe 参数的坑。
一、URI 和 URL 的关系
URI 是上位概念:只要能「标识」一个资源就算,不要求能定位到它。
URL 是下位概念:它必须能「定位」,也就是给出取到它的方法。
举个最好懂的例子:「张三」这个名字能标识一个人,但不能定位到他在哪儿;
「湖北省宜昌市某某路 1 号」既能标识也能定位。
前者像 URN(名字),后者像 URL(位置)。
两者都是 URI。
实践中这个区分经常被忽略,
因为今天几乎所有 URI 都是 URL——URN 那一支(比如 urn:isbn:...)几乎没铺开。
所以「URI 和 URL 有什么区别」这个问题的实用答案是:「技术上 URI 更大,
日常里说的是同一件事」。
二、没有 netloc 也是合法的 URI
mailto:[email protected] 后面没有 //,也就没有 netloc。
因为「发信给某个人」不需要「去哪台机器、敲哪个门」——收件人的域名要靠 DNS 的 MX 记录去查。
所以「有 netloc」不是 URI 的必备项,是「用 // 开头的定位型 URL」才有的。
这个 // 的来源还挺有意思:它最初是为了兼容「URL 和路径可以混着写」的老用法,
结果留成了一个永久的语法标记。
相对的 URI 更是常见:网页里的每一个链接几乎都是 a/b.html 这种形式,
它不带 scheme 和主机,只能在某个上下文里解析。
所以「一个 URI 是不是绝对地址」,取决于它有没有 scheme。
三、中文为什么变长 9 倍
URI 只允许一小撮 ASCII 字符,其他字符必须写成 %XX 的形式(XX 是字节的十六进制)。
一个汉字在 UTF-8 里是 3 个字节,
每个字节编码成 3 个字符(% 加两位十六进制),所以一个汉字变 9 个字符。
膨胀 9 倍。
这不是「URL 不支持中文」,是「URL 的字符集太小」。
这个 9 倍有实际后果:一个中文标题 30 个字,编码后 270 个字符,再加上域名和路径,
很容易超过浏览器或服务器的 URL 长度上限。
所以「用中文做 URL」在工程上一般被劝阻——不是不能用,是「长度不可控」。
而「哪些字符要编码」这件事,是 URI 规范里最容易写错的一页:
因为有些字符「在某些位置安全、在另一些位置不安全」。
最典型的是 /:它在路径里是分隔符,天然安全;但它是参数值的一部分时就必须编码成 %2F。
所以标准库给了一个 safe 参数,让你自己决定这个清单。
四、动手 Demo:URI 的每一段在说什么,以及中文为什么要变长
先把四类 URI 逐段拆开,再量百分号编码的膨胀。
注意 a/b 在默认行为下根本没变,加 safe='' 才变。
import urllib.parse
cases = [
'https://user:[email protected]:8443/a/b.html?q=1&n=2#frag',
'mailto:[email protected]',
'ftp://ftp.example.org/pub/file.bin',
'/a/b.html',
]
for u in cases:
sp = urllib.parse.urlsplit(u)
print(u)
print(' scheme =', sp.scheme or '(无)')
print(' netloc =', sp.netloc or '(无)')
print(' path =', sp.path or '(无)')
samples = ['a', '报告', '季度报告2026', 'a b', 'a/b']
for s in samples:
q = urllib.parse.quote(s)
q2 = urllib.parse.quote(s, safe='')
print(repr(s), len(s), len(s.encode('utf-8')))
print(' quote 默认 ', q, len(q))
print(' quote 全编码', q2, len(q2))
实跑输出:
--------------------------------------------------------------------
URI 的每一段在说什么,以及中文为什么要变长
--------------------------------------------------------------------
**URI 是「标识一个资源」的通用写法**,**URL 只是它的一种。**
**它的结构不许有歧义,所以每一段都有严格定义。**
**四个不同类型的 URI,拆出来是这样**:
https://user:[email protected]:8443/a/b.html?q=1&n=2#frag
scheme = https
netloc = user:[email protected]:8443
path = /a/b.html
query = q=1&n=2
frag = frag
mailto:[email protected]
scheme = mailto
netloc = (无)
path = [email protected]
ftp://ftp.example.org/pub/file.bin
scheme = ftp
netloc = ftp.example.org
path = /pub/file.bin
/a/b.html
scheme = (无)
netloc = (无)
path = /a/b.html
**注意第二段**:**`mailto:` 后面没有 `//`,也就没有 netloc。**
**因为「发信给某个人」不需要「去哪台机器、敲哪个门」**——**收件人的域名要靠 DNS 的 MX 记录去查。**
**所以「有 netloc」不是 URI 的必备项,是「定位型 URL」才有的。**
**第四段是「相对 URI」**:**它不带 scheme 和主机,只能在某个上下文里解析。**
**网页里的每一个链接几乎都是这种形式。**
**下一件事是「中文为什么在 URL 里变长」**:
「a」 字符 1 UTF-8 1 字节
quote 默认 a (1 字符)
quote 全编码 a (1 字符)
「报告」 字符 2 UTF-8 6 字节
quote 默认 %E6%8A%A5%E5%91%8A (18 字符)
quote 全编码 %E6%8A%A5%E5%91%8A (18 字符)
「季度报告2026」 字符 8 UTF-8 16 字节
quote 默认 %E5%AD%A3%E5%BA%A6%E6%8A%A5%E5%91%8A2026 (40 字符)
quote 全编码 %E5%AD%A3%E5%BA%A6%E6%8A%A5%E5%91%8A2026 (40 字符)
「a b」 字符 3 UTF-8 3 字节
quote 默认 a%20b (5 字符)
quote 全编码 a%20b (5 字符)
「a/b」 字符 3 UTF-8 3 字节
quote 默认 a/b (3 字符)
quote 全编码 a%2Fb (5 字符)
**`报告` 这一行说明两件事**:**一个汉字在 UTF-8 里是 3 个字节**,
**每个字节编码成 `%XX` 又要 3 个字符,所以一个汉字变 9 个字符。**
**膨胀 9 倍。**
**这不是「URL 不支持中文」,是「URL 只允许一小撮 ASCII 字符」。**
**最后一行还有一个坑**:**`a/b` 在 `quote` 的默认行为下根本没变,斜杠原样留着。**
**因为 `/` 在 URL 的路径里本来就是合法分隔符,标准把一小撮字符列成了「安全字符」。**
**但只有当斜杠是「路径结构」时才安全。**
**如果 `/` 是「参数值的一部分」,就必须编码成 `%2F`**——**否则服务端会把它当成新的一级目录。**
**所以 `quote` 才留了一个 `safe` 参数让你改这个清单。**
**同理 `?`、`#`、`&`、`=` 都有各自的「什么时候算安全」的规则。**
**「哪些字符要编码」这件事,是 URI 规范里最容易写错的一页。**
第二段 mailto: 没有 netloc,第四段 /a/b.html 连 scheme 都没有。
这两个都是合法的 URI,只是一个「不需要定位」,一个「还没给出定位的前半截」。
报告 两个汉字 6 个字节,编码后 18 个字符,正好 9 倍。
季度报告2026 是 16 字节变 40 字符——因为里面那 4 个数字不用编码,所以不到 9 倍。
最后一行最能说明问题:a/b 在 quote 的默认行为下原样留成 a/b,
加 safe='' 之后才变成 a%2Fb。
因为 / 在 URL 的路径里本来就是合法分隔符,标准把它列进了「安全字符」。
但只有当斜杠是「路径结构」时才安全——
如果它是参数值的一部分,不编码就会被服务端当成新的一级目录。
30 秒实验
- 算一下:30 个汉字的标题,编码后是多少字符?
- 想一下:为什么
mailto:后面不需要//? - 想一下:
quote的safe参数该在什么时候用?
常见误区
- 以为「URI 就是 URL」。 URL 是 URI 的一种,只要求「能定位」。
- 以为「
//是 URI 的必备语法」。mailto:就没有,它不是必需的。 - 以为「中文不能被编码成 URL」。 能,只是要膨胀 9 倍,问题在长度不在可行性。
自测三问(点开对答案)
1. URI 和 URL 的关系是什么? —— **URL 是 URI 的子集**,要求能定位到资源。 2. 一个汉字编码后为什么变 9 个字符? —— **UTF-8 里 3 个字节,每个字节写成 `%XX` 占 3 个字符。** 3. `a/b` 为什么默认不被编码? —— **`/` 在路径里是合法分隔符**,属于「安全字符」。下一节看 HTML 到底是干什么的。
- ☐ (本节完,下一节 8.5.4 HTML)