XversePro 知识库 · 编码
Unicode 给每个字符编号,UTF-8 把编号变成 1 到 4 个字节,兼容 ASCII。
结论:Unicode 管编号,UTF-8 管存储。Unicode 给每个字符分配一个码点,比如中是 U+4E2D。UTF-8 把码点变成 1 到 4 个字节,中变成 E4 B8 AD。
UTF-8 的首字节自带长度信息,ASCII 字符编码后原样不变。所以按 ASCII 处理文本的老程序和文件系统,大多能直接处理 UTF-8,RFC 3629 第 1 节专门提到了这种兼容性。
码点(code point)就是字符在 Unicode 里的编号,写成 U+ 加十六进制。Unicode 的编号范围是 0 到 10FFFF,Unicode 术语表把它分成 17 个平面,编号 0 到 16,每个平面 65,536 个码点。其中 0 号平面叫基本多文种平面(BMP)。
码点只是一个整数,落到文件或网络上还要选一种编码形式。常见的有 UTF-8、UTF-16 和 UTF-32,同一个码点在三者里的字节完全不同。写入和读取用的编码不一致,就会出现乱码。
规则全在 RFC 3629 第 3 节的一张表里。只要一个字节的码点,最高位是 0,其余 7 位放码点。多字节时,首字节开头有几个 1 就表示一共几个字节,后面跟一个 0;后续字节一律以 10 开头,各带 6 位有效数据。
RFC 3629 在 2003 年 11 月发布,把 UTF-8 的范围限定在 U+0000 到 U+10FFFF,所以最多 4 个字节。它取代的 RFC 2279 还允许最长 6 字节的序列。
| 码点范围 | 字节数 | 字节格式(x 为码点的位) | 有效位 |
|---|---|---|---|
U+0000 到 U+007F | 1 | 0xxxxxxx | 7 |
U+0080 到 U+07FF | 2 | 110xxxxx 10xxxxxx | 11 |
U+0800 到 U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx | 16 |
U+10000 到 U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 21 |
表 1 UTF-8 字节布局,摘自 RFC 3629 第 3 节。
U+4E2D 落在 U+0800 到 U+FFFF 这一档,要用 3 个字节,正好 16 个有效位。先把 4E2D 写成二进制:0100 1110 0010 1101。
RFC 3629 规定从最后一个字节的最低位开始,由低到高往前填。于是低 6 位 101101 进第三个字节,接着 6 位 111000 进第二个字节,剩下的高 4 位 0100 进首字节。拼上前缀后得到 11100100、10111000、10101101,也就是 E4 B8 AD。
写代码时要分清字符数和字节数。在 Python 里,len('中') 是 1,len('中'.encode('utf-8')) 是 3。HTTP 的 Content-Length 就是按字节计数,RFC 9110 第 8.6 节写明它是内容的八位字节数。
'中'.encode('utf-8') 核对。RFC 3629 第 1 节列了 UTF-8 的一串性质,工程上最有用的有下面几条。
第一,ASCII 文本本身就是合法的 UTF-8,而且 00 到 7F 这些字节不会出现在多字节字符里。按 ASCII 字符切分的老代码,比如找斜杠、找换行,处理 UTF-8 不会切坏汉字。
第二,字符边界好找。从字节流中间任意位置开始,只要往前跳过 10 开头的后续字节,就能找到一个字符的开头。第三,C0、C1 和 F5 到 FF 这几个字节值永远不会出现,看到它们就说明数据不是合法的 UTF-8。
UTF-8 由 Ken Thompson 在 1992 年 9 月设计,设计目标由 Rob Pike 提出,最初用在 Plan 9 操作系统上,这段历史也写在 RFC 3629 里。
UTF-16 用 2 字节存 BMP 里的字符,BMP 以外的字符要拆成一对代理项(surrogate pair),共 4 字节。Unicode 官方 FAQ 说明,高代理项取值 D800 到 DBFF,低代理项取值 DC00 到 DFFF,它们本身不代表字符。RFC 3629 也禁止把 D800 到 DFFF 编成 UTF-8。
这也带来一个常见坑:有些老软件把代理对的两半分别编成 3 字节,一个表情变成 6 字节。FAQ 明确说这不符合 UTF-8 定义,这种格式另有名字叫 CESU-8。
BOM 是文件开头的 U+FEFF,在 UTF-8 里是 EF BB BF。UTF-8 没有字节序问题,BOM 只起标记作用。FAQ 提醒它会干扰要求文件以特定 ASCII 字符开头的格式,比如 Unix 脚本第一行的 #!。
| 字符 | 码点 | UTF-8 | UTF-16BE |
|---|---|---|---|
| A | U+0041 | 41(1 字节) | 00 41(2 字节) |
| é | U+00E9 | C3 A9(2 字节) | 00 E9(2 字节) |
| 中 | U+4E2D | E4 B8 AD(3 字节) | 4E 2D(2 字节) |
| 😀 | U+1F600 | F0 9F 98 80(4 字节) | D8 3D DE 00(代理对,4 字节) |
表 2 四个字符在两种编码下的字节,均用 Python 的 str.encode 计算。