1. 程式人生 > >Python2.7字元編碼詳解

Python2.7字元編碼詳解

Python2.7字元編碼詳解

宣告

原文連結
本文主要介紹字元編碼基礎知識,以及Python2.7字元編碼實踐。
注意,文中關於Python字元編碼的解釋和建議僅適用於Python2.x版本,而不適用於Python3.x版本。

一. 字元編碼基礎

為明確概念,將字元的編碼模型分為以下4個層次:

  • 抽象字元清單(Abstract Character Repertoire, ACR):
    待編碼文字和符號的無序集合,包括各國文字、標點、圖形符號、數字等。
  • 已編碼字符集(Coded Character Set, CCS):
    從抽象字元清單到非負整數碼點(code point)集合的對映。
  • 字元編碼格式(Character Encoding Form, CEF):
    從碼點集合到指定寬度(如32位元整數)編碼單元(code unit)的對映。
  • 字元編碼方案(Character Encoding Scheme, CES):
    從編碼單元序列集合(一個或多個CEF)到一個序列化位元組序列的可逆轉換。

字元編碼順序:

    字元—>碼點—>編碼單元序列—>序列化位元組序列

例如中文的“漢”字使用GB2312時的編碼序列:
    漢—>2626—>\xBABA—>10111010.10111010

1.1 抽象字元清單(ACR)

抽象字元清單可以理解為無序的抽象字元集合。”抽象”意味著字元物件未必直接儲存於計算機系統中,也未必是真實世界中的具體事物,例如”a”和”為”。抽象字元也不必是圖形化的物件,例如控制字元是”0寬度空格”(zero-width space)。

大多數字符編碼的清單較小且處於”fixed”狀態,即不再追加新的抽象字元(否則將建立新的清單);其他清單處於”open”狀態,即允許追加新字元。例如,Unicode旨在成為通用編碼,其字元清單本身是開放的,以便週期性的新增新的可編碼字元。

1.2 已編碼字符集(CCS)

已編碼字符集是從抽象字元清單到非負整數(範圍不必連續)的對映。該整數稱為抽象字元的碼點(code point),或碼位(code position),該字元則稱為已編碼字元。注意,碼點並非位元或位元組,因此與計算機表示無關。碼點的取值範圍由編碼標準限定,該範圍稱為編碼空間(code space)。在一個標準中,已編碼字符集也稱為字元編碼、已編碼字元清單、字符集定義或碼頁(code page)。

在CCS中,需要明確定義已編碼字元相關的任何屬性。通常,標準為每個已編碼字元分配唯一的名稱,例如“拉丁小寫字母a(LATIN SMALL LETTER a)”。當同一個抽象字元出現在不同的已編碼字符集且被賦予不同的碼點時,通過其名稱可無歧義地標識該字元。這意味著:對於不同的編碼方式,同一個字元的碼點一般並不相同,相應的,其序列化位元組序列也不相同。因此在不同的編碼方式之間進行轉換進,必須基於字元而非其碼點或位元組序,即字元不變性,因為計算機儲存是的字元的二進位制位而不是字元本身

某些工作在CCS層的工業標準將字符集標準化(可能也包括其名稱或其他屬性),但並未將它們在計算機中的編碼表示進行標準化。例如,東亞字元標準GB2312-80(簡體中文)、CNS 11643(繁體中文)、JIS X 0208(日文),KS X 1001(韓文)。這些標準使用與之獨立的標準進行字元編碼的計算機表示,這將在CEF層描述。

1.3 字元編碼格式(CEF)

字元編碼格式是已編碼字符集中的碼點集合到編碼單元(code unit)序列的對映。注意,這裡是編碼單元序列,而非編碼單元。編碼單元為整數,在計算機架構中佔據特定的二進位制寬度,例如7位元、8位元等(最常用的是8/16/32位元)。編碼格式使字元表示為計算機中的實際資料。

編碼單元的序列不必具有相同的長度。序列具有相同長度的字元編碼格式稱為固定寬度(或稱等寬),否則稱為可變寬度(或稱變長)。

固定寬度的編碼格式示例如下:

編碼格式 編碼單元 編碼單元序列 適用字符集
7-bit 7位元 1個編碼單元 ASCII
8-bit 8位元 1個編碼單元 Windows CP1252
8-bit G0/G1 8位元 1個編碼單元 ISO 8859-1
8-bit EBCDIC 8位元 1個編碼單元 CP037和CP500
UCS-2 16位元 1個編碼單元 Unicode1.1(預設)
UCS-4 32位元 1個編碼單元 ISO/IEC 10646:2003
UTF-32 32位元 1個編碼單元 Unicode4.0/5.0,ISO/IEC 10646:2009


可變寬度的編碼格式示例如下:

編碼格式 編碼單元 編碼單元序列 適用字符集
UTF-8 8位元 1-4個編碼單元 Unicode1.1/3.0/4.0/5.0
8位元 1-6個編碼單元 ISO/IEC 10646:2003/2009
UTF-16 16位元 1-2個編碼單元 Unicode3.0(預設)/4.0/5.0


一個碼點未必對應一個編碼單元,編碼格式將一個碼點對映為多個編碼單元的序列,例如微軟碼頁932(日文)或950(繁體中文)中一個字元編碼為兩個位元組。然而,碼點到編碼單元序列的對映是唯一的

除東亞字符集外,所有傳統字符集的編碼空間都未超出單位元組範圍,因此它們通常使用相同的編碼格式(對此不必區分碼點和編碼單元)。

某些字符集可使用多種編碼格式。例如,GB2312-80字符集可使用GBK編碼、ISO 2022編碼或EUC編碼。此外,有的編碼格式可用於多種字符集,例如ISO 2022標準。ISO 2022為其支援的每個特定字符集分配一個特定的轉義序列(escape sequence)。預設情況下,ISO 2022資料被解釋為ASCII字符集;遇到任一轉義序列時則以特定的字符集解釋後續的資料,直到遇到一個新的轉義序列或恢復到預設狀態。ISO 2022標準旨在提供統一的編碼格式,以期支援所有字符集(尤其是中日韓等東亞文字)。但其資料解釋的控制機制相當複雜,且缺點很多,僅在日本使用普遍。

