Unicode 的编码范围为 0~0x10FFFF
,如此大的范围,显然没办法像 ASCII 编码一样使用一个字节存储。为此,Unicode 制定了各种储存编码的方式,如:UTF-8
、UTF-16
和 UTF-32
,这些存储格式被称为 Unicode 转换格式 UTF
。
每种 Unicode 转换格式都会把一个编码存储为一到多个编码单元,如 UTF-8
的编码单元为 8 位的字节;UTF-16
的编码单元为 16 位,即 2 个字节;UTF-32
的编码单元为 32 位,即 4 个字节。
其中,UTF-8
是在互联网上使用最广泛的一种 Unicode 转换格式,具有以下显著的优势。下面,我们就先来看看 UTF-8
具有哪些有点吧~
UTF-8 的特点
1. UTF-8 中每个 ASCII 字符只需要一个字节去存储,因此一个 ASCII 文本本身也是一个 UTF-8 文本,即做到了向后兼容。
比如 A
的 ASCII 码对应为 0x41
,a
的 ASCII 码对应为 0x61
,那么 UTF-8 兼容 ASCII 也就意味着:
>> assert 'A'.encode('utf-8') == b'\x41'
>> assert 'a'.encode('utf-8') == b'\x61'
>>
这里,需要再次提醒一下:Unicode 是表现形式,UTF-8 是存储形式;即 UTF-8 解码之后为 Unicode ,Unicode 可以编码成 UTF-8 。
2. UTF-8 采用字节为存储单元,因此不存在字节的大端和小段的问题。
UTF-16
和 UTF-32
的存储单元分别是 2 字节和 4 字节,因此在存储时会涉及到大小端的问题。那什么是大小端模式呢?下面我们来暂停补充一下~
注:计算机中数据存储以 8 bit (位)为一个 byte (字节),比如
0x01
就是一个字节。但当我们存储的内容长度不止 8 位。比如 short 为 16 位,那就需要占用两个 byte,比如0x0001
它其实是保存在两个位置。
其中,0x00
为高位,0x01
为低位。对于大端模式 big-endian 高位放在低地址处,低位放在高地址处;小端模式 little-endian 则是高位放在高地址处,低位放在低地址处。
关于如何获知你的环境使用的是大端模式还是小端模式,这里有个简单的方式:定义一个 short
类型的数组即可:
>> import array
>> array.array('H', [1]).tostring()
b'\x01\x00'
数字 1
在 short
类型中表示为 0x0001
,高位为 0x00
,低位为 0x01
。我们可以很直观地看到,数组在保存数据时,将高位 0x00
放在了高地址处,将低位 0x01
放在了低地址处。因此使用的就是小端模式。
那 UTF-8 为什么可以使用字节来作为存储单元,而不用担心字节序的问题呢?这就涉及到了 UTF-8 巧妙的编码规则~
UTF-8 的编码规则
UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用 1~4 个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8的编码规则很简单,只有二条:
1)对于单字节符号,字节的第一位设为 0 ,后 7 位为这个符号的 Unicode 码。也就是我们上文提到的向后兼容:对于英文字母,UTF-8 编码和 ASCII 码是相同的。
2)对于使用 X
个字节存储的符号,第一个字节的前 X
位设置为 1
,第 X+1
位设置为 0
,后面字节的前 2 位一律设置为 10
,剩下的位置一次填充这个符号的 Unicode 码。
下表总结了编码规则,字母 x 表示可用于编码的位:
Unicode 符号范围(十六进制) | UTF-8 编码方式 (二进制) |
---|---|
0000 0000 - 0000 007F | 0xxxxxxx
|
0000 0080 - 0000 07FF | 110xxxxx 10xxxxxx
|
0000 0800 - 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
|
0001 0000 - 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
跟据上表,解读 UTF-8 编码也非常简单:如果一个字节的第一位是 0 ,则这个字节单独就是一个字符;如果第一位是 1 ,则连续有多少个 1 ,就表示当前字符占用多少个字节。
注:字节头部识别就是前面的
0
,110
,1110
,11110
表示字节数,因此头已经标出来了就不存在字节序问题了。
下面,我们就来演示一下 UTF-8 编码的过程。
首先,获取汉字 鱼
的 Unicode 码:
>> ord('鱼')
40060
>> bin(40060)
'0b1001110001111100'
我们不妨先对 鱼
这个汉字使用 utf-8
编码看看使用几个字节存储:
>> '鱼'.encode('utf-8')
b'\xe9\xb1\xbc'
鱼
在 UTF-8 编码中使用 3 个字节存储,因此其存储的二进制的形式为 1110xxxx 10xxxxxx 10xxxxxx
,将 Unicode 1001 110001 111100
依次填充到占位符 x
的位置就得到:11101001 10110001 10111100
。
下面,我们将上述推导得出的 11101001 10110001 10111100
转换为十六进制,验证一下是否为 b'\xe9\xb1\xbc'
:
>> hex(int('0b111010011011000110111100',2))
'0xe9b1bc'
验证无误!