计算机编码发展的历史

关于计算机系统中编码的历史(例如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
7 U+0000 - U+007F 1 0xxxxxxx
11 U+0080 - U+07FF 2 110xxxxx 10xxxxxx
16 U+0800 - U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
21 U+10000 - U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

上面只列举了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