Unicode標準並未依照慣例,將每個字元直接對映為特定模式的編碼位元序列。相反地,Unicode先將字元對映為碼點,再將碼點以各種方式表示為編碼單元序列。通過將CCS和CEF分離,Unicode的編碼格式更為靈活(如UCS-X和UTF-X)。

以下詳細介紹中文編碼時常見的字符集及其編碼格式。為符合程式設計師既有概念,此處並未嚴格區分CCS與CEF。但應認識到,ASCII/EASCII和GB2312/GBK/GB18030既是CCS也是CEF;區位碼和Unicode是CCS;EUC-CN/ISO-2022-CN/HZ、UCS-2/UCS-4、UTF-8/UTF-16/UTF-32是CEF。

注意,中文編碼還有交換碼輸入碼機內碼輸出碼等概念。交換碼又稱國標碼,用於漢字資訊交換,即GB2312-80(區位碼加0x20)。輸入碼又稱外碼,即使用英文鍵盤輸入漢字時的編碼,大體分為音碼、形碼、數字碼和音形碼四類。例如,漢字”肖”用拼音輸入時外碼為xiao,用區位碼輸入時為4804,用五筆字型輸入時為IEF。機內碼又稱內碼或漢字儲存碼,即計算機作業系統內部儲存、處理和交換漢字所用的編碼(GB2312/GBK)。儘管同一漢字的輸入碼有多種,但其內碼相同。輸出碼又稱字型碼,即根據漢字內碼找到字型檔中的地址,再將其點陣字型在螢幕上輸出。

早期Windows系統預設的內碼與語言相關,英文系統內碼為ASCII,簡體中文系統內碼為GB2312或GBK,繁體中文系統內碼為BIG5。Windows NT+核心則採用Unicode編碼,以便支援所有語種字元。但由於現有的大量程式和文件都採用某種特定語言的編碼,因此微軟使用碼頁適應各種語言。例如,GB2312碼頁是CP20936,GBK碼頁是CP936,BIG5碼頁是CP950。此時,”內碼”的概念變得模糊。微軟一般將預設碼頁指定的編碼稱為內碼,在特殊場合也稱其內碼為Unicode。

1.3.1 ASCII(初創)

1.3.1.1 ASCII

ASCII(American Standard Code for Information Interchange)為7位元編碼,最高位空閒或者用作校驗位,編碼範圍是0x00-0x7F,共計128個字元。ASCII字符集包括英文字母、阿拉伯數字、英式標點和控制字元等。其中,0x00-0x1F和0x7F為33個無法列印的控制字元。

ASCII編碼設計良好,如數字和字母連續排列,數字對應其16進位制碼點的低四位大小寫字母可通過一個bit的翻轉而相互轉化(即第6個bit),等等。初創標準的影響力如此之強,以致於後世所有廣泛應用的編碼標準都要相容ASCII編碼。

在Internet上使用時,ASCII的別名(不區分大小寫)有ANSI_X3.4-1968、iso-ir-6、ANSI_X3.4-1986、ISO_646.irv:1991、ISO646-US、US-ASCII、IBM367、cp367和csASCII。

1.3.1.2 EASCII

EASCII擴充套件ASCII編碼位元組中閒置的最高位,即8位元編碼,以支援其他非英語語言。EASCII編碼範圍是0x00-0xFF,共計256個字元

不同國家對0x80-0xFF這128個碼點的不同擴充套件,最終形成15個ISO-8859-X編碼標準(X=1~11,13~16),涵蓋拉丁字母的西歐語言、使用西裡爾字母的東歐語言、希臘語、泰語、現代阿拉伯語、希伯來語等。例如為西歐語言而擴充套件的字符集編碼標準編號為ISO-8859-1,其別名為cp819、csISO、Latin1、ibm819、iso_8859-1、iso_8859-1:1987、iso8859-1、iso-ir-100、l1、latin-1。ISO-8859-1標準中,0x00-0x7F之間與ASCII字元相同,0x80-0x9F之間是控制字元,0xA0-0xFF之間是文字元號。其字符集詳見ASCII碼錶。

ISO-8859-1編碼空間覆蓋單位元組所有取值,在支援ISO-8859-1的系統中傳輸和儲存其他任何編碼的位元組流都不會造成資料丟失。換言之,可將任何編碼的位元組流視為ISO-8859-1編碼。因此,很多傳輸(如Java網路傳輸)和儲存(如MySQL資料庫)過程預設使用該編碼。

注意,ISO-8859-X編碼標準互不相容。例如,0xA3在Latin1編碼中代表英鎊符號”£”,在Latin2編碼中則代表”Ł”(帶斜線的大寫L)。而且,這兩個符號無法同時出現在一個檔案內。

ASCII和EASCII均為單位元組編碼(Single Byte Character System, SBCS),即使用一個位元組存放一個字元。只支援ASCII碼的系統會忽略每個位元組的最高位,只認為低7位是有效位。

1.3.2 MBCS/DBCS/ANSI(本地化)

由於單位元組能表示的字元太少,且同時也需要與ASCII編碼保持相容,所以不同國家和地區紛紛在ASCII基礎上制定自己的字符集。這些字符集使用大於0x80的編碼作為一個前導位元組,前導位元組與緊跟其後的第二(甚至第三)個位元組一起作為單個字元的實際編碼;而ASCII字元仍使用原來的編碼。以漢字為例,字符集GB2312/BIG5/JIS使用兩個位元組表示一個漢字,使用一個位元組表示一個ASCII字元。這類字符集統稱為ANSI字符集,正式名稱為MBCS(Multi-Byte Chactacter Set,多位元組字符集)或DBCS(Double Byte Charecter Set,雙位元組字符集)。在簡體中文作業系統下,ANSI編碼指代GBK編碼;在日文作業系統下,ANSI編碼指代JIS編碼。

ANSI編碼之間互不相容,因此Windows作業系統使用碼頁轉換表技術支援各字符集的顯示問題,即通過指定的轉換表將非Unicode的字元編碼轉換為同一字元對應的系統內部使用的Unicode編碼。可在”區域和語言選項”中選擇一個內碼表作為非Unicode編碼所採用的預設編碼方式,如936為簡體中文GBK,950為繁體中文Big5。但當資訊在國際間交流時,仍無法將屬於兩種語言的文字以同一種ANSI編碼儲存和傳輸。

1.3.2.1 GB2312

GB2312為中國國家標準簡體中文字符集,全稱《資訊交換用漢字編碼字符集基本集》,由中國國家標準總局於1980年釋出,1981年5月1日開始實施。標準號是GB2312—1980。

