← 首页

XversePro 知识库 · 编码

Unicode 与 UTF-8 编码

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 节专门提到了这种兼容性。

1. 先分清两件事:编号和存储

码点(code point)就是字符在 Unicode 里的编号,写成 U+ 加十六进制。Unicode 的编号范围是 0 到 10FFFF,Unicode 术语表把它分成 17 个平面,编号 0 到 16,每个平面 65,536 个码点。其中 0 号平面叫基本多文种平面(BMP)。

码点只是一个整数,落到文件或网络上还要选一种编码形式。常见的有 UTF-8、UTF-16 和 UTF-32,同一个码点在三者里的字节完全不同。写入和读取用的编码不一致,就会出现乱码。

中 字符 U+4E2D 码点(Unicode 编号) UTF-8E4 B8 AD UTF-16BE4E 2D UTF-32BE00 00 4E 2D 编码形式(字节)
图 1 同一个码点的三种编码结果。BE 表示大端,即高位字节在前。字节值用 Python 计算并与 Unihan 数据库 U+4E2D 条目核对。

2. UTF-8 的字节规则

规则全在 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+007F10xxxxxxx7
U+0080 到 U+07FF2110xxxxx 10xxxxxx11
U+0800 到 U+FFFF31110xxxx 10xxxxxx 10xxxxxx16
U+10000 到 U+10FFFF411110xxx 10xxxxxx 10xxxxxx 10xxxxxx21

表 1 UTF-8 字节布局,摘自 RFC 3629 第 3 节。

3. 手算一遍:中 = E4 B8 AD

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 节写明它是内容的八位字节数。

码点 U+4E2D 的 16 位 0100 111000 101101 填入模板 11100100 10111000 10101101 E4B8AD 首字节,1110 表示共 3 字节后续字节后续字节
图 2 中字的位拆分。灰色是 UTF-8 固定前缀,彩色是码点的位。结果已用 Python 的 '中'.encode('utf-8') 核对。

4. 这套设计带来的好处

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 里。

A中😀 U+1F600 41E4B8ADF09F9880 0xxxxxxx1110101011110101010 从 98 往前跳过 10 开头的字节,停在 F0 首字节 后续字节(10 开头)
图 3 三个字符的 UTF-8 字节流,用 Python 核对过。每个框下方是该字节的前缀位。

5. 和 UTF-16、BOM 的关系

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-8UTF-16BE
AU+004141(1 字节)00 41(2 字节)
éU+00E9C3 A9(2 字节)00 E9(2 字节)
中U+4E2DE4 B8 AD(3 字节)4E 2D(2 字节)
😀U+1F600F0 9F 98 80(4 字节)D8 3D DE 00(代理对,4 字节)

表 2 四个字符在两种编码下的字节,均用 Python 的 str.encode 计算。

参考资料

  1. 《RFC 3629: UTF-8, a transformation format of ISO 10646》(2003 年 11 月) https://www.rfc-editor.org/rfc/rfc3629.html
  2. 《RFC 9110: HTTP Semantics》 https://www.rfc-editor.org/rfc/rfc9110.html
  3. 《FAQ: UTF-8, UTF-16, UTF-32 & BOM》,Unicode https://www.unicode.org/faq/utf_bom.html
  4. 《RFC 2279: UTF-8, a transformation format of ISO 10646》(已被 RFC 3629 取代) https://www.rfc-editor.org/rfc/rfc2279.html
  5. 《Glossary of Unicode Terms》,Unicode https://www.unicode.org/glossary/
  6. 《Unihan Database Lookup: U+4E2D》,Unicode https://www.unicode.org/cgi-bin/GetUnihanData.pl?codepoint=4E2D