计算机编码发展的历史

关于计算机系统中编码的历史(例如 ASCII、GBK 这些编码等等)就不介绍了,如果感兴趣可以看看知乎上面的 这个回答

Unicode 字符集

Unicode(Universal Multiple-Octet Coded Character Set) 是由 ISO 提出的,这是一种旨在包含目前世界上所有文化、所有字母的字符编码表,其目的是世界上任意一个字符都可以在该表中找到其所对应的编码值。Unicode 字符集兼容 ASCII 码中的 0 ~ 127 位字符,对于这些字符 Unicode 会将其 ASCII 码值放在低位,高位全部置为零。

Unicode 字符平面映射 把 Unicode 分为 17 个平面(Plane),每个平面拥有 2 ^ 16 = 65536 个代码点,即每个平面可以表示 65536 个字符。这 17 个平面的范围如下表:

平面编码取值范围
0 号平面U+0000 - U+FFFF
1 号平面U+10000 - U+1FFFF
2 号平面U+20000 - U+2FFFF
3 号平面U+30000 - U+3FFFF
4 号平面 - 13 号平面U+40000 - U+DFFFF
14 号平面U+E0000 - U+EFFFF
15 号平面U+F0000 - U+FFFFF
16 号平面U+100000 - U+10FFFF

作为使用者,我们并不需要了解 Unicode 是如何设计并对字符进行分类的,我们只需要知道在 Unicode 中一个字符对应了一个唯一的编码就可以了,这个唯一编码叫做code point,其实就是该字符在编码表中的位置下标。

utf-8 编码

utf-8 是 Unicode 的一种变长度的编码表达方式,Unicode 字符集中有相当多的字符其编码高位都是零,这浪费了很多的空间。为了节省空间,可以使用 utf-8 编码方式对 Unicode 做进一步的编码注 1。相较于 Unicode,使用 utf-8 编码可以显著的减少字符的空间占用。