GB2312標準適用於漢字處理、漢字通訊等系統之間的資訊交換,通行於中國大陸地區及新加坡,簡稱國標碼。GB2312標準共收錄6763個簡體漢字,其中一級漢字3755個,二級漢字3008個。此外,GB2312還收錄數學符號、拉丁字母、希臘字母、日文平假名及片假名字母、俄語西裡爾字母等682個字元。這些非漢字字元有些來自ASCII字符集,但被重新編碼為雙位元組,並稱為”全形”字元;ASCII原字元則稱為”半形”字元。例如,全形a編碼為0xA3E1,半形a則編碼為0x61。

GB2312是基於區位碼設計的。區位碼將整個字符集分成94個區,每區有94個位。每個區位上只有一個字元,因此可用漢字所在的區和位來對其編碼。

區位碼中01-09區為特殊符號。16-55區為一級漢字,按拼音字母/筆形順序排序;56-87區為二級漢字,按部首/筆畫排序。10-15區及88-94區為未定義的空白區。

區位碼是一個四位的10進位制數,如1601表示16區1位,對應的字元是“啊”。Windows系統支援區位輸入法,例如通過”中文(簡體) - 內碼”輸入法小鍵盤輸入1601可得到”啊”,輸入0528則得到”ゼ”。

區位碼可視為已編碼字符集,其編碼格式可為EUC-CN(常用)、ISO-2022-CN(罕用)或HZ(用於新聞組)。ISO-2022-CN和HZ針對早期只支援7位元ASCII的系統而設計,且因為使用轉義序列而存在諸多缺點。ISO-2022標準將區號和位號加上32,以避開ASCII的控制符區。而EUC(Extended Unix Code)基於ISO-2022區位碼的94x94編碼表,將其編碼位元組的最高位置1,以簡化日文、韓文、簡體中文表示。可見,EUC區(位) = 原始區(位)碼 + 32 + 0x80 = 原始區(位)碼 + 0xA0。這樣易於軟體識別字符串中的特定位元組,例如小於0x7F的位元組表示ASCII字元,兩個大於0x7F的位元組組合表示一個漢字EUC-CN是GB2312最常用的表示方法,可認為通常所說的GB2312編碼就指EUC-CN或EUC-GB2312

綜上,GB2312標準中每個漢字及符號以兩個位元組來表示。第一個位元組稱為高位元組(也稱區位元組),使用0xA1-0xF7(將01-87區的區號加上0xA0);第二個位元組稱為低位元組(也稱位位元組),使用0xA1-0xFE(將01-94加上 0xA0)。漢字區的高位元組範圍是0xB0-0xF7,低位元組範圍是0xA1-0xFE,佔用碼位72*94=6768。其中有5個空位是D7FA-D7FE。例如,漢字”肖”的區位碼為4804,將其區號和位號分別加上0xA0得到0xD0A4,即為GB2312編碼。漢字的GB2312編碼詳見GB2312簡體中文編碼表,也可通過漢字編碼網站查詢。

GB2312所收錄的漢字已覆蓋中國大陸99.75%的使用頻率,但不包括人名、地名、古漢語等方面出現的生僻字。

1.3.2.2 GBK

GBK全稱為《漢字內碼擴充套件規範》,於1995年釋出,向下完全相容GB2312-1980國家標準,向上支援ISO 10646.1國際標準。該規範收錄Unicode基本多文種平面中的所有CJK(中日韓)漢字,幷包含BIG5(繁體中文)編碼中的所有漢字。其編碼高位元組範圍是0x81-0xFE,低位元組範圍是0x40-0x7E和0x80-0xFE,共23940個碼位,收錄21003個漢字和883個圖形符號。

GBK碼位空間可劃分為以下區域:

類別 區名 高位元組(Hex) 低位元組(Hex) 碼位數 字元數
符號區 1區 A1-A9 A1-FE 846 717
5區 A8-A9 40-A0(7F除外) 192 166
漢字區 2區 B0-F7 A1-FE 6768 6763
3區 81-A0 40-FE(7F除外) 6080 6080
4區 AA-FE 40-A0(7F除外) 8160 8160
使用者區 使用者區1 AA-AF A1-FE 564
使用者區2 F8-FE A1-FE 658
使用者區3 A1-A7 40-A0(7F除外) 672
合計 23940 21886


由上表可知,GBK的漢字區2區即是GB2312的編碼區。注意,碼位空間中的碼位並非都已編碼,例如0xA2E3和0xA2E4並未定義編碼。

在GB2312的EUC-CN編碼模式中,小於0x7F的位元組表示ASCII字元,兩個大於0x7F的位元組組合表示一個漢字,但是為擴充套件碼位空間,GBK規定只要高位元組大於0x7F就表示一個漢字的開始。但低位元組為0x40-0x7E的GBK字元會佔用ASCII碼位,而程式可能使用該範圍內的ASCII字元作為特殊符號,例如將反斜槓”\”作為轉義序列的開始。若定位這些符號時未判斷是否屬於某個GBK漢字的低位元組,就會造成誤判。

1.3.2.3 GB18030

GB18030全稱為國家標準GB18030-2005《資訊科技中文編碼字符集》,是中國計算機系統必須遵循的基礎性標準之一。GB18030與GB2312-1980完全相容,與GBK基本相容,收錄GB 13000及Unicode3.1的全部字元,包括70244個漢字、多種中國少數民族字元、GBK不支援的韓文表音字元等。

GB2312和GBK均為雙位元組等寬編碼,若算上相容ASCII所支援的單位元組,也可視為單位元組和雙位元組混合的變長編碼。GB18030編碼是變長編碼,每個字元可用一個、兩個或四個位元組表示。GB18030碼位定義如下:

位元組數 編碼範圍(Hex) 碼位數 字元數
單位元組 00-7F 128 128
雙位元組 8140-FE7E和8180-FEFE 23940 21897
四位元組 81308130-FE39FE39 1587600 54531
合計 1611668 76556


可見,GB18030的單位元組編碼範圍與ASCII相同,雙位元組編碼範圍則與GBK相同。此外,GB18030有1611668個碼位,多於Unicode的碼位數目(1114112)。因此,GB18030有足夠的空間對映Unicode的所有碼位。

