少女祈祷中...
秋月竹云的博客
杂项

Unicode编码

最根本的问题:计算机怎么表示文字?

计算机只能存 0 和 1。要表示文字,必须解决两个问题:

1. 字符集(Character Set)——给每个字符分配一个唯一的数字编号。
2. 编码(Encoding)——把这个数字编号用具体的二进制字节表示出来。

乱码的根源:编码不统一时代

在 Unicode 诞生之前,不同国家、不同系统用各自独立的编码方案:

ASCII

美国

7 位,只能表示 128 个字符(英文+基本符号)

编码

适用区域

特点

ISO-8859-1

西欧

在 ASCII 上扩展了带音符的拉丁字母

GB2312 / GBK

中国大陆

中文编码,1-2 字节

Shift-JIS

日本

日文编码

Big5

繁体中文

台港地区使用

问题是:同一个二进制数,在不同编码下代表不同字符。 当你用 GBK 打开日文编码的文件时——乱码就出现了。

Unicode:一个编号体系统一全世界

Unicode 的目标:给所有文字系统的每个字符分配一个唯一的编号,不再依赖任何特定编码。

码点(Code Point)

Unicode 中每个字符的编号叫做码点,写作 U+XXXX:

字符

码点

十进制

A

U+0041

65

0

U+0030

48

U+4E2D

20013

U+6211

25041

😊

U+1F60A

128522

目前 Unicode 已定义超过 15 万个字符,涵盖 160+ 种文字系统,仍在持续扩充。

Unicode 的平面(Plane)结构

Unicode 的编码空间从 U+0000 到 U+10FFFF,共有 1,114,112 个码点位置。

这些位置被分成 17 个平面(Plane),每个平面 65,536 个码点:

平面

范围

名称

包含内容

Plane 0

U+0000 ~ U+FFFF

BMP(基本多文种平面)

ASCII、CJK 汉字、常用符号

Plane 1

U+10000 ~ U+1FFFF

SMP(多文种补充平面)

Emoji、古文字

Plane 2

U+20000 ~ U+2FFFF

SIP(表意文字补充平面)

罕用汉字

Plane 3-13

-

未分配

-

Plane 14

U+E0000 ~ U+EFFFF

SSP

标签字符等

Plane 15-16

U+F0000 ~ U+10FFFF

私用区

-

BMP(Basic Multilingual Plane) 是最重要的平面:

  • 范围:U+0000 ~ U+FFFF(0~65535)

  • 包含:ASCII、CJK 统一汉字(约 20,000+ 个)、标点符号、数学符号等

  • 日常使用的几乎所有字符都在 BMP 内

BMP 之外的情况:

  • Emoji 大部分在 Plane 1(U+1F600 ~ U+1F9FF)

  • 极罕用汉字(如 𪚥)在 Plane 2

编码方案:把码点变成字节

有了码点,下一步是决定如何存储它。Unicode 本身不是一种编码方案,它只定义了编号。把码点变成字节有三种主流方案:UTF-8、UTF-16、UTF-32

UTF-32:最简单、最浪费

每个码点固定用 4 字节

U+0041  (A)  -> 00 00 00 41    (4 字节)
U+4E2D (中) -> 00 00 4E 2D    (4 字节)
U+1F60A (😊) -> 00 01 F6 0A   (4 字节)
  • 优点:解码最快,每个码点固定 4 字节,直接读出

  • 缺点:极浪费——纯英文文件膨胀 4 倍

  • 应用:几乎不用,只在少数需要随机访问字符的场合

UTF-16:系统内部的主流

BMP 内的字符用 2 字节,BMP 外的字符用 4 字节(通过代理对):

U+0041  (A)  -> 00 41          (2 字节)
U+4E2D (中) -> 4E 2D          (2 字节)
U+1F60A (😊) -> D8 3D DE 0A   (4 字节,两个码元)

代理对(Surrogate Pair):保留 U+D800~U+DFFF 这段空间(不分配给任何实际字符),专门用于拼接出 BMP 外的码点。

大小端问题:4E 2D 还是 2D 4E?UTF-16 文件头部常有 BOM(Byte Order Mark,U+FEFF)来标记字节序。

实际应用:Windows NT 内核、Java String、JavaScript String 内部都用 UTF-16。

UTF-8:互联网的事实标准

变长编码——不同码点范围用不同字节数:

码点范围

字节数

编码模板

U+0000 ~ U+007F

1 字节

0xxxxxxx

U+0080 ~ U+07FF

2 字节

110xxxxx 10xxxxxx

U+0800 ~ U+FFFF

3 字节

