用 Python 理清編碼問(wèn)題:Unicode萬(wàn)國(guó)碼
Python中文社區(qū) ID:python-china
Unicode——萬(wàn)國(guó)碼
為解決語(yǔ)言各自為政的編碼,人們提出了Unicode編碼方案,這個(gè)方案簡(jiǎn)單粗暴:把世界上所有語(yǔ)言字符統(tǒng)一編碼。Unicode的兩種方案UCS-2和UCS-4,可使用空間分別達(dá)到2^16和2^32個(gè):外星人到訪(fǎng)地球之前,應(yīng)該是夠用的。
我們看幾個(gè)字符的Unicode編碼碼位(code point)是怎樣的:
- ls = 'abAB鞏★☆'
- print([ord(l) for l in ls])
結(jié)果:[97, 98, 65, 66, 24041, 9733, 9734]。可見(jiàn),字母abAB的Unicode碼位和其ASCII碼位一致,所以字符為字母時(shí)兩者兼容,而漢字鞏的碼位為24041(0x5de9),與之前的GB系列編碼47534(0xb9ae)不同,所以Unicode和GB系列編碼之間是不完全兼容的:只有ASCII部分兼容。
所有國(guó)家的人都使用Unicode編碼之后,擴(kuò)展、亂碼問(wèn)題都不復(fù)存在:所有人類(lèi)語(yǔ)言字符都有了一個(gè)統(tǒng)一的編碼碼位,溝通中我們寫(xiě)出的每個(gè)數(shù)字編碼,都有唯一的字符與他對(duì)應(yīng)。Python中chr函數(shù)返回Unicode碼位對(duì)應(yīng)的字符。
- >>> print([chr(i) for i in [123,957,24041]])
- ['{', 'ν', '鞏']
那么我們可以使用強(qiáng)大的Unicode進(jìn)行編碼了么?
- >>> ls = 'abAB鞏★☆'
- >>> ls.encode('Unicode')
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module> LookupError: unknown encoding: Unicode
未知編碼Unicode!這是因?yàn)?,并不存在Unicode碼這種編碼形式,Unicode只是一個(gè)碼位表,它只是建立了字符和整數(shù)之間的映射。至于整數(shù)碼位(code point)如何存儲(chǔ)成字節(jié),先存高位低位,有沒(méi)有特殊標(biāo)志,Unicode并不直接決定,而是交給具體編碼來(lái)考慮這些細(xì)節(jié):UTF-32,UTF-16和UTF-8。
UTF-32 四字節(jié)為單位
UTF-32,顧名思義,是用32位,也就是四個(gè)字節(jié)來(lái)存儲(chǔ)一個(gè)字符的編碼方案。
- >>> 'aA鞏'.encode('utf-32LE')
- b'a\x00\x00\x00A\x00\x00\x00\xe9\x5d\x00\x00'
可見(jiàn),所有的字符,都使用了四個(gè)字節(jié)來(lái)存儲(chǔ):每個(gè)字節(jié)除了Unicode碼位之外,不足用\x00來(lái)填充。此法簡(jiǎn)單明了,Unicode碼位不用轉(zhuǎn)換,直接填充。但大量的\x00造成了極大的浪費(fèi)。有沒(méi)有辦法解決這種浪費(fèi)了?壓縮下用兩位行不行?
UTF-16 兩字節(jié)為單位
當(dāng)用UTF-16來(lái)編碼時(shí)。
- >>> 'aA鞏'.encode('utf-16LE')
- b'a\x00A\x00\xe9\x5d'
兩個(gè)字節(jié)對(duì)絕大多數(shù)Unicode碼位來(lái)說(shuō)是夠用的,不夠用的話(huà)系統(tǒng)自動(dòng)用四位表示。這是系統(tǒng)實(shí)現(xiàn),我們無(wú)需關(guān)心。UTF-16編碼后的字節(jié)序列和字符,依然能夠一一對(duì)應(yīng)起來(lái)。UTF-16其實(shí)有兩種編碼方法,分別為上例的UTF-16LE和如下的UTF-16BE,測(cè)試:
- >>> 'aA鞏'.encode('utf-16BE')
- b'\x00a\x00A\x5d\xe9'
兩者基本一樣,只是高低字節(jié)位置發(fā)生了顛倒。LE和BE后綴,表示小字節(jié)序(little endian)和大字節(jié)序(big endian)。這是計(jì)算機(jī)內(nèi)部關(guān)于字節(jié)的MSB(大權(quán)重字節(jié))放在字節(jié)的開(kāi)頭還是結(jié)尾的具體實(shí)現(xiàn)細(xì)節(jié)。
《格列佛游記》中,小人國(guó)國(guó)民為吃雞蛋先吃大頭或小頭,針?shù)h相對(duì),組成了兩個(gè)軍事對(duì)立集團(tuán)big endians和little endians,相互間多次發(fā)動(dòng)戰(zhàn)爭(zhēng)。
那么兩個(gè)字節(jié)就是Unicode編碼的極限了么?
UTF-8 變長(zhǎng)字節(jié)編碼
能不能用可變數(shù)目的字節(jié)來(lái)存儲(chǔ)文本呢?如果存儲(chǔ)的是英文文本的話(huà),每個(gè)字符只用一個(gè)字節(jié)就可以;漢字的話(huà),再進(jìn)行擴(kuò)展。如此來(lái)進(jìn)一步節(jié)省存儲(chǔ)空間。答案是可以的,這就是可變長(zhǎng)度編碼UTF-8。
- >>> 'aA鞏'.encode('utf-8')
- b'aA\xe5\xb7\xa9'
這是目前最短的字節(jié)序列,因?yàn)閍A分別存儲(chǔ)成了一個(gè)字節(jié)。需要注意的是,UTF-32和UTF-16中,鞏的字節(jié)序列是0x5de9,但在UTF-8中,字節(jié)序列變成了0xe5b7a9。這說(shuō)明UTF-8編碼不是簡(jiǎn)單地把Unicode碼位直接存儲(chǔ)進(jìn)字節(jié)序列中,而是進(jìn)行了某些轉(zhuǎn)換。這些轉(zhuǎn)換,保證了英文用一位存儲(chǔ),漢語(yǔ)等較大字符多字節(jié)存儲(chǔ)。那么是如何轉(zhuǎn)換的呢?
UTF-8 編碼轉(zhuǎn)換規(guī)則
本部分過(guò)于細(xì)節(jié),可略過(guò)。UTF-8實(shí)現(xiàn)了可變長(zhǎng)度的編碼,為解碼時(shí)區(qū)分可變長(zhǎng)度究竟多長(zhǎng),需要在字節(jié)序列里使用特殊模板。UTF-8編碼遵循以下規(guī)則:
- 0x00-0x7F之間的碼位,兼容ASCII碼,單字節(jié)直接存儲(chǔ)在以下模板 0*** ****
- 0x80-0x7ff之間,使用兩個(gè)字節(jié)存儲(chǔ),字節(jié)模板是110* **** 10** ****
- 0x800-0xffff之間,使用三個(gè)字節(jié)存儲(chǔ),字節(jié)模板是1110 **** 10** **** 10** ****
- 0x10000-0x1fffff之間,使用四個(gè)字節(jié)存儲(chǔ),字節(jié)模板是1111 0*** 10** **** 10** **** 10** ****
以漢字鞏為例,其Unicode碼位為0x6c49,二進(jìn)制位110 1100 0100 1001。位于第三行范圍,所以需要三個(gè)字節(jié)來(lái)存儲(chǔ),寫(xiě)出模板,1110 **** 10** **** 10** ****,使用二進(jìn)制,從右向左填充,不足部分補(bǔ)零,可得結(jié)果1110 0110 1011 0001 1000 1001,十六進(jìn)制為0xe6 0xb7 0x89,所以鞏編碼為UTF-8的字節(jié)序列形式為0Xe6b789。讓我們從UTF-8編碼轉(zhuǎn)換細(xì)節(jié)中,回到UTF三種編碼的長(zhǎng)度問(wèn)題上來(lái)。
UTF三種編碼后的長(zhǎng)度
以上三種編碼方式,由于壓縮率不用,導(dǎo)致文件長(zhǎng)度也不同,以下程序比較當(dāng)文本為漢字和英語(yǔ)內(nèi)容時(shí),三種不同編碼的長(zhǎng)度:
- es = 'abcdefghij'
- cs = '莫愁前路無(wú)知己,天下誰(shuí)人不識(shí)君。'
- codes = ['utf-32le','utf-16le','utf-8']
- print([len(es.encode(code)) for code in codes])
- print([len(cs.encode(code)) for code in codes])
輸出為 [40, 20, 10] [64, 32, 48] 可見(jiàn),對(duì)于英文來(lái)說(shuō),UTF-8比UTF-16和UTF-32編碼都要有優(yōu)勢(shì);對(duì)漢字來(lái)說(shuō),最有優(yōu)勢(shì)的反而是UTF16編碼。這是因?yàn)閁TF-16編碼中,大部分漢字采用2Byte存儲(chǔ),而UTF-8中漢字需要三個(gè)字節(jié)存儲(chǔ)。在日常生活中,因?yàn)榭紤]到最大兼容性,UTF-8使用的最為廣泛。至此,我們從ASCII碼到GB系列編碼,再到Unicode和相應(yīng)的UTF系列編碼,一路進(jìn)化,擁有了一個(gè)包羅萬(wàn)碼,不會(huì)亂碼和有較高壓縮率的字符編碼系統(tǒng)??梢允褂昧嗣?沒(méi)有!因?yàn)槲覀冎皇蔷幋a了文本自身,并沒(méi)有記載具體用了那個(gè)編碼:當(dāng)我們發(fā)送一份文件后,除非告訴對(duì)方,否則對(duì)方不知道應(yīng)該該用什么編碼打開(kāi)它。解決這個(gè)問(wèn)題,我們留待下篇文章分析。
總結(jié)
- Unicode統(tǒng)一了世界各語(yǔ)言字符。Unicode幾種編碼形式中;
- UTF-32簡(jiǎn)單,但浪費(fèi)嚴(yán)重。
- UTF-16使用兩個(gè)字節(jié)為單位存儲(chǔ),節(jié)省了空間。
- UTF-8使用一個(gè)字節(jié)直接存儲(chǔ),是效率、空間的平衡。