GBK編碼不支援歐元符號”€”,Windows CP936碼頁使用0x80表示歐元,GB18030編碼則使用0xA2E3表示歐元。

從ASCII、GB2312、GBK到GB18030,編碼向下相容,即相同字元編碼也相同。這些編碼可統一處理英文和中文,區分中文編碼的方法是高位元組的最高位不為0。

1.3.3 Unicode(國際化)

Unicode字符集由多語言軟體製造商組成的統一碼聯盟(Unicode Consortium)與國際標準化組織的ISO-10646工作組制訂,為各種語言中的每個字元指定統一且唯一的碼點,以滿足跨語言、跨平臺轉換和處理文字的要求。

最初統一碼聯盟和ISO組織試圖獨立制訂單一字符集,從Unicode 2.0後開始協作和共享,但仍各自發布標準(每個Unicode版本號都能找到對應的ISO 10646版本號)。兩者的字符集相同,差異主要是編碼格式。

Unicode碼點範圍為0x0-0x10FFFF,共計1114112個碼點,劃分為編號0-16的17個字元平面,每個平面包含65536個碼點。其中編號為0的平面最為常用,稱為基本多語種平面(Basic Multilingual Plane, BMP);其他則稱為輔助語言平面。Unicode碼點的表示方式是”U+”加上16進位制的碼點值,例如字母”A”的Unicode編碼寫為U+0041。通常所說的Unicode字元多指BMP字元。其中,U+0000到U+007F的範圍與ASCII字元完全對應,U+4E00到U+9FA5的範圍定義常用的20902個漢字字元(這些字元也在GBK字符集中)。

ISO-10646標準將Unicode稱為通用字符集(Universal Character Set, UCS),其編碼格式以”UCS-“加上編碼所用的位元組數命名。例如,UCS-2使用雙位元組編碼,僅能表示BMP中的字元;UCS-4使用四位元組編碼(實際只用低31位),可表示所有平面的字元。UCS-2中每兩個位元組前再加上0x0000就得到BMP字元的UCS-4編碼。這兩種編碼格式都是等寬編碼,且已經過時。另一種編碼格式來自Unicode標準,名為通用編碼轉換格式(Unicode Translation Format, UTF),其編碼格式以”UTF-“加上編碼所用的位元數命名。例如,UTF-8以8位元單位元組為單位,BMP字元在UTF-8中被編碼為1到3個位元組,BMP之外的字元則對映為4個位元組;UTF-16以16位元雙位元組為單位,BMP字元為2個位元組,BMP之外的字元為4個位元組;UTF-32則是定長的四位元組。這三種編碼格式均都可表示所有平面的字元。

UCS-2不同於GBK和BIG5,它是真正的等寬編碼,每個字元都使用兩個位元組,這種特性在字串截斷和字元數計算時非常方便。UTF-16是UCS-2的超集,在BMP平面內UCS-2完全等同於UTF-16。由於BMP之外的字元很少用到,實際使用中UCS-2和UTF-16可近似視為等價。類似地,UCS-4和UTF-32是等價的,但目前使用比較少。

Windows系統中Unicode編碼就指UCS-2或UTF-16編碼,即英文字元和中文漢字均由兩位元組表示,也稱為寬位元組。但這種編碼對網際網路上廣泛使用的ASCII字元而言會浪費空間,因此網際網路字元編碼主要使用UTF-8

1.3.3.1 UTF-8

UTF-8是一種針對Unicode的可變寬度字元編碼,可表示Unicode標準中的任何字元。UTF-8已逐漸成為電子郵件、網頁及其他儲存或傳輸文字的應用中,優先採用的編碼方式。網際網路工程工作小組(IETF)要求所有網際網路協議都必須支援UTF-8編碼。

UTF-8使用1-4個位元組為每個字元編碼,其規則如下(x表示可用編碼的位元位):

Unicode碼點(Hex) UTF-8序列(Bin) 位元組數
0000 - 007F 0xxxxxxx 1
0080 - 07FF 110xxxxx 10xxxxxx 2
0800 - FFFF 1110xxxx 10xxxxxx 10xxxxxx 3
10000~10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 4


亦即:
1) 對於單位元組符號,位元組最高位置為0,後面7位為該符號的Unicode碼。這與128個US-ASCII字元編碼相同,即相容ASCII編碼。因此,原先處理ASCII字元的軟體無須或只須做少部份修改,即可繼續使用。
2)對於n位元組符號(n>1),首位元組的前n位均置為1,第n+1位置為0,後面位元組的前兩位一律設為10。其餘二進位制位為該符號的Unicode碼。
可見,若首位元組最高位為0,則表明該位元組單獨就是一個字元;若首位元組最高位為1,則連續出現多少個1就表示當前字元佔用多少個位元組

以中文字元”漢”為例,其Unicode編碼是U+6C49,位於0x0800-0xFFFF之間,因此”漢”的UTF-8編碼需要三個位元組,即格式是1110xxxx 10xxxxxx 10xxxxxx。將0x6C49寫成二進位制0110 110001 001001,用這個位元流依次代替x,得到11100110 10110001 10001001,即”漢”的UTF-8編碼為0xE6B189。注意,常用漢字的UTF-8編碼佔用3個位元組,中日韓超大字符集裡的漢字佔用4個位元組。

考慮到輔助平面字元很少使用,UTF-8規則可簡記為(0),(110,10),(1110,10,10)或(00-7F),(C0-DF,80-BF),(E0-E7,80-BF,80-BF)。即,單位元組編碼的位元組取值範圍為0x00-0x7F,雙位元組編碼的首位元組為0xC0-0xDF,三位元組編碼的首位元組為0xE0-0xEF。這樣只要看到首位元組範圍就知道編碼位元組數,可大大簡化演算法。