1110xxxx 10xxxxxx 10xxxxxx

U+10000 ~ U+10FFFF

4 字节

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

编码逻辑可以总结为三条规则:

  1. 首字节前缀决定总字节数:前面有几个 1 就占几字节,最后以 0 结尾

  2. 续字节都以 10 开头:解码器看到 10xxxxxx 就知道是上一个字符的延续

  3. 剩余位填入码点二进制:从低位开始填充

实战拆解 1:A(U+0041)-> 1 字节

码点二进制:          01000001                    (7 位)
    模板:  0xxxxxxx
    填入:  01000001
    -> [0x41] = 1 字节

U+0000 ~ U+007F 就是 ASCII 的范围。
UTF-8 完全兼容 ASCII——纯英文的 ASCII 文件用 UTF-8 解读完全不变。

实战拆解 2:英镑符号(U+00A3)-> 2 字节

码点: U+00A3 = 10100011                       (8 位)
模板: 110xxxxx 10xxxxxx
填入: 110 00010  10 100011
     |__c2__|   |__a3__|
    -> [c2 a3] = 2 字节

U+0080 ~ U+07FF 覆盖了西欧带音符的字母、中东文字、部分标点符号等。

实战拆解 3:中(U+4E2D)-> 3 字节 (中文汉字的典型)

码点: U+4E2D = 0100 111000 101101             (16 位)
模板: 1110xxxx 10xxxxxx 10xxxxxx
填入: 1110 0100  10 111000  10 101101
     |_e4__|    |__b8__|    |__ad__|
    -> [e4 b8 ad] = 3 字节

这是中文在 UTF-8 下最常见的编码长度。每一个常用汉字被编码为 3 个连续的 UTF-8 字节。

实战拆解 4:笑脸(U+1F600)-> 4 字节

码点: U+1F600 = 000 011111 011000 000000       (17 位)
模板: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
填入: 11110 000  10 011111  10 011000  10 000000
      |_f0__|    |_9f__|     |_98__|    |_80__|
    -> [f0 9f 98 80] = 4 字节

所有 Emoji 和 BMP 外的罕用汉字都需要 4 字节。

UTF-8 的设计巧思

  1. 自同步(Self-Synchronizing):从任意位置读一个字节就能判断自己是字符开头还是续字节

    • 0xxxxxxx -> 单字节字符开头

    • 10xxxxxx -> 续字节(属于上一个字符)

    • 110xxxxx -> 2 字节字符开头

    • 1110xxxx -> 3 字节字符开头

    • 11110xxx -> 4 字节字符开头 即使数据从中间截断,解码器也能在下一个 0 或 11 开头处重新同步。

  2. 无字节序问题:UTF-8 以字节为单位,没有大端小端的歧义,不需要 BOM

  3. 完全向后兼容 ASCII:0x00~0x7F 编码与 ASCII 完全一致

三种编码方案对比

特性

UTF-8

UTF-16

UTF-32

英文

1 字节

2 字节

4 字节

常见中文

3 字节

2 字节

4 字节

Emoji

4 字节

4 字节(代理对)

4 字节

兼容 ASCII

完全兼容

不兼容

不兼容

字节序问题

有(需 BOM)

有(需 BOM)

自同步

可以

不可以

可以

主流应用

网络、文件、LLM

系统内部(Java/JS/Win32)

极少使用

实际选择:

  • 出门在外用 UTF-8(文件、网络、API、数据库)

  • 窝里用 UTF-16(操作系统和编程语言内部的字符串表示)

  • 极少用 UTF-32(只在需要 O(1) 随机访问特定码点的特殊场景)

如何实际查看编码

在python中:

ord() 函数将单个 Unicode 字符转换为其整数表示形式。
chr() 函数将整数 Unicode 代码点转换为包含对应字符的字符串。
encode() 函数将 Unicode 字符串编码为 UTF-8的字节字符串
decode() 函数将 UTF-8 字节字符串解码为 Unicode 字符串

>>> ord('中')
20013
>>> chr(20013)
'中'
>>> hex(20013)
'0x4e2d'
>>> "中".encode()
b'\xe4\xb8\xad'

的码点为 U+4E2D,UTF-8编码后的字节为 e4 b8 ad

>>> "中".encode('utf-8')
b'\xe4\xb8\xad'
>>> "中".encode('utf-16')
b'\xff\xfe-N'
>>> "中".encode('utf-32')
b'\xff\xfe\x00\x00-N\x00\x00'
>>> chr(0x4e)
'N'
>>> chr(0x2d)
'-'

\xff\xfe 表示小端序

评论