人类交流使用 A
、B
、C
、中
等字符,但计算机只认识 0
和 1
。因此,就需要将人类的字符,转换成计算机认识的二进制编码。这个过程就是字符编码。
ASCII
最简单、常用的字符编码就是 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码),它将美国人最常用的 26 个英文字符的大小写和常用的标点符号,编码成 0
到 127
的数字。例如 A
映射成 65
(0x41
),这样计算机中就可以用 0100 0001
这组二进制数据,来表示字母 A
了。
ASCII 编码的字符可以分成两类:
- 控制字符:
0
-31
和127
(0x00
-0x1F
和0x7F
) - 可显示字符:
32
-126
(0x20
-0x7E
)
Unicode ASCII 只编码了美国常用的 128 个字符。显然不足以满足世界上这么多国家、这么多语言的字符使用。于是各个国家和地区,就都开始对自己需要的字符设计其他编码方案。例如,中国有自己的 GB2312,不够用了之后又扩展了 GBK,还是不够用,又有了 GB18030。欧洲有一系列的 ISO-8859 编码。这样各国人民就都可以在计算机上处理自己的语言文字了。
但每种编码方案,都只考虑了自己用到的字符,没办法跨服交流。如果一篇文档里,同时使用了多种语言的字符,总不能分别指定哪个字符使用了那种编码方式。
如果能统一给世界上的所有字符分配编码,就可以解决跨服交流的问题了,Unicode 就是来干这个事情的。
Unicode 统一编码了世界上大部分的字符,例如将
A
编码成 0x00A1
,将 中
编码成 0x4E2D
,将 α
编码成 0x03B1
。这样,中国人、美国人、欧洲人,就可以使用同一种编码方式交流了。一个 Unicode 字符可以使用
U+
和 4 到 6 个十六进制数字来表示。例如 U+0041
表示字符 A
、U+4E2D
表示字符 中
,U+03B1
表示字符 α
。Unicode 最初编码的范围是
0x0000
到 0xFFFF
,也就是两个字节,最多 65536 (2^16
) 个字符。但随着编码的字符越来越多,两个字节的编码空间已经不够用,因此又引入了 16 个辅助平面,每个辅助平面同样最多包含 65536 个字符。原来的编码范围称为基本平面,也叫第 0 平面。各平面的字符范围和名称如下表:
平面 | 字符范围 | 名称 |
---|---|---|
0 号平面 | U+0000 - U+FFFF |
基本多文种平面 (Basic Multilingual Plane, BMP) |
1 号平面 | U+10000 - U+1FFFF |
多文种补充平面 (Supplementary Multilingual Plane, SMP) |
2 号平面 | U+20000 - U+2FFFF |
表意文字补充平面 (Supplementary Ideographic Plane, SIP) |
3 号平面 | U+30000 - U+3FFFF |
表意文字第三平面 (Tertiary Ideographic Plane, TIP) |
14 号平面 | U+E0000 - U+EFFFF |
特别用途补充平面 |
15 号平面 | U+F0000 - U+FFFFF |
保留作为私人使用区(A 区)(Private Use Area-A, PUA-A) |
16 号平面 | U+100000 - U+10FFFF |
保留作为私人使用区(B 区)(Private Use Area-B, PUA-B) |
字符编码的基本概念 “字符编码”是一个模糊、笼统的概念,为了进一步说明字符编码的过程,需要将其拆解为一些更加明确的概念:
字符 (Character)
人类使用的字符。例如:
A
;中
等。
把一些字符的集合 (Character Set) 中的每个字符 (Character),映射成一个编号或坐标。例如:
- 在 ASCII 中,把
A
编号为65
(0x41
); - 在 Unicode 中,把
中
编号为0x4E2D
; - 在 GB2312 中,把
中
映射到第 54 区第 0 位。
Unicode 就是一个 CCS。
字符编码表 (Character Encoding Form, CEF)
把 Code Point 转换成特定长度的整型值的序列。这个特定长度的整型值叫做 Code Unit。例如:
- 在 ASCII 中,
0x41
这个 Code Point 会被转换成0x41
这个 Code Unit; - 在 UTF-8 中,
0x4E2D
这个 Code Point 会被转换成0xE4 B8 AD
这三个 Code Unit 的序列。
字符编码方案 (Character Encoding Scheme, CES)
【详解字符编码与 Unicode】把 Code Unit 序列转换成字节序列(也就是最终编码后的二进制数据,供计算机使用)。例如 :
0x0041
这个 Code Unit,使用大端序会转换成0x00 41
两个字节;- 使用小端序会转换成
0x41 00
两个字节。
这些概念间的关系如下:
文章图片
因此,我们说 ASCII 是“字符编码”时,“字符编码”指的是上面从 Character 到字节数组的整个过程。因为 ASCII 足够简单,中间的 Code Point 到 Code Unit,再到字节数组,都是一样的,没必要拆开说。
而我们说 Unicode 是“字符编码”时,“字符编码”其实指的仅是上面的 CCS 部分。
同理,ASCII、Unicode、UTF-8、UTF-16、UTF-16 LE,都可以笼统的叫做“字符编码”,但每个“字符编码”表示的含义都是不同的。可能是 CCS、CEF、CES,也可能是整个过程。
Unicode 转换格式 Unicode 只是把字符映射成了 Code Point (字符编码表,CCS)。将 Code Point 转换成 Code Unit 序列(字符编码表,CEF),再最终将 Code Unit 序列转换成字节序列(字符编码方案,CES),有多种不同的实现方式。这些实现方式叫做 Unicode 转换格式 (Unicode Transformation Format, UTF)。主要包括:
- UTF-32
- UTF-16
- UTF-8
UTF-32 将每个 Unicode Code Point 转换成 1 个 32 位长的 Code Unit。
UTF-32 是固定长度的编码方案,每个 Code Unit 的值就是其 Code Point 的值。例如
0x00 00 00 41
这个 Code Unit,就表示了 0x0041
这个 Code Point。UTF-32 的一个 Code Unit,需要转换成 4 个字节的序列。因此,有大端序 (UTF-32 BE) 和小端序 (UTF-32 LE) 两种转换方式。
例如
0x00 00 00 41
这个 Code Unit,使用 UTF-32 BE 最终会编码为 0x00 00 00 41
;使用 UTF-32 LE 最终会编码为 0x41 00 00 00
。UTF-16
UTF-16 将每个 Unicode Code Point 转换成 1 到 2 个 16 位长的 Code Unit。
对于基本平面的 Code Point(
0x0000
到 0xFFFF
),每个 Code Point 转换成 1 个 Code Unit,Code Unit 的值就是其对应 Code Point 的值。例如 0x0041
这个 Code Unit,就表示了 0x0041
这个 Code Point。对于辅助平面的 Code Point(
0x010000
到 0x10FFFF
),每个 Code Point 转换成 2 个 Code Unit 的序列。如果还是直接使用 Code Point 数值转换成 Code Unit,就有可能和基本平面的编码重叠。例如 U+010041
如果转换成 0x0001
、0x0041
这两个 Code Unit,解码的时候没办法知道这是 U+010041
一个字符,还是 U+0001
和 U+0041
两个字符。为了让辅助平面编码的两个 Code Unit,都不与基本平面编码的 Code Unit 重叠,就需要利用基本平面中一个特殊的区段了。基本平面中规定了从
0xD800
到 0xDFFF
之间的区段,是永久保留不映射任何字符的。UTF-16 将辅助平面的 Code Point,编码成一对在这个范围内的 Code Unit,叫做代理对。这样解码的时候,如果解析到某个 Code Unit 在 0xD800
到 0xDFFF
范围内,就知道他不是基本平面的 Code Unit,而是要两个 Code Unit 组合在一起去表示 Code Point。具体转换方式是:
- 将辅助平面的 Code Point 的值 (
0x010000
-0x10FFFF
),减去0x010000
,得到0x00000
到0xFFFFF
范围内的一个数值,也就是最多 20 个比特位的数值 - 将前 10 位的值(范围在
0x0000
到0x03FF
),加上0xD800
,得到范围在0xD800
到0xDBFF
的一个值,作为第一个 Code Unit,称作高位代理或前导代理 - 将后 10 位的值(范围在
0x0000
到0x03FF
),加上0xDC00
,得到范围在0xDC00
到0xDFFF
的一个只,作为第二个 Code Unit,称作低位代理或后尾代理
0xD800
- 0xDBFF
和 0xDC00
- 0xDFFF
这两个区段,也分别叫做 UTF-16 高半区 (High-half zone of UTF-16) 和 UTF-16 低半区 (Low-half zone of UTF-16)。UTF-16 的一个 Code Unit,需要转换成 2 个字节的序列。因此,有大端序 (UTF-16 BE) 和小端序 (UTF-16 LE) 两种转换方式。
例如
0x0041
这个 Code Unit,使用 UTF-16 BE 最终会编码为 0x0041
;使用 UTF-16 LE 最终会编码为 0x4100
。UTF-8
UTF-8 将每个 Unicode Code Point 转换成 1 到 4 个 8 位长的 Code Unit。
UTF-8 是不定长的编码方案,使用前缀来标识 Code Unit 序列的长度。解码时,根据前缀,就知道该将哪几个 Code Unit 组合在一起解析成一个 Code Point 了。
具体编码方式是:
Code Point 范围 | Code Unit 个数 | 每个 Code Unit 前缀 | 示例 Code Point | 示例 Code Unit 序列 |
---|---|---|---|---|
7 位以内 (0 - 0xEF ) |
1 | 0b0 |
0b0zzz zzzz |
0b0zzz zzzz |
8 到 11 位 (0x80 - 0x07FF ) |
2 | 第一个 0b110 ,剩下的 0b10 |
0b0yyy yyzz zzzz |
0b110y yyyy 10zz zzzz |
12 到 16 位 (0x0800 - 0xFFFF ) |
3 | 第一个 0b1110 ,剩下的 0b10 |
0bxxxx yyyy yyzz zzzz |
0b1110 xxxx 10yy yyyy 10zz zzzz |
17 到 21 位 (0x10000 - 10FFFF ) |
4 | 第一个 0b11110 ,剩下的 0b10 |
0b000w wwxx xxxx yyyy yyzz zzzz |
0b1111 0www 10xx xxxx 10yy yyyy 10zz zzzz |
- 前缀是
0b0
,说明这个 Code Point 是一个 Code Unit 组成 - 前缀是
0b110
,说明这个 Code Point 是两个 Code Unit 组成,后面还会有 1 个0b10
前缀的 Code Unit - 前缀是
0b1110
,说明这个 Code Point 是三个 Code Unit 组成,后面还会有 2 个0b10
前缀的 Code Unit - 前缀是
0b11110
,说明这个 Code Point 是四个 Code Unit 组成,后面还会有 3 个0b10
前缀的 Code Unit
参考上表,对于 ASCII 范围内的字符,使用 ASCII 和 UTF-8 编码的结果是一样的。所以 UTF-8 是 ASCII 的超集,使用 ASCII 编码的字节流也可以使用 UTF-8 解码。
UTF-8 与 UTF-16 对比
Code Point 范围 | UTF-8 编码长度 | UTF-16 编码长度 |
---|---|---|
7 位以内 (0x00 - 0xEF ) |
1 | 2 |
8 到 11 位 (0x0080 - 0x07FF ) |
2 | 2 |
12 到 16 位 (0x0800 - 0xFFFF ) |
3 | 2 |
17 到 21 位 (0x10000 - 10FFFF ) |
4 | 4 |
0x00
到 0xEF
范围的字符,UTF-8 编码比 UTF-16 短;而在 0x0800
- 0xFFFF
范围内,UTF-8 编码是比 UTF-16 长的。而中文主要在
0x4E00
到 0x9FFF
,如果写一篇文档,全都是中文,一个英文字母和符号都没有。那使用 UTF-8 编码,可能比 UTF-16 编码还要多占用一半的空间。相关文章:
- Unicode 标准化
- Unicode 与编程语言
- 字节顺序标记
- 字节序
- Unicode 与 UCS
推荐阅读
- Unicode 与编程语言
- Unicode 标准化
- Unicode 与 UCS
- #yyds干货盘点#Golang 字符编码UTF-8Unicode之间的关系
- 定位|万字长文(关于sourcemap,这篇文章就够了)
- Appium升级后安装UnicodeIME-debug.apk 提示
- Appendix AUnicode
- Some characters were lost while converting from UNICODE to CP 0. Save to file anyway? winedt
- android中一些特殊字符(如(←↑→↓等箭头符号)的Unicode码值)