UTF-8具有(包括但不限於)如下優點:
ASCII文字串也是合法的UTF-8文字,因此所有現存的ASCII文字不需要轉換,且僅支援7位元字元的軟體也可處理UTF-8文字。
UTF-8可編碼任意Unicode字元,而無需選擇碼頁或字型,且支援同一文字內顯示不同語種的字元。
Unicode字串經UTF-8編碼後不含零位元組(因為0位元組為NULL,表示控制序列),因此可由C語言字串函式(如strcpy)處理,也能通過無法處理零位元組的協議傳輸。
UTF-8編碼較為緊湊。ASCII字元佔用一個位元組,與ASCII編碼相當;拉丁字元佔用兩個位元組,與UTF-16相當;中文字元一般佔用三個位元組,雖遜於GBK但優於UTF-32。
UTF-8為自同步編碼,很容易掃描定位字元邊界。若位元組在傳輸過程中損壞或丟失,根據編碼規律很容易定位下一個有效的UTF-8碼點並繼續處理(再同步)。 許多雙位元組編碼(尤其是GB2312這種高低位元組均大於0x7F的編碼),一旦某個位元組出現差錯,就會影響到該位元組之後的所有字元。
UTF-8字串可由簡單的啟發式演算法可靠地識別。合法的UTF-8字元序列不可能出現最高位為1的單個位元組,而出現最高位為1的位元組對的概率僅為11.7%,這種概率隨序列長度增長而減小。因此,任何其他編碼的文字都不太可能是合法的UTF-8序列。

1.3.3.2 UTF-16

當Unicode字元碼點位於BMP平面(即小於U+10000)時,UTF-16將其編碼為1個16位元編碼單元(即雙位元組),該單元的數值與碼點值相同。例如,U+8090的UTF-16編碼為0x8090。同時可見,UTF-16不相容ASCII

當Unicode字元碼點超出BMP平面時,UTF-16編碼較為複雜,詳見surrogate pairs

UTF-16編碼在空間效率上比UTF-32高兩倍,而且對於BMP平面內的字串,可在常數時間內找到其中的第N個字元。

1.3.3.3 UTF-32

UTF-32將Unicode字元碼點編碼為1個32位元編碼單元(即四位元組),因此空間效率較低,不如其它Unicode編碼應用廣泛。

UTF-32編碼可在常數時間內定位Unicode字串裡的第N個字元,因為第N個字元從第4×Nth個位元組開始。

1.3.3.4 位元組序問題

我們知道,對於UTF-16和UTF-32來說,存在位元組序問題,即大端位元組和小端位元組:例如,“奎”的Unicode編碼是594E,“乙”的Unicode編碼是4E59。如果我們收到UTF-16位元組流“594E”,那麼這是“奎”還是“乙”?如果BOM是大端序,那麼程式碼點就應該是594E,那麼就是“奎”,如果BOM是小端序,那麼程式碼點就應該是4E59,就是“乙”了。但是對於UTF-8來說,卻不存在位元組序問題。原因如下:

第一,編碼單元與編碼單元在網路中傳輸的順序是確定的。即使是多位元組編碼方案,在網路層傳輸是沒有問題的。比如 a b c,分別代表三個位元組,傳送時順序是abc,那麼接收時,仍然是abc,這個順序不會錯亂。我們經常會想utf8是多位元組編碼,怎麼就不會存在位元組序問題,這一條就很好的解答這個問題了。

第二,位元組序指的是編碼單元內部的位元組順序
1、雖然utf8是變長編碼,但是它的編碼單元是單個位元組,不存在誰在高位、誰在低位的問題,所以不存在順序問題!順便說一下解碼問題,由於utf8的首位元組記錄了總位元組數(比如3個),所以讀取首位元組後,再讀取後續位元組(2個),然後進行解碼,得到完整的位元組數,從而保證解碼也是正確的。
2、utf16,utf32是定長編碼,這裡拿utf16舉例,它總是以2個位元組為編碼單元,鑑於“第一條”編碼單元與編碼單元之間的順序是正確的,問題只能在編碼單元內部中位元組與位元組的順序,由於硬體cpu的不同,編碼單元內部位元組與位元組的順序不確定。假如cpu是大端序那麼高位在前,如果cpu是小端序那麼低位在前,為了區分,所以有了BOM(byte order mark),然後計算機才能知道誰是高位,誰是低位,知道了高低位,從而能正確組裝,然後才能解碼正確。

綜上所述,因為utf8是單位元組為編碼單元,在網路傳輸時,不存在位元組序列問題。在解碼時,由於首位元組記錄了總位元組數,所以能正確解碼
因為utf16是定長編碼,總是以2個位元組為編碼單元,在網路傳輸時,不存在位元組序列問題。在解碼時,由於cpu硬體差異,存在位元組序問題,所以通過BOM來標記位元組順序

1.3.3.5 編碼適用場景

當程式需要與現存的那些專為8位元資料而設計的實現協作時,應選擇UTF-8編碼;當程式需要處理BMP平面內的字元(尤其是東亞語言)時,應選擇UTF-16編碼;當程式需要處理單個字元(如接收鍵盤驅動產生的一個字元),應選擇UTF-32編碼。因此,許多應用程式選用UTF-16作為其主要的編碼格式,而網際網路則廣泛使用UTF-8編碼。

1.4 字元編碼方案(CES)

字元編碼方案主要關注跨平臺處理編碼單元寬度超過一個位元組的資料

對於不同的編碼格式來說,存在兩個問題:一是跨平臺的問題,另一個是跨語言的問題。前者是因為不同計算機平臺對於位元組序的處理方式不同,後者是因為不同語言的編碼格式對應的二進位制序列可能相同。對於統一使用unicode的編碼格式來說,不存在跨語言的問題,是存在跨平臺的問題。

大多數等寬的單位元組CEF可直接對映為CES,即每個7位元或8位元編碼單元對映為一個取值與之相同的位元組。大多數混合寬度的單位元組CEF也可簡單地將CEF序列對映為位元組,例如UTF-8。UTF-16因為編碼單元為雙位元組,序列化位元組時必須指明位元組順序。例如,UTF-16BE以大位元組序序列化雙位元組編碼單元;UTF-16LE則以小位元組序序列化雙位元組編碼單元。

早期的處理器對記憶體地址解析方式存在差異。例如,對於一個雙位元組的記憶體單元(值為0x8096),PowerPC等處理器以記憶體低地址作為最高有效位元組,從而認為該單元為U+8096(肖);x86等處理器以記憶體高地址作為最高有效位元組,從而認為該單元為U+9680(隀)。前者稱為大位元組序(Big-Endian),後者稱為小位元組序(Little-Endian)。無論是兩位元組的UCS-2/UTF-16還是四位元組的UCS-4/UTF-32,既然編碼單元為多位元組,便涉及位元組序問題。

