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