utf-8 对 Unicode 编码值小于 U+FFFF 的字符编码效率较高,而对于高于 U+FFFF 的字符值使用 utf-16 编码可能是个更好的选择,好在我们常用的字符其编码值基本上都不会超过 U+FFFF,所以一般来说 utf-8 编码方式已经足够满足我们的需求。不过事实上,假设不使用 utf-8 或者 utf-16 等编码方式,而是直接使用 Unicode 编码加上使用数据压缩算法(例如 DEFLATE的方式,也能得到较好的空间节省效果。

我们已经知道了引入 utf-8 编码机制的目的是为了减少空间的占用,下面我们就详细了解一下 utf-8 的编码方式。如下是一个 utf-8 编码与 Unicode 编码的关系表

位数Unicode 范围字节数字节 1字节 2字节 3字节 4
7U+0000 - U+007F10xxxxxxx
11U+0080 - U+07FF2110xxxxx10xxxxxx
16U+0800 - U+FFFF31110xxxx10xxxxxx10xxxxxx
21U+10000 - U+1FFFFF411110xxx10xxxxxx10xxxxxx10xxxxxx

上面只列举了 utf-8 编码在 4 个字节以内的编码方式,再多的因为很少使用没有列举出来。上表中的 x 代表了 utf-8 编码中的编码值,可以根据字符计算得到。

根据上表我们可以得到 utf-8 编码四种情况:

  1. 第一个 bit 为 0,则说明这个字符只占用一个字节,字符的值为后 7 个 bit
  2. 前三个 bit 为 110,则说明这个字符占用两个字节,字符的值为第一个字节的后 5 个 bit 加上第二个字节的后 6 个 bit
  3. 前四个 bit 为 1110,该字符占用三个字节,字符的值为 字节一 [4:] + 字节二 [2:] + 字节三 [2:]
  4. 前五个 bit 为 11110,该字符占用四个字节,字符的值为 字节一 [5:] + 字节二 [2:] + 字节三 [2:] + 字节四 [2:]

下面我们看一个实际的 utf-8 和 unicode 相互转化的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package main

import (
"errors"
"fmt"
)

// 将输入的utf-8字符依次转化为其unicode
func utf8ToUnicode(words string) ([]uint32, error) {
// 对字节做移位处理。基础字节,andByte用于剔除无用的bit,leftMoveBit代表左移位数
cal := func(baseByte byte, andByte byte, leftMoveBit uint32) uint32 {
return uint32(baseByte&andByte) << leftMoveBit
}

bytes := []byte(words)
offset := 0
length := len(bytes)

unicodeList := make([]uint32, 0) // 存储最终的unicode列表

for offset < length {
var codePoint uint32 // 字符集中的字符位置下标,即unicode码
codeUnit := bytes[offset] // 最小编码单位,即第一个byte

// 下面这段代码比较机械,参考上面的编码表即可理解
if codeUnit>>7 == 0 {
// 一个字节
codePoint = uint32(codeUnit)
offset += 1
} else if codeUnit>>5 == 0x6 {
// 两个字节
codePoint = cal(bytes[offset], 0x1f, 6) +
cal(bytes[offset+1], 0x3f, 0)
offset += 2
} else if codeUnit>>4 == 0xe {
// 三个字节
codePoint = cal(bytes[offset], 0xf, 12) +
cal(bytes[offset+1], 0x3f, 6) +
cal(bytes[offset+2], 0x3f, 0)
offset += 3
} else if codeUnit>>3 == 0x1e {
// 四个字节
codePoint = cal(bytes[offset], 0x7, 18) +
cal(bytes[offset+1], 0x3f, 12) +
cal(bytes[offset+2], 0x3f, 6) +
cal(bytes[offset+3], 0x3f, 0)
offset += 4
} else {
return nil, errors.New("非法的utf-8字符")
}
unicodeList = append(unicodeList, codePoint)
}
return unicodeList, nil
}

// 将输入的unicode转化为其对应的utf-8编码
func unicodeToUtf8(code uint32) ([]byte, error) {
bytes := make([]byte, 0)
// 同样是一段很机械的代码,实现参考上面的编码表
if code < 0x80 {
bytes = append(bytes, byte(code)&0x7f)
} else if code < 0x800 {
bytes = append(bytes, byte(code>>6)&0x1f|0xc0)
bytes = append(bytes, byte(code)&0x3f|0x80)
} else if code < 0x10000 {
bytes = append(bytes, byte(code>>12)&0xf|0xe0)
bytes = append(bytes, byte(code>>6)&0x3f|0x80)
bytes = append(bytes, byte(code)&0x3f|0x80)
} else if code < 0x20000 {
bytes = append(bytes, byte(code>>18)&0x7|0xf0)
bytes = append(bytes, byte(code>>12)&0x3f|0x80)
bytes = append(bytes, byte(code>>6)&0x3f|0x80)
bytes = append(bytes, byte(code)&0x3f|0x80)
} else {
return nil, errors.New(fmt.Sprintf("0x%x can't be convert to utf-8", code))
}
return bytes, nil
}

func main() {
testSet := "Ƞ南京👉上海12aA北京の𐀉☺©🐵😆😄Ⅷ🖤" // golang默认使用utf-8对源代码文件进行编码
unicodeList, _ := utf8ToUnicode(testSet)
for _, unicode := range unicodeList {
result, err := unicodeToUtf8(unicode)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("0x%x %s\n", unicode, string(result))
}
}
}

总结

utf-8 作为现在最广泛使用的编码方式,了解一下其工作原理还是有必要的。utf-8 离不开 unicode,utf-8 编码出现的目的是为对 unicode 的编码值进行压缩,这才是 utf-8 编码的最核心意义。

  1. 这里有点绕,Unicode 和 utf-8 都是编码方式,我们可以这样对它们作区分:
    • Unicode 是一种表驱动的编码方式,即编码值是通过查表的方式获得的
    • utf-8 是一种通过计算而编码的方式,我们可以通过计算来实现一个字符的 utf-8 编码与 Unicode 编码的相互转化

参考

十分钟搞清字符集和字符编码
Emoji 与 Unicode