Unicode將碼點U+FEFF的字元定義為位元組順序標記(Byte Order Mark, BOM),而位元組顛倒的U+FFFE在UTF-16中並非字元,(0xFFFE0000)對UTF-32而言又超出編碼空間。因此,通過在Unicode資料流頭部新增BOM標記,可無歧義地指示編碼單元的位元組順序。若接收者收到0xFEFF,則表明資料流為UTF-16編碼,且為大位元組序;若收到0xFEFF,則表明資料流為小位元組序的UTF-16編碼。注意,U+FEFF本為零寬不換行字元(ZERO WIDTH NO-BREAK SPACE),在Unicode資料流頭部以外出現時,該字元被視為零寬不換行字元。自Unicode3.2標準起廢止U+FEFF的不換行功能,由新增的U+2060(Word Joiner)代替。

不同的編碼方案對零寬不換行字元的解析如下:

編碼方案 大位元組序(Hex) 小位元組序(Hex)
UTF-8 EF BB BF EF BB BF
UTF-16 Fe FF FF Fe
UTF-32 00 00 Fe FF FF Fe 00 00


UTF-16和UTF-32編碼預設為大位元組序。UTF-8以位元組為編碼單元,沒有位元組序問題,BOM用於表明其編碼格式(signature),但不建議如此。因為UTF-8編碼特徵明顯,無需BOM即可檢測出是否UTF-8序列(序列較短時可能不準確)。

微軟建議所有Unicode檔案以BOM標記開頭,以便於識別檔案使用的編碼和位元組順序。例如,Windows記事本預設儲存的編碼格式是ANSI(簡體中文系統下為GBK編碼),不新增BOM標記。另存為”Unicode”編碼(Windows預設Unicode編碼為UTF-16LE)時,檔案開頭新增0xFFFE的BOM;另存為”Unicode big endian”編碼時,檔案開頭新增0xFEFF的BOM;另存為”UTF-8”編碼時,檔案開頭新增0xEFBBBF的BOM。使用UEStudio開啟ANSI編碼的檔案時,右下方行列資訊後顯示”DOS”;開啟Unicode檔案時顯示”U-DOS”;開啟Unicode big endian檔案時顯示”UBE-DOS”;開啟UTF-8檔案時顯示”U8-DOS”。

藉助BOM標記,記事本在開啟文字檔案時,若開頭沒有BOM,則判斷為ANSI編碼;否則根據BOM的不同判斷是哪種Unicode編碼格式然而,即使檔案開頭沒有BOM,記事本開啟該檔案時也會先用UTF-8檢測編碼,若符合UTF-8特徵則以UTF-8解碼顯示。考慮到某些GBK編碼序列也符合UTF-8特徵,檔案內容很短時可能會被錯誤地識別為UTF-8編碼。例如,記事本中只寫入”聯通”二字時,以ANSI編碼儲存後再開啟會顯示為黑框;而只寫入”奼塧”時,再開啟會顯示為”漢a”。若再輸入更多漢字並儲存,然後開啟清空重新輸入”聯通”,儲存後再開啟時會正常顯示,這說明記事本確實能”記事”。當然,也可通過記事本【檔案】|【開啟】選單開啟顯示為黑框的”聯通”檔案,在”編碼”下拉框中將UTF-8改為ANSI,即可正常顯示。然而Windows系統下的記事本根本 沒有編碼選項。

Unicode標準並未要求或建議UTF-8編碼使用BOM,但確實允許BOM出現在檔案開頭。帶有BOM的Unicode檔案有時會帶來一些問題:

Linux/UNIX系統未使用BOM,因為它會破壞現有ASCII檔案的語法約定。
某些編輯器不會新增BOM,或者可以選擇是否新增BOM。
某些語法分析器可以處理字串常量或註釋中的UTF-8,但無法分析檔案開頭的BOM。
某些程式在檔案開頭插入前導字元來宣告檔案型別等資訊,這與BOM的用途衝突。

綜合起來,程式可通過下一步驟識別文字的字符集和編碼:
1) 檢查文字開頭是否有BOM,若有則已指明文字編碼。
2) 若無BOM,則檢視是否有編碼宣告(針對Python指令碼和XML文件等)。
3) 若既無BOM也無編碼宣告,則Python指令碼應為ASCII編碼,其他文字則需要猜測編碼或請示使用者。

記事本就是根據文字的特徵來猜測其字元編碼。缺點是當檔案內容較少時編碼特徵不夠明確,導致猜測結果不能完全精準。Word則通過彈出一個對話方塊來請示使用者。例如,將”聯通”檔案右鍵以Word開啟時,Word也會猜測該檔案是UTF-8編碼,但並不能確定,因此會彈出檔案轉換的對話方塊,請使用者選擇使文件可讀的編碼。這時無論選擇”Windows(預設)”還是”MS-DOS”或是”其他編碼”下拉框(初始顯示UTF-8)裡的簡體中文編碼,均能正常顯示”聯通”二字。

注意,文字檔案並不單指記事本純文字,各種原始碼檔案也是文字檔案。因此,編輯和儲存原始碼檔案時也要考慮字元編碼(除非僅使用ASCII字元),否則編譯器或直譯器可能會以錯誤的編碼格式去解析原始碼。

另外,不建議使用windows的記事本編寫純文字檔案,建議使用Notepad或者Sublime Text。

1.5 中文字元亂碼(Mojibake)

亂碼(mojibake)是指以非期望的編碼格式解碼文字時產生的混亂字元,通常表現為正常文字被系統地替換為其他書寫系統中不相關的符號。當字元的二進位制表示被視為非法時,可能被替換為通用替換字元U+FFFD。當多個連續字元的二進位制編碼恰好對應其他編碼格式的一個字元時,也會產生亂碼。這要麼發生在不同長度的等寬編碼之間(如東亞雙位元組編碼與歐洲單位元組編碼),要麼是因為使用變長編碼格式(如UTF-8和UTF-16)。

本節不討論因字型(font)或字型中字形(glyph)缺失而導致的字形渲染失敗。這種渲染失敗表現為整塊的碼點以16進位制顯示,或被替換為U+FFFD。

為正確再現被編碼的原始文字,必須確保所編碼資料與其編碼宣告一致。因為資料本身可被操縱,編碼宣告可被改寫,兩者不一致時必然產生亂碼。

亂碼常見於文字資料被宣告為錯誤的編碼,或不加編碼宣告就在預設編碼不同的計算機之間傳輸。例如,通訊協議依賴於每臺計算機的編碼設定,而不是與資料一起傳送或儲存元資料。

