Unicode 的編碼范圍為 0~0x10FFFF ,如此大的范圍,顯然沒辦法像 ASCII 編碼一樣使用一個(gè)字節(jié)存儲(chǔ)。為此,Unicode 制定了各種儲(chǔ)存編碼的方式,如:UTF-8、UTF-16 和 UTF-32 ,這些存儲(chǔ)格式被稱為 Unicode 轉(zhuǎn)換格式 UTF 。
每種 Unicode 轉(zhuǎn)換格式都會(huì)把一個(gè)編碼存儲(chǔ)為一到多個(gè)編碼單元,如 UTF-8 的編碼單元為 8 位的字節(jié);UTF-16 的編碼單元為 16 位,即 2 個(gè)字節(jié);UTF-32 的編碼單元為 32 位,即 4 個(gè)字節(jié)。
其中,UTF-8 是在互聯(lián)網(wǎng)上使用最廣泛的一種 Unicode 轉(zhuǎn)換格式,具有以下顯著的優(yōu)勢。下面,我們就先來看看 UTF-8 具有哪些有點(diǎn)吧~
UTF-8 的特點(diǎn)
1. UTF-8 中每個(gè) ASCII 字符只需要一個(gè)字節(jié)去存儲(chǔ),因此一個(gè) ASCII 文本本身也是一個(gè) UTF-8 文本,即做到了向后兼容。
比如 A 的 ASCII 碼對(duì)應(yīng)為 0x41 ,a 的 ASCII 碼對(duì)應(yīng)為 0x61 ,那么 UTF-8 兼容 ASCII 也就意味著:
>> assert 'A'.encode('utf-8') == b'\x41'
>> assert 'a'.encode('utf-8') == b'\x61'
>>
這里,需要再次提醒一下:Unicode 是表現(xiàn)形式,UTF-8 是存儲(chǔ)形式;即 UTF-8 解碼之后為 Unicode ,Unicode 可以編碼成 UTF-8 。
2. UTF-8 采用字節(jié)為存儲(chǔ)單元,因此不存在字節(jié)的大端和小段的問題。
UTF-16 和 UTF-32 的存儲(chǔ)單元分別是 2 字節(jié)和 4 字節(jié),因此在存儲(chǔ)時(shí)會(huì)涉及到大小端的問題。那什么是大小端模式呢?下面我們來暫停補(bǔ)充一下~
注:計(jì)算機(jī)中數(shù)據(jù)存儲(chǔ)以 8 bit (位)為一個(gè) byte (字節(jié)),比如
0x01就是一個(gè)字節(jié)。但當(dāng)我們存儲(chǔ)的內(nèi)容長度不止 8 位。比如 short 為 16 位,那就需要占用兩個(gè) byte,比如0x0001它其實(shí)是保存在兩個(gè)位置。
其中,0x00為高位,0x01為低位。對(duì)于大端模式 big-endian 高位放在低地址處,低位放在高地址處;小端模式 little-endian 則是高位放在高地址處,低位放在低地址處。
關(guān)于如何獲知你的環(huán)境使用的是大端模式還是小端模式,這里有個(gè)簡單的方式:定義一個(gè) short 類型的數(shù)組即可:
>> import array
>> array.array('H', [1]).tostring()
b'\x01\x00'
數(shù)字 1 在 short 類型中表示為 0x0001 ,高位為 0x00 ,低位為 0x01 。我們可以很直觀地看到,數(shù)組在保存數(shù)據(jù)時(shí),將高位 0x00 放在了高地址處,將低位 0x01 放在了低地址處。因此使用的就是小端模式。
那 UTF-8 為什么可以使用字節(jié)來作為存儲(chǔ)單元,而不用擔(dān)心字節(jié)序的問題呢?這就涉及到了 UTF-8 巧妙的編碼規(guī)則~
UTF-8 的編碼規(guī)則
UTF-8 最大的一個(gè)特點(diǎn),就是它是一種變長的編碼方式。它可以使用 1~4 個(gè)字節(jié)表示一個(gè)符號(hào),根據(jù)不同的符號(hào)而變化字節(jié)長度。UTF-8的編碼規(guī)則很簡單,只有二條:
1)對(duì)于單字節(jié)符號(hào),字節(jié)的第一位設(shè)為 0 ,后 7 位為這個(gè)符號(hào)的 Unicode 碼。也就是我們上文提到的向后兼容:對(duì)于英文字母,UTF-8 編碼和 ASCII 碼是相同的。
2)對(duì)于使用 X 個(gè)字節(jié)存儲(chǔ)的符號(hào),第一個(gè)字節(jié)的前 X 位設(shè)置為 1 ,第 X+1 位設(shè)置為 0 ,后面字節(jié)的前 2 位一律設(shè)置為 10 ,剩下的位置一次填充這個(gè)符號(hào)的 Unicode 碼。
下表總結(jié)了編碼規(guī)則,字母 x 表示可用于編碼的位:
| Unicode 符號(hào)范圍(十六進(jìn)制) | UTF-8 編碼方式 (二進(jìn)制) |
|---|---|
| 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
|
跟據(jù)上表,解讀 UTF-8 編碼也非常簡單:如果一個(gè)字節(jié)的第一位是 0 ,則這個(gè)字節(jié)單獨(dú)就是一個(gè)字符;如果第一位是 1 ,則連續(xù)有多少個(gè) 1 ,就表示當(dāng)前字符占用多少個(gè)字節(jié)。
注:字節(jié)頭部識(shí)別就是前面的
0,110,1110,11110表示字節(jié)數(shù),因此頭已經(jīng)標(biāo)出來了就不存在字節(jié)序問題了。
下面,我們就來演示一下 UTF-8 編碼的過程。
首先,獲取漢字 魚 的 Unicode 碼:
>> ord('魚')
40060
>> bin(40060)
'0b1001110001111100'
我們不妨先對(duì) 魚 這個(gè)漢字使用 utf-8 編碼看看使用幾個(gè)字節(jié)存儲(chǔ):
>> '魚'.encode('utf-8')
b'\xe9\xb1\xbc'
魚 在 UTF-8 編碼中使用 3 個(gè)字節(jié)存儲(chǔ),因此其存儲(chǔ)的二進(jìn)制的形式為 1110xxxx 10xxxxxx 10xxxxxx,將 Unicode 1001 110001 111100 依次填充到占位符 x 的位置就得到:11101001 10110001 10111100。
下面,我們將上述推導(dǎo)得出的 11101001 10110001 10111100 轉(zhuǎn)換為十六進(jìn)制,驗(yàn)證一下是否為 b'\xe9\xb1\xbc' :
>> hex(int('0b111010011011000110111100',2))
'0xe9b1bc'
驗(yàn)證無誤!