摘要:字符編碼表,碼位碼元將編碼字符集中的碼位轉(zhuǎn)換成有限比特長(zhǎng)度的整型值的序列。字符編碼方案,碼元序列化也稱為常說(shuō)的序列化。每個(gè)字節(jié)里的二進(jìn)制數(shù)就是字節(jié)序列。另一個(gè)情況則是壓縮字節(jié)序列的值,如或進(jìn)程長(zhǎng)度編碼等無(wú)損壓縮技術(shù)。
《流暢的Python》筆記。1. 前言
本篇主要講述不同編碼之間的轉(zhuǎn)換問(wèn)題,比較繁雜,如果平時(shí)處理文本不多,或者語(yǔ)言比較單一,沒(méi)有多語(yǔ)言文本處理的需求,則可以略過(guò)此篇。
本篇主要講述Python對(duì)文本字符串的處理。主要內(nèi)容如下:
字符集基本概念以及Unicode;
Python中的字節(jié)序列;
Python對(duì)編碼錯(cuò)誤的處理以及BOM;
Python對(duì)文本文件的編解碼,以及對(duì)Unicode字符的比較和排序,而這便是本篇的主要目的;
雙模式API和Unicode數(shù)據(jù)庫(kù)
如果對(duì)字符編碼很熟悉,也可直接跳過(guò)第2節(jié)。
2. 字符集相關(guān)概念筆者在初學(xué)字符集相關(guān)內(nèi)容的時(shí)候,對(duì)這個(gè)概念并沒(méi)有什么疑惑:字符集嘛,就是把我們?nèi)粘J褂玫淖址h子,英文,符號(hào),甚至表情等)轉(zhuǎn)換成為二進(jìn)制嘛,和摩斯電碼本質(zhì)上沒(méi)啥區(qū)別,用數(shù)學(xué)的觀點(diǎn)就是一個(gè)函數(shù)變換,這有什么好疑惑的?直到后來(lái)越來(lái)也多地接觸字符編碼,終于,筆者被這堆概念搞蒙了:一會(huì)兒Unicode編碼,一會(huì)兒又Unicode字符集,UTF-8編碼,UTF-16字符集還有什么字符編碼、字節(jié)序列。到底啥時(shí)候該叫“編碼”,啥時(shí)候該叫“字符集”?這些概念咋這么相似呢?既然這么相似,干嘛取這么多名字?后來(lái)仔細(xì)研究后發(fā)現(xiàn),確實(shí)很多學(xué)術(shù)名次都是同義詞,比如“字符集”和“字符編碼”其實(shí)就是同義詞;有的譯者又在翻譯外國(guó)的書(shū)的時(shí)候,無(wú)意識(shí)地把一個(gè)概念給放大或者給縮小了。
說(shuō)到這不得不吐槽一句,我們國(guó)家互聯(lián)網(wǎng)相關(guān)的圖書(shū)質(zhì)量真的低。國(guó)人自己寫(xiě)的IT方面的書(shū),都不求有多經(jīng)典,能稱為好書(shū)的都少之又少;而翻譯的書(shū),要么翻譯得晦澀難懂,還不如直接看原文;要么故作風(fēng)騷,非得體現(xiàn)譯者的文學(xué)修養(yǎng)有多“高”;要么生造名詞,同一概念同一單詞,這本書(shū)里你翻譯成這樣,另一本書(shū)里我就偏要翻譯成那樣(你們這是在翻譯小說(shuō)嗎)。所以勸大家有能力的話還是直接看原文吧,如果要買(mǎi)譯本,還請(qǐng)大家認(rèn)真比較比較,否則讀起來(lái)真的很痛苦。
回到主題,我們繼續(xù)討論字符集相關(guān)問(wèn)題。翻閱網(wǎng)上大量資料,做出如下總結(jié)。
2.1 基本概念始終記住編碼的核心思想:就是給每個(gè)字符都對(duì)應(yīng)一個(gè)二進(jìn)制序列,其他的所有工作都是讓這個(gè)過(guò)程更規(guī)范,更易于管理。
現(xiàn)代編碼模型將這個(gè)過(guò)程分了5個(gè)層次,所用的術(shù)語(yǔ)列舉如下(為了避免混淆,這里不再列出它們的同義詞):
抽象字符表(Abstract character repertoire):
系統(tǒng)支持的所有抽象字符的集合??梢院?jiǎn)單理解為人們使用的文字、符號(hào)等。
這里需要注意一個(gè)問(wèn)題:有些語(yǔ)系里面的字母上方或者下方是帶有特殊符號(hào)的,比如一點(diǎn)或者一撇;有的字符表里面會(huì)將字母和特殊符號(hào)組合成一個(gè)新的字符,為它多帶帶編碼;有的則不會(huì)多帶帶編碼,而是字母賦予一個(gè)編碼,特殊符號(hào)賦予一個(gè)編碼,然后當(dāng)這倆在文中相遇的時(shí)候再將這倆的編碼組合起來(lái)形成一個(gè)字符。后面我們會(huì)談到這個(gè)問(wèn)題,這也是以前字符編碼轉(zhuǎn)換常出毛病的一個(gè)原因。
提醒:雖然這里扯到了編碼,但抽象字符表這個(gè)概念還和編碼沒(méi)有聯(lián)系。
編碼字符集(Coded Character Set,CCS):字符 --> 碼位
首先給出總結(jié):編碼字符集就是用數(shù)字代替抽象字符集中的每一個(gè)字符!
將抽象字符表中的每一個(gè)字符映射到一個(gè)坐標(biāo)(整數(shù)值對(duì):(x, y),比如我國(guó)的GBK編碼)或者表示為一個(gè)非負(fù)整數(shù)N,便生成了編碼字符集。與之相應(yīng)的還有兩個(gè)抽象概念:編碼空間(encoding space)、碼位(code point)和碼位值(code point value)。
簡(jiǎn)單的理解,編碼空間就相當(dāng)于許多空位的集合,這些空位稱之為碼位,而這個(gè)碼位的坐標(biāo)通常就是碼位值。我們將抽象字符集中的字符與碼位一一對(duì)應(yīng),然后用碼位值來(lái)代表字符。以二維空間為例,相當(dāng)于我們有一個(gè)10萬(wàn)行的表,每一行相當(dāng)于一個(gè)碼位,二維的情況下,通常行號(hào)就是碼位值(當(dāng)然你也可以設(shè)置為其他值),然后我們把每個(gè)漢字放到這個(gè)表中,最后用行號(hào)來(lái)表示每一個(gè)漢字。一個(gè)編碼字符集就是把抽象字符映射為碼位值。這里區(qū)分碼位和碼位值只是讓這個(gè)映射的過(guò)程更形象,兩者類似于座位和座位號(hào)的區(qū)別,但真到用時(shí),并不區(qū)分這兩者,以下兩種說(shuō)法是等效的:
字符A的碼位是123456 字符A的碼位值是123456(很少這么說(shuō),但有這種說(shuō)法)
編碼空間并不只能是二維的,它也可以是三維的,甚至更高,比如當(dāng)你以二維坐標(biāo)(x, y)來(lái)編號(hào)字符,并且還對(duì)抽象字符集進(jìn)行了分類,那么此時(shí)的編碼空間就可能是三維的,z坐標(biāo)表示分類,最終使用(x, y, z)在這個(gè)編碼空間中來(lái)定位字符。不過(guò)筆者還沒(méi)真見(jiàn)過(guò)(或者見(jiàn)過(guò)但不知道......)三維甚至更高維的編碼,最多也就見(jiàn)過(guò)變相的三維編碼空間。但編碼都是人定的,你也可以自己定一個(gè)編碼規(guī)則~~
并不是每一個(gè)碼位都會(huì)被使用,比如我們的漢字有8萬(wàn)多個(gè),用10萬(wàn)個(gè)數(shù)字來(lái)編號(hào)的話還會(huì)剩余1萬(wàn)多個(gè),這些剩余的碼位則留作擴(kuò)展用。
注意:到這一步我們只是將抽象字符集進(jìn)行了編號(hào),但這個(gè)編號(hào)并不一定是二進(jìn)制的,而且它一般也不是二進(jìn)制的,而是10進(jìn)制或16進(jìn)制。該層依然是個(gè)抽象層。
而這里之所以說(shuō)了這么多,就是為了和下面這個(gè)概念區(qū)分。
字符編碼表(Character Encoding Form,CEF):碼位 --> 碼元
將編碼字符集中的碼位轉(zhuǎn)換成有限比特長(zhǎng)度的整型值的序列。這個(gè)整型值的單位叫碼元(code unit)。即一個(gè)碼位可由一個(gè)或多個(gè)碼元表示。而這個(gè)整型值通常就是碼位的二進(jìn)制表示。
到這里才完成了字符到二進(jìn)制的轉(zhuǎn)換。程序員的工作通常到這里就完成了。但其實(shí)還有后續(xù)兩步。
注意:直到這里都還沒(méi)有將這些序列存到存儲(chǔ)器中!所以這里依然是個(gè)抽象,只是相比上面兩步更具體而已。
字符編碼方案(Character Encoding Scheme,CES):碼元 --> 序列化
也稱為“serialization format”(常說(shuō)的“序列化”)。將上面的整型值轉(zhuǎn)換成可存儲(chǔ)或可傳輸8位字節(jié)序列。簡(jiǎn)單說(shuō)就是將上面的碼元一個(gè)字節(jié)一個(gè)字節(jié)的存儲(chǔ)或傳輸。每個(gè)字節(jié)里的二進(jìn)制數(shù)就是字節(jié)序列。這個(gè)過(guò)程中還會(huì)涉及大小端模式的問(wèn)題(碼元的低位字節(jié)里的內(nèi)容放在內(nèi)存地址的高位還是低位的問(wèn)題,感興趣的請(qǐng)自行查閱,這里不再贅述)。
直到這時(shí),才真正完成了從我們使用的字符轉(zhuǎn)換到機(jī)器使用的二進(jìn)制碼的過(guò)程。 抽象終于完成了實(shí)例化。
傳輸編碼語(yǔ)法(transfer encoding syntax):
這里則主要涉及傳輸?shù)膯?wèn)題,如果用計(jì)算機(jī)網(wǎng)絡(luò)的概念來(lái)類比的話,就是如何實(shí)現(xiàn)透明傳輸。相當(dāng)于將上面的字節(jié)序列的值映射到一個(gè)更受限的值域內(nèi),以滿足傳輸環(huán)境的限制。比如Email的Base64或quoted-printable協(xié)議,Base64是6bit作為一個(gè)單位,quoted-printable是7bit作為一個(gè)單位,所以我們得想辦法把8bit的字節(jié)序列映射到6bit或7bit的單位中。另一個(gè)情況則是壓縮字節(jié)序列的值,如LZW或進(jìn)程長(zhǎng)度編碼等無(wú)損壓縮技術(shù)。
綜上,整個(gè)編碼過(guò)程概括如下:
字符 --> 碼位 --> 碼元 --> 序列化,如果還要在特定環(huán)境傳輸,還需要再映射。從左到右是編碼的過(guò)程,從右到左就是解碼的過(guò)程。
下面我們以Unicode為例,來(lái)更具體的說(shuō)明上述概念。
2.2 統(tǒng)一字符編碼Unicode每個(gè)國(guó)家每個(gè)地區(qū)都有自己的字符編碼標(biāo)準(zhǔn),如果你開(kāi)發(fā)的程序是面向全球的,則不得不在這些標(biāo)準(zhǔn)之間轉(zhuǎn)換,而許多問(wèn)題就出在這些轉(zhuǎn)換上。Unicode的初衷就是為了避免這種轉(zhuǎn)換,而對(duì)全球各種語(yǔ)言進(jìn)行統(tǒng)一編碼。既然都在同一個(gè)標(biāo)準(zhǔn)下進(jìn)行編碼,那就不存在轉(zhuǎn)換的問(wèn)題了唄。但這只是理想,至今都沒(méi)編完,所以還是有轉(zhuǎn)換的問(wèn)題,但已經(jīng)極大的解決了以前的編碼轉(zhuǎn)換的問(wèn)題了。
Unicode編碼就是上面的編碼字符集CCS。而與它相伴的則是經(jīng)常用到的UTF-8,UTF-16等,這些則是上面的字符編碼表CEF。
最新版的Unicode庫(kù)已經(jīng)收錄了超過(guò)10萬(wàn)個(gè)字符,它的碼位一般用16進(jìn)制表示,并且前面還要加上U+,十進(jìn)制表示的話則是前面加,例如字母“A”的Unicode碼位是U+0041,十進(jìn)制表示為A。
Unicode目前一共有17個(gè)Plane(面),從U+0000到U+10FFFF,每個(gè)Plane包含65536(=2^16^)個(gè)碼位,比如英文字符集就在0號(hào)平面中,它的范圍是U+0000 ~ U+FFFF。這17個(gè)Plane中4號(hào)到13號(hào)都還未使用,而15、16號(hào)Plane保留為私人使用區(qū),而使用的5個(gè)Plane也并沒(méi)有全都用完,所以Unicode還沒(méi)有很大的未編碼空間,相當(dāng)長(zhǎng)的時(shí)間內(nèi)夠用了。
注意:自2003年起,Unicode的編碼空間被規(guī)范為了21bit,但Unicode編碼并沒(méi)有占多少位之說(shuō),而真正涉及到在存儲(chǔ)器中占多少位時(shí),便到了字符編碼階段,即UTF-8,UTF-16,UTF-32等,這些字符編碼表在編程中也叫做編解碼器。
UTF-n表示用n位作為碼元來(lái)編碼Unicode的碼位。以UTF-8為例,它的碼元是1字節(jié),且最多用4個(gè)碼元為Unicode的碼位進(jìn)行編碼,編碼規(guī)則如下表所示:
表中的×用Unicode的16進(jìn)制碼位的2進(jìn)制序列從右向左依次替換,比如U+07FF的二進(jìn)制序列為 :00000,11111,111111(這里的逗號(hào)位置只是為了和后面作比較,并不是正確的位置);
那么U+07FF經(jīng)UTF-8編碼后的比特序列則為110 11111,10 111111,暫時(shí)將這個(gè)序列命名為a。
至此已經(jīng)完成了前3步工作,現(xiàn)在開(kāi)始執(zhí)行序列化:
如果CPU是大端模式,那么序列a就是U+07FF在機(jī)器中的字節(jié)序列,但如果是小端模式,序列a的這兩個(gè)字節(jié)需要調(diào)換位置,變?yōu)?b>10 111111,110 11111,這才是實(shí)際的字節(jié)序列。
3. Python中的字節(jié)序列Python3明確區(qū)分了人類可讀的字符串和原始的字節(jié)序列。Python3中,文本總是Unicode,由str類型表示,二進(jìn)制數(shù)據(jù)由bytes類型表示,并且Python3不會(huì)以任何隱式的方式混用str和bytes。Python3中的str類型基本相當(dāng)于Python2中的unicode類型。
Python3內(nèi)置了兩種基本的二進(jìn)制序列類型:不可變bytes類型和可變bytearray類型。這兩個(gè)對(duì)象的每個(gè)元素都是介于0-255之間的整數(shù),而且它們的切片始終是同一類型的二進(jìn)制序列(而不是單個(gè)元素)。
以下是關(guān)于字節(jié)序列的一些基本操作:
>>> "China".encode("utf8") # 也可以 temp = bytes("China", encoding="utf_8") b"China" >>> a = "中國(guó)" >>> utf = a.encode("utf8") >>> utf b"xe4xb8xadxe5x9bxbd" >>> a "中國(guó)" >>> len(a) 2 >>> len(utf) 6 >>> utf[0] 228 >>> utf[:1] b"xe4" >>> b = bytearray("China", encoding="utf8") # 也可以b = bytearray(utf) >>> b bytearray(b"China") >>> b[-1:] bytearray(b"a")
二進(jìn)制序列實(shí)際是整數(shù)序列,但在輸出時(shí)為了方便閱讀,將其進(jìn)行了轉(zhuǎn)換,以b開(kāi)頭,其余部分:
可打印的ASCII范圍內(nèi)的字節(jié),使用ASCII字符本身;
制表符、換行符、回車符和對(duì)應(yīng)的字節(jié),使用轉(zhuǎn)義序列 , , 和;
其他字節(jié)的值,使用十六進(jìn)制轉(zhuǎn)義序列,以x開(kāi)頭。
bytes和bytesarray的構(gòu)造方法如下:
一個(gè)str對(duì)象和一個(gè)encoding關(guān)鍵字參數(shù);
一個(gè)可迭代對(duì)象,值的范圍是range(256);
一個(gè)實(shí)現(xiàn)了緩沖協(xié)議的對(duì)象(如bytes,bytearray,memoryview,array.array),此時(shí)它將源對(duì)象中的字節(jié)序列復(fù)制到新建的二進(jìn)制序列中。并且,這是一種底層操作,可能涉及類型轉(zhuǎn)換。
除了格式化方法(format和format_map)和幾個(gè)處理Unicode數(shù)據(jù)的方法外,bytes和bytearray都支持str的其他方法,例如bytes. endswith,bytes.replace等。同時(shí),re模塊中的正則表達(dá)式函數(shù)也能處理二進(jìn)制序列(當(dāng)正則表達(dá)式編譯自二進(jìn)制序列時(shí)會(huì)用到)。
二進(jìn)制序列有個(gè)str沒(méi)有的方法fromhex,它解析十六進(jìn)制數(shù)字對(duì),構(gòu)件二進(jìn)制序列:
>>> bytes.fromhex("31 4b ce a9") b"1Kxcexa9"
補(bǔ)充:struct模塊提供了一些函數(shù),這些函數(shù)能把打包的字節(jié)序列轉(zhuǎn)換成不同類型字段組成的元組,或者相反,把元組轉(zhuǎn)換成打包的字節(jié)序列。struct模塊能處理bytes、bytearray和memoryview對(duì)象。這個(gè)不是本篇重點(diǎn),不再贅述。
4. 編解碼器問(wèn)題如第2節(jié)所述,我們常說(shuō)的UTF-8,UTF-16實(shí)際上是字符編碼表,在編程中一般被稱為編解碼器。本節(jié)主要講述關(guān)于編解碼器的錯(cuò)誤處理:UnicodeEncodeError,UnicodeDecodeError和SyntaxError。
Python中一般會(huì)明確的給出某種錯(cuò)誤,而不會(huì)籠統(tǒng)地拋出UnicodeError,所以,在我們自行編寫(xiě)處理異常的代碼時(shí),也最好明確錯(cuò)誤類型。
4.1 UnicodeEncodeError當(dāng)從文本轉(zhuǎn)換成字節(jié)序列時(shí),如果編解碼器沒(méi)有定義某個(gè)字符,則有可能拋出UnicodeEncodeError。
>>> country = "中國(guó)" >>> country.encode("utf8") b"xe4xb8xadxe5x9bxbd" >>> country.encode("utf16") b"xffxfe-NxfdV" >>> country.encode("cp437") Traceback (most recent call last): File "", line 1, inFile "E:CodePythonStudyvenvlibencodingscp437.py", line 12, in encode return codecs.charmap_encode(input,errors,encoding_map) UnicodeEncodeError: "charmap" codec can"t encode characters in position 0-1: character maps to
可以指定錯(cuò)誤處理方式:
>>> country.encode("cp437", errors="ignore") # 跳過(guò)無(wú)法編碼的字符,不推薦 b"" >>> country.encode("cp437", errors="replace") # 把無(wú)法編碼的字符替換成“?” b"??" >>> country.encode("cp437", errors="xmlcharrefreplace") # 把無(wú)法編碼的字符替換成XML實(shí)體 b"中国"4.2 UnicodeDecodeError
相應(yīng)的,當(dāng)從字節(jié)序列轉(zhuǎn)換成文本時(shí),則有可能發(fā)生UnicodeDecodeError。
>>> octets.decode("cp1252") "Montréal" >>> octets.decode("iso8859_7") "Montrιal" >>> octets.decode("utf_8") Traceback (most recent call last): File "", line 1, in4.3 SyntaxErrorUnicodeDecodeError: "utf-8" codec can"t decode byte 0xe9 in position 5: invalid continuation byte # 解碼錯(cuò)誤的處理與4.1類似 >>> octets.decode("utf8", errors="replace") # "?"字符是官方指定的替換字符(REPLACEMENT CHARACTER),表示未知字符,碼位U+FFFD "Montr?al"
當(dāng)加載Python模塊時(shí),如果源碼的編碼與文件解碼器不符時(shí),則會(huì)出現(xiàn)SyntaxError。比如Python3默認(rèn)UTF-8編碼源碼,如果你的Python源碼編碼時(shí)使用的是其他編碼,而代碼中又沒(méi)有聲明編解碼器,那么Python解釋器可能就會(huì)發(fā)出SyntaxError。為了修正這個(gè)問(wèn)題,可在文件開(kāi)頭指明編碼類型,比如表明編碼為UTF-8,則應(yīng)在源文件頂部寫(xiě)下此行代碼:#-*- coding: utf8 -*- ”(沒(méi)有引號(hào)?。?/p>
補(bǔ)充:Python3允許在源碼中使用非ASCII標(biāo)識(shí)符,也就是說(shuō),你可以用中文來(lái)命名變量(笑。。。)。如下:
>>> 甲="abc" >>> 甲 "abc"
但是極不推薦!還是老老實(shí)實(shí)用英文吧,哪怕拼音也行。
4.4 找出字節(jié)序列的編碼有時(shí)候一個(gè)文件并沒(méi)有指明編碼,此時(shí)該如何確定它的編碼呢?實(shí)際并沒(méi)有100%確定編碼類型的方法,一般都是靠試探和分析找出編碼。比如,如果b"x00"字節(jié)經(jīng)常出現(xiàn),就很有可能是16位或32位編碼,而不是8位編碼。Chardet就是這樣工作的。它是一個(gè)Python庫(kù),能識(shí)別所支持的30種編碼。以下是它的用法,這是在終端命令行中,不是在Python命令行中:
$ chardetect 04-text-byte.asciidoc 04-text-byte.asciidoc: utf-8 with confidence 0.994.5 字節(jié)序標(biāo)記BOM(byte-order mark)
當(dāng)使用UTF-16編碼時(shí),字節(jié)序列前方會(huì)有幾個(gè)額外的字節(jié),如下:
>>> "El Ni?o".encode("utf16") b"xffxfeEx00lx00 x00Nx00ix00xf1x00ox00" # 注意前兩個(gè)字節(jié)b"xffxfe"
BOM用于指明編碼時(shí)使用的是大端模式還是小端模式,上述例子是小端模式。UTF-16在要編碼的文本前面加上特殊的不可見(jiàn)字符ZERO WIDTH NO-BREAK SPACE(U+FEFF)。UTF-16有兩個(gè)變種:UTF-16LE,顯示指明使用小端模式;UTF-16BE,顯示指明大端模式。如果顯示指明了模式,則不會(huì)生成BOM:
>>> "El Ni?o".encode("utf_16le") b"Ex00lx00 x00Nx00ix00xf1x00ox00" >>> "El Ni?o".encode("utf_16be") b"x00Ex00lx00 x00Nx00ix00xf1x00o"
根據(jù)標(biāo)準(zhǔn),如果文件使用UTF-16編碼,且沒(méi)有BOM,則應(yīng)假定它使用的是UTF-16大端模式編碼。然而Intel x86架構(gòu)用的是小端模式,因此很多文件用的是不帶BOM的小端模式UTF-16編碼。這就容易造成混淆,如果把這些文件直接用在采用大端模式的機(jī)器上,則會(huì)出問(wèn)題(比較老的AMD也有大端模式,現(xiàn)在的AMD也是x86架構(gòu)了)。
由于大小端模式(字節(jié)順序)只對(duì)一個(gè)字(word)占多個(gè)字節(jié)的編碼有影響,所以對(duì)于UTF-8來(lái)說(shuō),不管設(shè)備使用哪種模式,生成的字節(jié)序列始終一致,因此不需要BOM。但在Windows下就比較扯淡了,有些應(yīng)用依然會(huì)添加BOM,并且會(huì)根據(jù)有無(wú)BOM來(lái)判斷是不是UTF-8編碼。
補(bǔ)充:筆者查資料時(shí)發(fā)現(xiàn)有“顯示指明BOM”一說(shuō),剛看到的時(shí)候筆者以為是在函數(shù)中傳遞一個(gè)bom關(guān)鍵字參數(shù)來(lái)指明BOM,然而不是,而是傳入一個(gè)帶有BOM標(biāo)識(shí)的編解碼器,如下:
# 默認(rèn)UTF-8不帶BOM,如果想讓字節(jié)序列帶上BOM,則應(yīng)傳入utf_8_sig >>> "El Ni?o".encode("utf_8_sig") b"xefxbbxbfEl Nixc3xb1o" >>> "El Ni?o".encode("utf_8") b"El Nixc3xb1o"5. 處理文本文件
處理文本的最佳實(shí)踐是"Unicode三明治"模型。圖示如下:
此模型的意思是:
對(duì)輸入的字節(jié)序列應(yīng)盡早解碼為字符串;
第二層相當(dāng)于程序的業(yè)務(wù)邏輯,這里應(yīng)該保證只處理字符串,而不應(yīng)該有編碼或解碼的操作存在;
對(duì)于輸出,應(yīng)盡晚地把字符串編碼為字節(jié)序列。
當(dāng)我們用Python處理文本時(shí),我們實(shí)際對(duì)這個(gè)模型并沒(méi)有多少感覺(jué),因?yàn)镻ython在讀寫(xiě)文件時(shí)會(huì)為我們做必要的編解碼工作,我們實(shí)際處理的是這個(gè)三明治的中間層。
5.1 Python編解碼Python中調(diào)用open函數(shù)打開(kāi)文件時(shí),默認(rèn)使用的是編解碼器與平臺(tái)有關(guān),如果你的程序?qū)?lái)要跨平臺(tái),推薦的做法是明確傳入encoding關(guān)鍵字參數(shù)。其實(shí)不管跨不跨平臺(tái),這都是推薦的做法。
對(duì)于open函數(shù),當(dāng)以二進(jìn)制模式打開(kāi)文件時(shí),它返回一個(gè)BufferedReader對(duì)象;當(dāng)以文本模式打開(kāi)文件時(shí),它返回的是一個(gè)TextIOWrapper對(duì)象:
>>> fp = open("zen.txt", "r", encoding="utf8") >>> fp <_io.TextIOWrapper name="zen.txt" mode="r" encoding="utf8"> >>> fp2 = open("zen.txt", "rb") # 當(dāng)以二進(jìn)制讀取文件時(shí),不需要指定編解碼器 >>> fp2 <_io.BufferedReader name="zen.txt">
這里有幾個(gè)點(diǎn):
除非想判斷編碼方式,或者文件本身就是二進(jìn)制文件,否則不要以二進(jìn)制模式打開(kāi)文本文件;就算想判斷編碼方式,也應(yīng)該使用Chardet,而不是重復(fù)造輪子。
如果打開(kāi)文件時(shí)未傳入encoding參數(shù),默認(rèn)值將由locale.getpreferredencoding()提供,但從這么函數(shù)名可以看出,其實(shí)它返回的也不一定是系統(tǒng)的默認(rèn)設(shè)置,而是用戶的偏好設(shè)置。用戶的偏好設(shè)置在不同系統(tǒng)中不一定相同,而且有的系統(tǒng)還沒(méi)法設(shè)置偏好,所以,正如官方文檔所說(shuō),該函數(shù)返回的是一個(gè)猜測(cè)的值;
如果設(shè)定了PYTHONENCODING環(huán)境變量,sys.stdout/stdin/stderr的編碼則使用該值,否則繼承自所在的控制臺(tái);如果輸入輸出重定向到文件,編碼方式則由locale.getpreferredencoding()決定;
Python讀取文件時(shí),對(duì)文件名(不是文件內(nèi)容?。┑木幗獯a器由sys.getfilesystemencoding()函數(shù)提供,當(dāng)以字符串作為文件名傳入open函數(shù)時(shí)就會(huì)調(diào)用它。但如果傳入的文件名是字節(jié)序列,則會(huì)直接將此字節(jié)序列傳給系統(tǒng)相應(yīng)的API。
總之:別依賴默認(rèn)值!
如果遵循Unicode三明治模型,并且始終在程序中指定編碼,那將避免很多問(wèn)題。但Unicode也有不盡人意的地方,比如文本規(guī)范化(為了比較文本)和排序。如果你只在ASCII環(huán)境中,或者語(yǔ)言環(huán)境比較固定單一,那么這兩個(gè)操作對(duì)你來(lái)說(shuō)會(huì)很輕松,但如果你的程序面向多語(yǔ)言文本,那么這兩個(gè)操作會(huì)很繁瑣。
5.2 規(guī)范化Unicode字符串由于Unicode有組合字符,所以字符串比較起來(lái)比較復(fù)雜。
補(bǔ)充:組合字符指變音符號(hào)和附加到前一個(gè)字符上的記號(hào),打印時(shí)作為一個(gè)整體。
>>> s1 = "café" >>> s2 = "cafeu0301" >>> s1, s2 ("café", "cafe?") >>> len(s1), len(s2) (4, 5) >>> s1 == s2 False
在Unicode標(biāo)準(zhǔn)中,"é"和"eu0301"叫做標(biāo)準(zhǔn)等價(jià)物,應(yīng)用程序應(yīng)該將它們視為相同的字符,但從上面代碼可以看出,Python并沒(méi)有將它們視為等價(jià)物,這就給Python中比較兩個(gè)字符串添加了麻煩。
解決的方法是使用unicodedata.normalize函數(shù)提供的Unicode規(guī)范化。它有四個(gè)標(biāo)準(zhǔn):NFC,NFD,NFKC,NFKD。
5.2.1 NFC和NFDNFC使用最少的碼位構(gòu)成等價(jià)的字符串,NFD把組合字符分解成基字符和多帶帶的組合字符。這兩種規(guī)范化方法都能讓比較行為符合預(yù)期:
>>> from unicodedata import normalize >>> len(normalize("NFC", s1)), len(normalize("NFC", s2)) (4, 4) >>> len(normalize("NFD", s1)), len(normalize("NFD", s2)) (5, 5) >>> normalize("NFD", s1) == normalize("NFD", s2) True >>> normalize("NFC", s1) == normalize("NFC", s2) True
NFC是W3C推薦的規(guī)范化形式。西方鍵盤(pán)通常能輸出組合字符,因此用戶輸入的文本默認(rèn)是NFC形式。我們對(duì)變音字符用的不多。但還是那句話,如果你的程序面向多語(yǔ)言文本,為了安全起見(jiàn),最好還是用normalize(”NFC“, user_text)清洗字符串。
使用NFC時(shí),有些單字符會(huì)被規(guī)范成另一個(gè)單字符,例如電阻的單位歐姆(Ω,U+2126,u2126)會(huì)被規(guī)范成希臘字母大寫(xiě)的歐米伽(U+03A9, u03a9)。這倆看著一樣,現(xiàn)實(shí)中電阻歐姆的符號(hào)也就是從希臘字母來(lái)的,兩者應(yīng)該相等,但在Unicode中是不等的,因此需要規(guī)范化,防止出現(xiàn)意外。
5.2.2 NFKC和NFKDNFKC和NFKD(K表示“compatibility”,兼容性)是比較嚴(yán)格的規(guī)范化形式,對(duì)“兼容字符”有影響。為了兼容現(xiàn)有的標(biāo)準(zhǔn),Unicode中有些字符會(huì)出現(xiàn)多次。比如希臘字母"μ"(U+03BC),Unicode除了有它,還加入了微符號(hào)"μ"(U+00B5),以便和latin1標(biāo)準(zhǔn)相互轉(zhuǎn)換,所以微符號(hào)是個(gè)“兼容字符”(上述的歐姆符號(hào)不是兼容字符?。?。這兩個(gè)規(guī)范會(huì)將兼容字符分解為一個(gè)或多個(gè)字符,如下:
>>> from unicodedata import normalize, name >>> half = "?" >>> normalize("NFKC", half) "1/2" >>> four_squared = "42" >>> normalize("NFKC", four_squared) "42"
從上面的代碼可以看出,這兩個(gè)標(biāo)準(zhǔn)可能會(huì)造成格式損失,甚至曲解信息,但可以為搜索和索引提供便利的中間表述。比如用戶在搜索1/2 inch時(shí),可能還會(huì)搜到包含? inch的文章,這便增加了匹配選項(xiàng)。
5.2.3 大小寫(xiě)折疊對(duì)于搜索或索引,大小寫(xiě)是個(gè)很有用的操作。同時(shí),對(duì)于Unicode來(lái)說(shuō),大小寫(xiě)折疊還是個(gè)復(fù)雜的問(wèn)題。對(duì)于此問(wèn)題,如果是初學(xué)者,首先想到的一定是str.lower()和str.upper()。但在處理多語(yǔ)言文本時(shí),str.casefold()更常用,它將字符轉(zhuǎn)換成小寫(xiě)。自Python3.4起,str.casefold()和str.lower()得到不同結(jié)果的有116個(gè)碼位。對(duì)于只包含latin1字符的字符串s,s.casefold()得到的結(jié)果和s.lower()一樣,但有兩個(gè)例外:微符號(hào)"μ"會(huì)變?yōu)橄ED字母"μ";德語(yǔ)Eszett(“sharp s”,?)為變成"ss"。
5.2.4 規(guī)范化文本匹配使用函數(shù)下面給出用以上內(nèi)容編寫(xiě)的幾個(gè)規(guī)范化匹配函數(shù)。對(duì)大多數(shù)應(yīng)用來(lái)說(shuō),NFC是最好的規(guī)范形式。不區(qū)分大小寫(xiě)的比較應(yīng)該使用str.casefold()。對(duì)于處理多語(yǔ)言文本,以下兩個(gè)函數(shù)應(yīng)該是必不可少的:
# 兩個(gè)多語(yǔ)言文本中的比較函數(shù) from unicodedata import normalize def nfc_equal(str1, str2): return normalize("NFC", str1) == normalize("NFC", str2) def fold_equal(str1, str2): return normalize("NFC", str1).casefold() == normalize("NFC", str2).casefold()
有時(shí)我們還想把變音符號(hào)去掉(例如“café”變“cafe”),比如谷歌在搜索時(shí)就有可能去掉變音符號(hào);或者想讓URL更易讀時(shí),也需要去掉變音符號(hào)。如果想去掉文本中的全部變音符號(hào),則可用如下函數(shù):
# 去掉多語(yǔ)言文本中的變音符號(hào) import unicodedata def shave_marks(txt): """去掉全部變音符號(hào)""" # 把所有字符分解成基字符和組合字符 norm_txt = unicodedata.normalize("NFD", txt) # 過(guò)濾掉所有組合記號(hào) shaved = "".join(c for c in norm_txt if not unicodedata.combining(c)) # 重組所有字符 return unicodedata.normalize("NFC", shaved) order = "“Herr Vo?: ? ? cup of ?tker? caffè latte ? bowl of a?aí.”" print(shave_marks(order)) greek = "Ζ?φυρο?, Zéfiro" print(shave_marks(greek)) # 結(jié)果: “Herr Vo?: ? ? cup of ?tker? caffe latte ? bowl of acai.” Ζεφυρο?, Zefiro
上述代碼去掉了所有的變音字符,包括非拉丁字符,但有時(shí)我們想只去掉拉丁字符中的變音字符,為此,我們還需要對(duì)基字符進(jìn)行判斷,以下這個(gè)版本只去掉拉丁字符中的變音字符:
# 僅去掉拉丁文中的變音符號(hào) import unicodedata import string def shave_marks_latin(txt): """去掉拉丁基字符中的所有變音符號(hào)""" norm_txt = unicodedata.normalize("NFD", txt) latin_base = unicodedata.combining(norm_txt[0]) # <1> keepers = [] for c in norm_txt: if unicodedata.combining(c) and latin_base: continue keepers.append(c) if not unicodedata.combining(c): latin_base = c in string.ascii_letters shaved = "".join(keepers) return unicodedata.normalize("NFC", shaved) # "?" 這是提取出來(lái)的變音符號(hào) t = "?cafe" print(shave_marks_latin(t)) # 結(jié)果 cafe
注意<1>處,如果一開(kāi)始直接latin_base = False,那么遇到刁鉆的人,該程序的結(jié)果將是錯(cuò)誤的:大家可以試一試,把<1>處改成latin_base = False,然后運(yùn)行該程序,看c上面的變音符號(hào)去掉了沒(méi)有。之所以第7行寫(xiě)成上述形式,就是考慮到可能有的人閑著沒(méi)事,將變音符號(hào)放在字符串的開(kāi)頭。
更徹底的規(guī)范化步驟是把西文中的常見(jiàn)符號(hào)替換成ASCII中的對(duì)等字符,如下:
# 將拉丁文中的變音符號(hào)去掉,并把西文中常見(jiàn)符號(hào)替換成ASCII中的對(duì)等字符 single_map = str.maketrans("""??????‘’“”?–—??""", """"f"*^<""""---~>""") multi_map = str.maketrans({ "€": "5.3 Unicode文本排序", "…": "...", "?": "OE", "?": "(TM)", "?": "oe", "‰": " ", "?": "**", }) multi_map.update(single_map) # 該函數(shù)不影響ASCII和latin1文本,只替換微軟在cp1252中為latin1額外添加的字符 def dewinize(txt): """把win1252符號(hào)替換成ASCII字符或序列""" return txt.translate(multi_map) def asciize(txt): no_mark = shave_marks_latin(dewinize(txt)) no_mark = no_mark.replace("?", "ss") return unicodedata.normalize("NFKC", no_mark) order = "“Herr Vo?: ? ? cup of ?tker? caffè latte ? bowl of a?aí.”" print(asciize(order)) # 結(jié)果: "Herr Voss: - 1?2 cup of OEtker(TM) caffe latte - bowl of acai."
Python中,非ASCII文本的標(biāo)準(zhǔn)排序方式是使用locale.strxfrm函數(shù),該函數(shù)“把字符串轉(zhuǎn)換成適合所在地區(qū)進(jìn)行比較的形式”,即和系統(tǒng)設(shè)置的地區(qū)相關(guān)。在使用locale.strxfrm之前,必須先為應(yīng)用設(shè)置合適的區(qū)域,而這還得指望著操作系統(tǒng)支持用戶自定義區(qū)域設(shè)置。比如以下排序:
>>> fruits = ["香蕉", "蘋(píng)果", "桃子", "西瓜", "獼猴桃"] >>> sorted(fruits) ["桃子", "獼猴桃", "蘋(píng)果", "西瓜", "香蕉"] >>> import locale >>> locale.setlocale(locale.LC_COLLATE, "zh_CN.UTF-8") # 設(shè)置后能按拼音排序 Traceback (most recent call last): File "", line 1, inFile "locale.py", line 598, in setlocale return _setlocale(category, locale) locale.Error: unsupported locale setting >>> locale.getlocale() (None, None)
筆者是Windows系統(tǒng),不支持區(qū)域設(shè)置,不知道Linux下支不支持,大家可以試試。
5.3.1 PyUCA想要正確實(shí)現(xiàn)Unicode排序,可以使用PyPI中的PyUCA庫(kù),這是Unicode排序算法的純Python實(shí)現(xiàn)。它沒(méi)有考慮區(qū)域設(shè)置,而是根據(jù)Unicode官方數(shù)據(jù)庫(kù)中的排序表排序,只支持Python3。以下是它的簡(jiǎn)單用法:
>>> import pyuca >>> coll = pyuca.Collator() >>> sorted(["cafe", "caff", "café"]) ["cafe", "caff", "café"] >>> sorted(["cafe", "caff", "café"], key=coll.sort_key) ["cafe", "café", "caff"]
如果想定制排序方式,可把自定義的排序表路徑傳給Collator()構(gòu)造方法。
6. 補(bǔ)充 6.1 Unicode數(shù)據(jù)庫(kù)Unicode標(biāo)準(zhǔn)提供了一個(gè)完整的數(shù)據(jù)庫(kù)(許多格式化的文本文件),它記錄了字符是否可打印、是不是字母、是不是數(shù)字、或者是不是其它數(shù)值符號(hào)等,這些數(shù)據(jù)叫做字符的元數(shù)據(jù)。字符串中的isidentifier、isprintable、isdecimal和isnumeric等方法都用到了該數(shù)據(jù)庫(kù)。unicodedata模塊中有幾個(gè)函數(shù)可用于獲取字符的元數(shù)據(jù),比如unicodedata.name()用于獲取字符的官方名稱(全大寫(xiě)),unicodedata.numeric()得到數(shù)值字符(如①,“1”)的浮點(diǎn)數(shù)值。
6.2 支持字符串和字節(jié)序列的雙模式API目前為止,我們一般都將字符串作為參數(shù)傳遞給函數(shù),但Python標(biāo)準(zhǔn)庫(kù)中有些函數(shù)既支持字符串也支持字節(jié)序列作為參數(shù),比如re和os模塊中就有這樣的函數(shù)。
6.2.1 正則表達(dá)式中的字符串和字節(jié)序列如果使用字節(jié)序列構(gòu)建正則表達(dá)式,d和w等模式只能匹配ASCII字符;如果是字符串模式,就能匹配ASCII之外的Unicode數(shù)字和字母,如下:
import re re_numbers_str = re.compile(r"d+") # 字符串模式 re_words_str = re.compile(r"w+") re_numbers_bytes = re.compile(rb"d+") # 字節(jié)序列模式 re_words_bytes = re.compile(rb"w+") # 要搜索的Unicode文本,包括“1729”的泰米爾數(shù)字 text_str = ("Ramanujan saw u0be7u0bedu0be8u0bef" " as 1729 = 13 + 123 = 93 + 103.") text_bytes = text_str.encode("utf_8") print("Text", repr(text_str), sep=" ") print("Numbers") print(" str :", re_numbers_str.findall(text_str)) # 字符串模式r"d+"能匹配多種數(shù)字 print(" bytes:", re_numbers_bytes.findall(text_bytes)) # 只能匹配ASCII中的數(shù)字 print("Words") print(" str :", re_words_str.findall(text_str)) # 能匹配字母、上標(biāo)、泰米爾數(shù)字和ASCII數(shù)字 print(" bytes:", re_words_bytes.findall(text_bytes)) # 只能匹配ASCII字母和數(shù)字 # 結(jié)果: Text "Ramanujan saw ???? as 1729 = 13 + 123 = 93 + 103." Numbers str : ["????", "1729", "1", "12", "9", "10"] bytes: [b"1729", b"1", b"12", b"9", b"10"] Words str : ["Ramanujan", "saw", "????", "as", "1729", "13", "123", "93", "103"] bytes: [b"Ramanujan", b"saw", b"as", b"1729", b"1", b"12", b"9", b"10"]6.2.2 os模塊中的字符串和字節(jié)序列
Python的os模塊中的所有函數(shù)、文件名或操作路徑參數(shù)既能是字符串,也能是字節(jié)序列。如下:
>>> os.listdir(".") ["π.txt"] >>> os.listdir(b".") [b"xcfx80.txt"] >>> os.fsencode("π.txt") b"xcfx80.txt" >>> os.fsdecode(b"xcfx80.txt") "π.txt"
在Unix衍生平臺(tái)中,這些函數(shù)編解碼時(shí)使用surrogateescape錯(cuò)誤處理方式以避免遇到意外字節(jié)序列時(shí)卡住。surrogateescape把每個(gè)無(wú)法解碼的字節(jié)替換成Unicode中U+DC00到U+DCFF之間的碼位,這些碼位是保留位,未分配字符,共應(yīng)用程序內(nèi)部使用。Windows使用的錯(cuò)誤處理方式是strict。
7. 總結(jié)本節(jié)內(nèi)容較多。本篇首先介紹了編碼的基本概念,并以Unicode為例說(shuō)明了編碼的具體過(guò)程;然后介紹了Python中的字節(jié)序列;隨后開(kāi)始接觸實(shí)際的編碼處理,如Python編解碼過(guò)程中會(huì)引發(fā)的錯(cuò)誤,以及Python中Unicode字符的比較和排序。最后,本篇簡(jiǎn)要介紹了Unicode數(shù)據(jù)庫(kù)和雙模式API。
迎大家關(guān)注我的微信公眾號(hào)"代碼港" & 個(gè)人網(wǎng)站 www.vpointer.net ~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/44716.html
摘要:第行把具名元組以的形式返回。對(duì)序列使用和通常號(hào)兩側(cè)的序列由相同類型的數(shù)據(jù)所構(gòu)成當(dāng)然不同類型的也可以相加,返回一個(gè)新序列。從上面的結(jié)果可以看出,它雖拋出了異常,但仍完成了操作查看字節(jié)碼并不難,而且它對(duì)我們了解代碼背后的運(yùn)行機(jī)制很有幫助。 《流暢的Python》筆記。接下來(lái)的三篇都是關(guān)于Python的數(shù)據(jù)結(jié)構(gòu),本篇主要是Python中的各序列類型 1. 內(nèi)置序列類型概覽 Python標(biāo)準(zhǔn)庫(kù)...
摘要:本篇繼續(xù)學(xué)習(xí)之路,實(shí)現(xiàn)更多的特殊方法以讓自定義類的行為跟真正的對(duì)象一樣。之所以要讓向量不可變,是因?yàn)槲覀冊(cè)谟?jì)算向量的哈希值時(shí)需要用到和的哈希值,如果這兩個(gè)值可變,那向量的哈希值就能隨時(shí)變化,這將不是一個(gè)可散列的對(duì)象。 《流暢的Python》筆記。本篇是面向?qū)ο髴T用方法的第二篇。前一篇講的是內(nèi)置對(duì)象的結(jié)構(gòu)和行為,本篇?jiǎng)t是自定義對(duì)象。本篇繼續(xù)Python學(xué)習(xí)之路20,實(shí)現(xiàn)更多的特殊方法以讓...
摘要:計(jì)算機(jī)中以字節(jié)為單位存儲(chǔ)和解釋信息,規(guī)定一個(gè)字節(jié)由八個(gè)二進(jìn)制位構(gòu)成,即個(gè)字節(jié)等于個(gè)比特。需要注意協(xié)議規(guī)定網(wǎng)絡(luò)字節(jié)序?yàn)榇蠖俗止?jié)序。以元組形式返回全部分組截獲的字符串。返回指定的組截獲的子串在中的結(jié)束索引子串最后一個(gè)字符的索引。 導(dǎo)語(yǔ):本文章記錄了本人在學(xué)習(xí)Python基礎(chǔ)之?dāng)?shù)據(jù)結(jié)構(gòu)篇的重點(diǎn)知識(shí)及個(gè)人心得,打算入門(mén)Python的朋友們可以來(lái)一起學(xué)習(xí)并交流。 本章重點(diǎn): 1、了解字符字節(jié)等概...
摘要:大多數(shù)待遇豐厚的開(kāi)發(fā)職位都要求開(kāi)發(fā)者精通多線程技術(shù)并且有豐富的程序開(kāi)發(fā)調(diào)試優(yōu)化經(jīng)驗(yàn),所以線程相關(guān)的問(wèn)題在面試中經(jīng)常會(huì)被提到。將對(duì)象編碼為字節(jié)流稱之為序列化,反之將字節(jié)流重建成對(duì)象稱之為反序列化。 JVM 內(nèi)存溢出實(shí)例 - 實(shí)戰(zhàn) JVM(二) 介紹 JVM 內(nèi)存溢出產(chǎn)生情況分析 Java - 注解詳解 詳細(xì)介紹 Java 注解的使用,有利于學(xué)習(xí)編譯時(shí)注解 Java 程序員快速上手 Kot...
閱讀 1452·2019-08-30 12:54
閱讀 1936·2019-08-30 11:16
閱讀 1668·2019-08-30 10:50
閱讀 2547·2019-08-29 16:17
閱讀 1343·2019-08-26 12:17
閱讀 1434·2019-08-26 10:15
閱讀 2450·2019-08-23 18:38
閱讀 839·2019-08-23 17:50