計算機的預設設定之所以不同,一部分是因為Unicode在作業系統家族中的部署不同,另一部分是因為針對人類語言的不同書寫系統存在互不相容的傳統編碼格式。目前多數Linux發行版已切換到UTF-8編碼(如LANG=zh_CN.UTF-8),但Windows系統仍使用碼頁處理不同語言的文字檔案。此外,若中文”漢字”以UTF-8編碼,軟體卻假定文字以Windows1252或ISO-8859-1編碼,則會錯誤地顯示為”汉字”或”汉字”。類似地,在Windows簡體中文系統(cp936)中手工建立檔案(如”GNU Readline庫函式的應用示例‹”)時,檔名為gbk編碼;而通過Samba服務複製到Linux系統時,檔名被改為utf-8編碼。再通過fileZilla將檔案下載至外部裝置時,若外設預設編碼為ISO-8859-1,則最終檔名會顯示為亂碼(如”GNU Readline库函数的应用示例”)。注意,通過Samba服務建立檔案並編輯時,檔名為UTF-8編碼,檔案內容則為GBK編碼。

以下介紹常見的亂碼原因及解決方案。

1.5.1 未指定編碼格式

若未指定編碼格式,則由軟體通過其他手段確定,例如字符集配置或編碼特徵檢測。文字檔案的編碼通常由作業系統指定,這取決於系統型別和使用者語言。當檔案來自不同配置的計算機時,例如Windows和Linux之間傳輸檔案,對檔案編碼的猜測往往是錯的。一種解決方案是使用位元組順序標記(BOM),但很多分析器不允許原始碼和其他機器可讀的文字中出現BOM。另一種方案是將編碼格式存入檔案系統元資料中,支援擴充套件檔案屬性的檔案系統可將其存為user.charset。這樣,想利用這一特性的軟體可去解析編碼元資料,而其他軟體則不受影響。此外,某些編碼特徵較為明顯,尤其是UTF-8,但仍有許多編碼格式難以區分,例如EUC-JP和Shift-JIS。總之,無論依靠字符集配置還是編碼特徵,都很容易誤判。

1.5.2 錯誤指定編碼格式

錯誤指定編碼格式時也會出現亂碼,這常見於相似的編碼之間

事實上,有些被人們視為等價的編碼格式仍有細微差別。例如,ISO 8859-1(Latin1)標準起草時,微軟也在開發碼頁1252(西歐語言),且先於ISO 8859-1完成。Windows-1252是ISO 8859-1的超集,包含C1範圍內額外的可列印字元。若將Windows-1252編碼的文字宣告為ISO 8859-1併發送,則接收端很可能無法完全正確地顯示文字。類似地,IANA將CP936作為GBK的別名,但GBK為中國官方規範,而CP936事實上由微軟維護,因此兩者仍有細微差異(但不如CP950和BIG5的差異大)。

很多仍在使用的編碼都與彼此部分相容,且將ASCII作為公共子集。因為ASCII文字不受這些編碼格式的影響,使用者容易誤認為他們在使用ASCII編碼,而將實際使用的ASCII超集宣告為”ASCII”。也許為了簡化,即使在學術文獻中,也能發現”ASCII”被當作不相容Unicode的編碼格式,而文中”ASCII”其實是Windows-1252編碼,”Unicode”其實是UTF-8編碼(UTF-8向後相容ASCII)。

1.5.3 過度指定編碼格式

多層協議中,當每層都試圖根據不同資訊指定編碼格式時,最不確定的資訊可能會誤導接受者。例如,Web伺服器通過HTTP服務提供靜態HTML檔案時,可用以下任一方式將字符集通知客戶端:

以HTTP標頭。這可基於伺服器配置或由伺服器上執行的應用程式控制。
以檔案中的HTML元標籤(http-equiv或charset)或XML宣告的編碼屬性。這是作者儲存該檔案時期望使用的編碼。
以檔案中的BOM標記。這是作者的編輯器儲存檔案時實際使用的編碼。除非發生意外的編碼轉換(如以一種編碼開啟而以另一種編碼儲存),該資訊將是正確的。
顯然,當任一方式出現差錯,而客戶端又依賴該方式確定編碼格式時,就會導致亂碼產生。

1.5.4 解決方案

應用程式使用UTF-8作為預設編碼時互通性更高,因為UTF-8使用廣泛且向後相容US-ASCII。UTF-8可通過簡單的演算法直接識別,因此設計良好的軟體可以避免混淆UTF-8和其他編碼。

現代瀏覽器和字處理器通常支援許多字元編碼格式。瀏覽器通常允許使用者即時更改渲染引擎的編碼設定,而文書處理器允許使用者開啟檔案時選擇合適的編碼。這需要使用者進行一些試錯,以找到正確的編碼。

當程式支援的字元編碼種類過少時,使用者可能需要更改作業系統的編碼設定以匹配該程式的編碼。然而,更改系統範圍的編碼設定可能導致已存在的程式出現亂碼。在Windows XP或更高版本的系統中,使用者可以使用Microsoft AppLocale,以改變單個程式的區域設定。

當然,出現亂碼時使用者也可手工或程式設計恢復原始文字,詳見本文”2.5 處理中文亂碼”節,或《Linux->Windows主機目錄和檔名中文亂碼恢復》一文。

二. Python2.7字元編碼

因字元編碼因系統而異,而本節程式碼例項較多,故首先指明執行環境,以免誤導讀者。

可通過以下程式碼獲取當前系統的字元編碼資訊:

#coding=utf-8
import sys, locale
def SysCoding():
    fmt = '{0}: {1}'
    #當前系統所使用的預設字元編碼
    print fmt.format('DefaultEncoding    ', sys.getdefaultencoding())
    #檔案系統編碼('None'表示使用系統預設編碼)
    print fmt.format('FileSystemEncoding ', sys.getfilesystemencoding())
    #預設的區域設定並返回元祖(語言, 編碼)
    print fmt.format('DefaultLocale      ', locale.getdefaultlocale())
    #使用者首選的文字資料編碼(猜測結果)
    print fmt.format('PreferredEncoding  ', locale.getpreferredencoding())
    #直譯器Shell標準輸入字元編碼
    print fmt.format('StdinEncoding      ', sys.stdin.encoding)
    #直譯器Shell標準輸出字元編碼
    print fmt.format('StdoutEncoding     ', sys.stdout.encoding)
if __name__ == '__main__':
    SysCoding()    

作者測試所用的Windows XP主機字元編碼資訊如下:

DefaultEncoding    : ascii
FileSystemEncoding : mbcs
DefaultLocale      : ('zh_CN', 'cp936')
PreferredEncoding  : cp936
StdinEncoding      : cp936
StdoutEncoding     : cp936

對於這幾種編碼方式的使用場景,我們將下文進行解釋。
如無特殊說明,本節所有程式碼片段均在這臺Windows主機上執行。

注意,Windows NT+系統中,檔名本就為Unicode編碼,故不必進行編碼轉換。但getfilesystemencoding()函式仍返回’mbcs’,以便應用程式使用該編碼顯式地將Unicode字串轉換為用途等同檔名的位元組串。注意,”mbcs”並非某種特定的編碼,而是根據設定的Windows系統區域不同,指代不同的編碼。例如,在簡體中文Windows預設的區域設定裡,”mbcs”指代GBK編碼。

作為對比,其他兩臺Linux主機字元編碼資訊分別為:

#Linux 1
DefaultEncoding    : ascii
FileSystemEncoding : UTF-8
DefaultLocale      : ('zh_CN', 'utf')
PreferredEncoding  : UTF-8
StdinEncoding      : UTF-8
StdoutEncoding     : UTF-8

#Linux 2
DefaultEncoding    : ascii
FileSystemEncoding : ANSI_X3.4-1968  #ASCII規範名
DefaultLocale      : (None, None)
PreferredEncoding  : ANSI_X3.4-1968
StdinEncoding      : ANSI_X3.4-1968
StdoutEncoding     : ANSI_X3.4-1968

可見,StdinEncoding、StdoutEncoding與FileSystemEncoding保持一致。這就可能導致Python指令碼編輯器和直譯器(CPython 2.7)的程式碼執行差異,後文將會給出例項。此處先引用Python幫助文件中關於stdin和stdout的描述:

stdin is used for all interpreter input except for scripts but including calls to input() and raw_input(). stdout is used for the output of print and expression statements and for the prompts of input() and raw_input(). The interpreter's own prompts and (almost all of) its error messages go to stderr.

可見,在Python Shell裡輸入中文字串時,該字串為cp936編碼,即gbk;當print或raw_input()向Shell輸出中文字串時,該字串按照cp936解碼。

通過sys.setdefaultencoding()可修改當前系統所使用的預設字元編碼。例如,在python27的Lib\site-packages目錄下新建sitecustomize.py指令碼,內容為:

#encoding=utf8  
import sys
reload(sys)
sys.setdefaultencoding('utf8')

重啟Python直譯器後執行sys.getdefaultencoding(),會發現預設編碼已改為UTF-8。多次重啟之後仍有效,這是因為Python啟動時自動呼叫該檔案設定系統預設編碼。而在作者的環境下,無論是Shell執行還是原始碼中新增上述語句,均無法修改系統預設編碼,反而導致sys模組功能異常。考慮到修改系統預設編碼可能導致詭異問題,且會破壞程式碼可一致性,故不建議作此修改。

2.1 str和unicode型別

Python中有兩種字元器型別,分別是str和unicode,它們都是抽象型別basestring的子類。str字串其實是位元組組成的序列,而unicode字串其實是字元組成序列。Python內部以16位元或32位元的整數表示Unicode字串,這取決於Python直譯器的編譯方式。https://docs.python.org/2/howto/unicode.html

可以通過sys模組maxunicode變數值判斷當前所使用的Unicode型別:

>>> import sys; print sys.maxunicode
65535

該變量表示支援的最大Unicode碼點。其值為65535時表示Unicode字元以UCS-2儲存;值為1114111時表示Unicode字元以UCS-4儲存。注意,上述示例為求簡短將多條語句置於一行,實際編碼中應避免如此。

unicode(string[, encoding, errors])函式可根據指定的encoding將string位元組序列轉換為Unicode字串。若未指定encoding引數,則預設使用ASCII編碼(大於127的字元將被視為錯誤)。errors引數指定轉換失敗時的處理方式。其預設值為’strict’,即轉換失敗時觸發UnicodeDecodeError異常。errors引數值為’ignore’時將忽略無法轉換的字元;值為’replace’時將以U+FFFD字元(REPLACEMENT CHARACTER)替換無法轉換的字元。舉例如下:

>>> unicode('abc'+chr(255)+'def', errors='strict')
UnicodeDecodeError: 'ascii' codec can't decode byte 0xff in position 3: ordinal not in range(128)
>>> unicode('abc'+chr(255)+'def', errors='ignore')
u'abcdef'
>>> unicode('abc'+chr(255)+'def', errors='replace')
u'abc\ufffddef'

.encode():unicode字串轉換成某種編碼的位元組序
.decode():某種編碼的位元組序轉換成unicode字串

方法.encode([encoding], [errors=’strict’])可根據指定的encoding將Unicode字串轉換為位元組序列。而.decode([encoding], [errors])根據指定的encoding將位元組序列轉換為Unicode字串,即使用該編碼方式解釋位元組序列。errors引數若取預設值’strict’,則編碼和解碼失敗時會分別觸發UnicodeEncodeError和UnicodeDecodeError異常。注意,unicode(str, encoding)與str.decode(encoding)是等效的

當方法期望Unicode字串,而實際編碼為位元組序列時,Python會先使用預設的ASCII編碼將位元組序列轉換為Unicode字串。例如:

# "ab"和"cd"都是str位元組序列,無資料型別轉換
>>> repr('ab' + 'cd')
"'abcd'"
# str位元組序列"ab"轉換成unicode字元序列
>>> repr('ab' + u'cd')
"u'abcd'"
# str位元組序列"cd"轉換成unicode字元序列
>>> repr(u'ab' + 'cd')
"u'abcd'"
# "ab"和"cd"都是unicode字元序列,無資料型別轉換
>>> repr(u'ab' + u'cd')
"u'abcd'"
# '中文'是位元組序列,預設無法使用ASCII解碼,所以出錯
>>> repr('中文' + u'cd')
UnicodeDecodeError: 'ascii' codec can't decode byte 0xd6 in position 0: ordinal not in range(128)
>>