Python2.7字元編碼詳解

Python2.7字元編碼詳解

標籤: Python 字元編碼


[TOC]

聲明

本文主要介紹字元編碼基礎知識,以及Python2.7字元編碼實踐。注意,文中關於Python字元編碼的解釋和建議適用於Python2.x版本,而不適用於3.x版本。本文同時也發佈於作業部落,閱讀體驗可能更好。

一. 字元編碼基礎

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

  • 抽象字元清單(Abstract Character Repertoire, ACR):待編碼文字和符號的無序集合,包括各國文字、標點、圖形符號、數字等。
  • 已編碼字符集(Coded Character Set, CCS):從抽象字元清單到非負整數碼點(code point)集合的映射。
  • 字元編碼格式(Character Encoding Form, CEF):從碼點集合到指定寬度(如32比特整數)編碼單元(code unit)的映射。
  • 字元編碼方案(Character Encoding Scheme, CES):從編碼單元序列集合(一個或多個CEF)到一個串列化位元組序列的可逆轉換。
  • 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)」。當同一個抽象字元出現在不同的已編碼字符集且被賦予不同的碼點時,通過其名稱可無歧義地標識該字元。但實際應用中廠商或其他標準組織未必遵循這一機制。Unicode/10646出現後,其通用性使得該機制近乎過時。

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

    1.3 字元編碼格式(CEF)

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

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

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

    一個碼點未必對應一個編碼單元。很多編碼格式將一個碼點映射為多個編碼單元的序列,例如微軟碼頁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的翻轉而相互轉化,等等。初創標準的影響力如此之強,以致於後世所有廣泛應用的編碼標準都要兼容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碼錶。在Windows記事本里,通過ALT+Latin1碼點10進位值可輸入相應字元。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日開始實施。標準號是GB 2312—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碼位空間可劃分為以下區域:

    注意,碼位空間中的碼位並非都已編碼,例如0xA2E3和0xA2E4並未定義編碼。

    為擴展碼位空間,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碼位定義如下:

    可見,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表示可用編碼的比特位):

    亦即: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編碼後不含零位元組,因此可由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 編碼適用場景

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

    1.4 字元編碼方案(CES)

    字元編碼方案主要關注跨平台處理編碼單元寬度超過一個位元組的數據。

    大多數等寬的單位元組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)代替。

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

    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,即可正常顯示。

    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字元),否則編譯器或解釋器可能會以錯誤的編碼格式去解析源代碼。

    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?o??????°????o???¨?¤o???")。注意,通過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, localedef SysCoding(): fmt = "{0}: {1}" #當前系統所使用的默認字元編碼 print fmt.format("DefaultEncoding ", sys.getdefaultencoding()) #轉換Unicode文件名至系統文件名時所用的編碼("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 : asciiFileSystemEncoding : mbcsDefaultLocale : ("zh_CN", "cp936")PreferredEncoding : cp936StdinEncoding : cp936StdoutEncoding : cp936

    如無特殊說明,本節所有代碼片段均在這台Windows主機上執行。

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

    作為對比,其他兩台Linux主機字元編碼信息分別為:

    #Linux 1DefaultEncoding : asciiFileSystemEncoding : UTF-8DefaultLocale : ("zh_CN", "utf")PreferredEncoding : UTF-8StdinEncoding : UTF-8StdoutEncoding : UTF-8#Linux 2DefaultEncoding : asciiFileSystemEncoding : ANSI_X3.4-1968 #ASCII規範名DefaultLocale : (None, None)PreferredEncoding : ANSI_X3.4-1968StdinEncoding : ANSI_X3.4-1968StdoutEncoding : ANSI_X3.4-1968

    可見,StdinEncoding、StdoutEncoding與FileSystemEncoding保持一致。這就可能導致Python腳本編輯器和解釋器(CPython 2.7)的代碼運行差異,後文將會給出實例。此處先引用Python幫助文檔中關於stdinstdout的描述:

    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;當printraw_input()向Shell輸出中文字元串時,該字元串按照cp936解碼。

    通過sys.setdefaultencoding()可修改當前系統所使用的默認字元編碼。例如,在python27的Libsite-packages目錄下新建sitecustomize.py腳本,內容為:

    #encoding=utf8 import sysreload(sys)sys.setdefaultencoding("utf8")

    重啟Python解釋器後執行sys.getdefaultencoding(),會發現默認編碼已改為UTF-8。多次重啟之後仍有效,這是因為Python啟動時自動調用該文件設置系統默認編碼。而在作者的環境下,無論是Shell執行還是源代碼中添加上述語句,均無法修改系統默認編碼,反而導致sys模塊功能異常。考慮到修改系統默認編碼可能導致詭異問題,且會破壞代碼可一致性,故不建議作此修改。

    2.1 str和unicode類型

    Python中有兩種字元串類型,分別是str和unicode,它們都由抽象類型basestring派生而來。str字元串其實是位元組組成的序列,unicode字元串則表示為unicode類型的實例,可視為字元的序列(對應C語言里真正的字元串)。

    Python內部以16比特或32比特的整數表示Unicode字元串,這取決於Python解釋器的編譯方式。可通過sys模塊maxunicode變數值判斷當前所使用的Unicode類型:

    >>> import sys; print sys.maxunicode65535

    該變數表示支持的最大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"abcufffddef"

    方法.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字元串。例如:

    >>> repr("ab" + u"cd")"u"abcd"">>> repr("abc".encode("gbk"))""abc"">>> repr("中文".encode("gbk"))UnicodeDecodeError: "ascii" codec can"t decode byte 0xd6 in position 0: ordinal not in range(128)

    在字元串拼接前,Python通過"ab".decode(sys.getdefaultencoding())將"ab"轉換為u"ab",然後將兩個Unicode字元串合併。在中文編碼前,Python試圖通過類似的方式對"中文"解碼,但sys.stdin(gbk)編碼形式的位元組序列xd6xd0xcexc4顯然超出ASCII範圍,因此觸發UnicodeDecodeError。

    若要將一個str類型轉換成特定的編碼形式(如utf-8、gbk等),可先將其轉為Unicode類型,再從Unicode轉為特定的編碼形式。例如:

    >>> def ParseStr(s): print "%s: %s(%s), Len: %s" %(type(s), s, repr(s), len(s))>>> zs = "肖"; ParseStr(zs)<type "str">: 肖("xd0xa4"), Len: 2>>> import sys; zs_u = zs.decode(sys.stdin.encoding)>>> ParseStr(zs_u)<type "unicode">: 肖(u"u8096"), Len: 1>>> zs_utf = zs_u.encode("utf8")>>> ParseStr(zs_utf)<type "str">: 肖("xe8x82x96"), Len: 3

    其中,"肖"為Shell標準輸入的中文字元,編碼為cp936(sys.stdin.encoding)。經過解碼和編碼後,"肖"從cp936編碼正確轉換為utf-8編碼。

    type()外,還可用isinstance()判斷字元串類型:

    >>> isinstance(zs, str), isinstance(zs, unicode), isinstance(zs, basestring)(True, False, True)>>> isinstance(zs_u, str), isinstance(zs_u, unicode), isinstance(zs_u, basestring)(False, True, True)

    通過以下代碼可查看Unicode字元名、類別等信息:

    from unicodedata import category, namedef ParseUniChar(uni): for i, c in enumerate(uni): print "%2d U+%04X [%s]" %(i, ord(c), category(c)), print name(c, "Unknown").title()

    執行ParseUniChar(u"é?1????-C??")後結果如下:

    0 U+00E9 [Ll] Latin Small Letter E With Acute 1 U+00A1 [Po] Inverted Exclamation Mark 2 U+00B9 [No] Superscript One 3 U+00E7 [Ll] Latin Small Letter C With Cedilla 4 U+009B [Cc] Unknown 5 U+00AE [So] Registered Sign 6 U+00E4 [Ll] Latin Small Letter A With Diaeresis 7 U+00AD [Cf] Soft Hyphen 8 U+0043 [Lu] Latin Capital Letter C 9 U+00BF [Po] Inverted Question Mark10 U+00BC [No] Vulgar Fraction One Quarter

    其中,類別縮寫"Ll"表示"字母,小寫(Letter, lowercase)","Po"表示"標點,其他(Punctuation, other)",等等。詳見Unicode通用類別值。

    2.2 源碼字元串常量(Literals)

    Python源碼中,Unicode字元串常量書寫時添加"u"或"U"前綴,如u"abc"。當源代碼文件編碼格式為utf-8時,u"中"等效於"中".decode("utf8");當源代碼文件編碼格式為gbk時,u"中"等效於"中".decode("gbk")。換言之,源文件的編碼格式決定該源文件中字元串常量的編碼格式。

    注意,不建議使用from __future__ import unicode_literals特性(可免除Unicode字元串前綴"u"),這會引發兼容性問題。

    Unicode字元串使得中文更容易處理,參考以下實例:

    >>> s = "中wen"; su = u"中wen">>> print repr(s), len(s), repr(su), len(su)"xd6xd0wen" 5 u"u4e2dwen" 4>>> print s[0], su[0]? 中

    可見,Unicode字元串長度以字元為單位,故len(su)為4,且su[0]對應第一個字元"中"。相比之下,s[0]截取"中"的第一個位元組,即0xD6,該值正好對應ASCII碼錶中的"?"。

    在源代碼文件中,若字元串常量包含ASCII(Python腳本默認編碼)以外的字元,則需要在文件首行或第二行聲明字元編碼,如#-*- coding: utf-8 -*-。實際上,Python只檢查注釋中的coding: namecoding=name,且字元編碼通常還有別名,因此也可寫為#coding:utf-8#coding=u8

    若不聲明字元編碼,則字元串常量包含非ASCII字元時,將無法保存源文件。若聲明的字元編碼不適用於非ASCII字元,則會觸發無效編碼的I/O Error,並提示保存為帶BOM的UTF-8文件 。保存後,源文件中的字元串常量將以UTF-8編碼,無論編碼聲明如何。而此時再運行,會提示存在語法錯誤,如"encoding problem: gbk with BOM"。所以,務必確保源碼聲明的編碼與文件實際保存時使用的編碼一致。

    此外,源文件里的非ASCII字元串常量,應採用添加Unicode前綴的寫法,而不要寫為普通字元串常量。這樣,該字元串將為Unicode編碼(即Python內部編碼),而與文件及終端編碼無關。參考如下實例:

    #coding: u8print u"漢字", unicode("漢字","u8"), repr(u"漢字")print "漢字", repr("漢字")print "中文", repr("中文")import syssi = raw_input("漢字$")print si, repr(si),print si.decode(sys.stdin.encoding),print repr(si.decode(sys.stdin.encoding))

    運行後Shell里的結果如下:

    漢字 漢字 u"u6c49u5b57"奼夊瓧 "xe6xb1x89xe5xadx97"中文 "xe4xb8xadxe6x96x87"奼夊瓧$漢字漢字 "xbaxbaxd7xd6" 漢字 u"u6c49u5b57"

    顯然,raw_input()的提示輸出編碼為cp936,因此誤將源碼中utf-8編碼的"漢字"按照cp936輸出為"奼夊瓧";raw_input()的輸入編碼也為cp936,這從repr和解碼結果可以看出。

    注意,"漢字"被錯誤輸出,u"漢字"卻能正常輸出。這是因為,當Python檢測到輸出與終端連接時,設置sys.stdout.encoding屬性為終端編碼。print會自動將Unicode參數編碼為str輸出。若Python檢測不到輸出所期望的編碼,則設置sys.stdout.encoding屬性為None並調用ASCII codec(默認編碼)強制將Unicode字元串轉換為位元組序列。

    這種處理會導致比較有趣的現象。例如,將以下代碼保存為test.py:

    # -*- coding: utf-8 -*-import sys; print "Enc:", sys.stdout.encodingsu = u"中文"; print su

    在cmd命令提示符中分別運行python test.pypython test.py > test.txt,結果如下:

    E:PyTeststuff>python test.pycp936中文E:PyTeststuff>python test.py > test.txtUnicodeEncodeError: "ascii" codec can"t encode characters in position 0-1: ordinal not in range(128)

    打開test.txt文件,可看到內容為"Enc: None"。這是因為,print到終端控制台時Python會自動調用ASCII codec(默認編碼)強制轉換編碼,而write到文件時則不會。將輸出語句改為print su.encode("utf8")即可正確寫入文件。

    最後,藉助sys.stdin.encoding屬性,可編寫小程序顯示漢字的主流編碼形式。如下所示(未考慮錯誤處理):

    #!/usr/bin/python#coding=utf-8def ReprCn(): strIn = raw_input("Enter Chinese: ") import sys encoding = sys.stdin.encoding print unicode(strIn, encoding), "->" print " Unicode :", repr(strIn.decode(encoding)) print " UTF8 :", repr(strIn.decode(encoding).encode("utf8")) strGbk = strIn.decode(encoding).encode("gbk") strQw = "".join([str(x) for x in ["%02d"%(ord(x)-0xA0) for x in strGbk]]) print " GBK :", repr(strGbk) print " QuWei :", strQwif __name__ == "__main__": ReprCn()

    以上程序保存為reprcn.py後,在控制台里執行python reprcn.py命令,並輸入目標漢字:

    [wangxiaoyuan_@localhost ~]$ python reprcn.py Enter Chinese: 漢字漢字 -> Unicode : u"u6c49u5b57" UTF8 : "xe6xb1x89xe5xadx97" GBK : "xbaxbaxd7xd6" QuWei : 26265554

    2.3 讀寫Unicode數據

    在寫入磁碟文件或通過套接字發送前,通常需要將Unicode數據轉換為特定的編碼;從磁碟文件讀取或從套接字接收的位元組序列,應轉換為Unicode數據後再處理。

    這些工作可以手工完成。例如:使用內置的open()方法打開文件後,將read()讀取的str數據,按照文件編碼格式進行decode();write()寫入前,將Unicode數據按照文件編碼格式進行encode(),或將其他編碼格式的str數據先按該str的編碼decode()轉換為Unicode數據,再按照文件編碼格式encode()。若直接將Unicode數據傳入write()方法,Python將按照源代碼文件聲明的字元編碼進行encode()後再寫入。

    這種手工轉換的步驟可簡記為"due",即:1) Decode early(將文件內容轉換為Unicode數據)2) Unicode everywhere(程序內部處理都用Unicode數據)3) Encode late(存檔或輸出前encode回所需的編碼)

    然而,並不推薦這種手工轉換。對於多位元組編碼(一個Unicode字元由多個位元組表示),若以塊方式讀取文件,如一次讀取1K位元組,可能會切割開同屬一個Unicode字元的若干位元組,因此必須對每塊末尾的位元組做錯誤處理。一次性讀取整個文件內容後再解碼固然可以解決該問題,但這樣就無法處理超大的文件,因為內存中需要同時存儲已編碼位元組序列及其Unicode版本。

    解決方法是使用codecs模塊,該模塊包含open()read()write()等方法。其中,open(filename, mode="rb", encoding=None, errors="strict", buffering=1)按照指定的編碼打開文件。若encoding參數為None,則返回接受位元組序列的普通文件對象;否則返回一個封裝對象,且讀寫該對象時數據編碼會按需自動轉換。

    Windows記事本以非Ansi編碼保存文件時,會在文件開始處插入Unicode字元U+FEFF作為位元組順序標記(BOM),以協助文件內容位元組序的自動檢測。例如,以utf-8編碼保存文件時,文件開頭被插入三個不可見的字元(0xEF 0xBB 0xBF)。讀取文件時應手工剔除這些字元:

    import codecsfileObj = codecs.open(r"E:PyTestdata_utf8.txt", encoding="utf-8")uContent = fileObj.readline()print "First line +", repr(uContent)#剔除utf-8 BOM頭uBomUtf8 = unicode(codecs.BOM_UTF8, "utf8")print repr(codecs.BOM_UTF8), repr(uBomUtf8)if uContent.startswith(uBomUtf8): uContent = uContent.lstrip(uBomUtf8)print "First line -", repr(uContent)fileObj.close()

    其中,data_utf8.txt為記事本以utf-8編碼保存的文件。執行結果如下:

    First line + u"ufeffabc
    ""xefxbbxbf" u"ufeff"First line - u"abc
    "

    使用codecs.open()創建文件時,若編碼指定為utf-16,則BOM會自動寫入文件,讀取時則自動跳過。而編碼指定為utf-8、utf-16le或utf-16be時,均不會自動添加和跳過BOM。注意,編碼指定為utf-8-sig時行為與utf-16類似。

    2.4 Unicode文件名

    現今的主流操作系統均支持包含任意Unicode字元的文件名,並將Unicode字元串轉換為某種編碼。例如,Mac OS X系統使用UTF-8編碼;而Windows系統使用可配置的編碼,當前配置的編碼在Python中表示為"mbcs"(即Ansi)。在Unix系統中,可通過環境變數LANG或LC_CTYPE設置唯一的文件系統編碼;若未設置則默認編碼為ASCII。

    os模塊內的函數也接受Unicode文件名。PEP277(Windows系統Unicode文件名支持)中規定:

    當open函數的filename參數為Unicode編碼時,文件對象的name屬性也為Unicode編碼。文件對象的表達,即repr(f),將顯示Unicode文件名。posix模塊包含chdir、listdir、mkdir、open、remove、rename、rmdir、stat和_getfullpathname等函數。它們直接使用Unicode編碼的文件和目錄名參數,而不再轉換(為mbcs編碼)。對rename函數而言,當任一參數為Unicode編碼時觸發上述行為,且使用默認編碼將另一參數轉換為Unicode編碼。當路徑參數為Unicode編碼時,listdir函數將返回一個Unicode字元串列表;否則返回位元組序列列表。

    注意,根據建議,不應直接import posix模塊,而要import os模塊。這樣移植性更好。

    os.listdir()方法比較特殊,參考以下實例:

    >>> import os, sys; dir = r"E:PyTest調試">>> os.listdir(unicode(dir, sys.stdin.encoding))[u"abcu.txt", u"dir1", u"u6d4bu8bd5.txt"]>>> os.listdir(dir)["abcu.txt", "dir1", "xb2xe2xcaxd4.txt"]>>> print os.listdir(dir)[2].decode(sys.getfilesystemencoding())測試.txt>>> fs = os.listdir(unicode(dir, sys.stdin.encoding))[2].encode("mbcs")>>> print open(os.path.join(dir, fs), "r").read()?abc中文

    可見,Shell里輸入的路徑字元串常量中的中文字元以gbk編碼,而文件系統也為gbk編碼("mbcs"),因此調用os.listdir()時既可傳入Unicode路徑也可傳入普通位元組序列路徑。對比之下,若在編碼聲明為utf-8的源代碼文件中調用os.listdir(),因為路徑字元串常量中的中文字元以utf-8編碼,必須先以unicode(dir, "u8")轉換為Unicode字元串,否則會產生"系統找不到指定的路徑"的錯誤。若要屏蔽編碼差異,可直接添加Unicode前綴,即os.listdir(u"E:\PyTest\測試")

    2.5 處理中文亂碼

    本節主要討論編碼空間不兼容導致的中文亂碼。

    亂碼可能發生在print輸出、寫入文件、資料庫存儲、網路傳輸、調用shell程序等過程中。解決方法分為事前事後:事前可約定相同的字元編碼,事後則根據實際編碼在代碼側重新轉換。例如,簡體中文Windows系統默認編碼為GBK,Linux系統編碼通常為en_US.UTF-8。那麼,在跨平台處理文件前,可將Linux系統編碼修改為zh_CN.UTF-8或zh_CN.GBK。

    關於代碼側處理亂碼,可參考一個簡單的亂碼產生與消除示例:

    #coding=gbks = "漢字編碼"print "[John(gb2312)] Send: %s(%s) --->" %(s, repr(s))su_latin = s.decode("latin1")print "[Mike(latin1)] Recv: %s(%s) ---messy!" %(su_latin, repr(su_latin))

    其中,John向Mike發送gb2312編碼的字元序列,Mike收到後以本地編碼latin1解碼,顯然會出現亂碼。假設此時Mike獲悉John以gb2312編碼,但已無法訪問原始字元序列,那麼接下來該怎麼消除亂碼呢?根據前文的字元編碼基礎知識,可先將亂碼恢復為位元組序列,再以gbk編碼去"解釋"(解碼)該字元序列,即:

    s_latin = su_latin.encode("latin1")print "[Mike(latin1)] Convert (%s) --->" %repr(s_latin)su_gb = s_latin.decode("gbk")print "[Mike(latin1)] to gbk: %s(%s) ---right!" %(su_gb, repr(su_gb))

    將亂碼的產生和消除代碼合併,其運行結果如下:

    [John(gb2312)] Send: 漢字編碼("xbaxbaxd7xd6xb1xe0xc2xeb") --->[Mike(latin1)] Recv: oo×?±à??(u"xbaxbaxd7xd6xb1xe0xc2xeb") ---messy![Mike(latin1)] Convert ("xbaxbaxd7xd6xb1xe0xc2xeb") --->[Mike(latin1)] to gbk: 漢字編碼(u"u6c49u5b57u7f16u7801") ---right!

    對於utf-8編碼的源文件,將解碼使用的"gbk"改為"utf-8"也可產生和恢復亂碼("?±??-????? ?")。

    可見,亂碼消除的步驟為:1)將亂碼位元組序列轉換為Unicode字元串;2)將該串"打散"為單位元組數組;3)按照預期的編碼規則將位元組數組解碼為真實的字元串。顯然,"打散"的步驟既可編碼轉換也可手工解析。例如下述代碼中的Dismantle()函數,就等效於encode("latin1")

    #coding=utf-8def Dismantle(messyUni): return "".join([chr(x) for x in [ord(x) for x in messyUni]])def Dismantle2(messyUni): return reduce(lambda x,y: "".join([x,y]), map(lambda x: chr(ord(x)), messyUni))su = u"oo×?"s1 = su.encode("latin1"); s2 = Dismantle(su); s3 = Dismantle2(su)print repr(su), repr(s1), repr(s2), repr(s3)print s1.decode("gbk"), s2.decode("gbk"), s3.decode("gbk")print u"??°?μa?????¢".encode("latin_1").decode("utf8")print u"??¨?o?".encode("cp1252").decode("utf8")print u"奼夊瓧緙栫爜".encode("gbk").decode("utf8")

    通過正確地編解碼,可以完全消除亂碼:

    u"xbaxbaxd7xd6" "xbaxbaxd7xd6" "xbaxbaxd7xd6" "xbaxbaxd7xd6"漢字 漢字 漢字新浪博客慘事漢字編碼

    更進一步,考慮中文字元在不同編碼間的轉換場景。以幾種典型的編碼形式為例:

    su = u"a漢字b"sl = su.encode("latin1", "replace")su_g2l = su.encode("gbk").decode("latin1")su_glg = su.encode("gbk").decode("latin1").encode("latin1").decode("gbk")su_g2u = su.encode("gbk").decode("utf8", "replace")su_gug = su.encode("gbk").decode("utf8", "replace").encode("utf8").decode("gbk")su_u2l = su.encode("utf8").decode("latin1")su_u2g = su.encode("utf8").decode("gbk")print "Convert %s(%s) ==>" %(su, repr(su))print " latin1 :%s(0x%s)" %(sl, sl.encode("hex"))print " gbk->latin1 :%s(%s)" %(su_g2l, repr(su_g2l))print " g->l->g :%s(%s)" %(su_glg, repr(su_glg))print " gbk->utf8 :%s(%s)" %(su_g2u, repr(su_g2u))print " g->u->g :%s(%s)" %(su_gug, repr(su_gug))print " utf8->latin1 :%s(%s)" %(su_u2l, repr(su_u2l))print " utf8->gbk :%s(%s)" %(su_u2g, repr(su_u2g))

    運行結果如下:

    Convert a漢字b(u"au6c49u5b57b") ==> latin1 :a??b(0x613f3f62) gbk->latin1 :aoo×?b(u"axbaxbaxd7xd6b") g->l->g :a漢字b(u"au6c49u5b57b") gbk->utf8 :a????b(u"aufffdufffdufffdufffdb") g->u->g :a錕斤拷錕斤拷b(u"au951fu65a4u62f7u951fu65a4u62f7b") utf8->latin1 :a?±??-?b(u"axe6xb1x89xe5xadx97b") utf8->gbk :a奼夊瓧b(u"au59f9u590au74e7b")

    至此,可簡單地總結中文亂碼產生與消除的場景:1) 一個漢字對應一個問號當以latin1編碼將Unicode字元串轉換為位元組序列時,由於一個Unicode字元對應一個位元組,無法識別的Unicode字元將被替換為0x3F,即問號"?"。2) 一個漢字對應兩個EASCII或若干U+FFFD字元當以gbk編碼將Unicode字元串轉換為位元組序列時,由於一個Unicode字元對應兩個位元組,再以latin1編碼轉換為字元串時,將會出現兩個EASCII字元。然而,這種亂碼是可以恢復的。因為latin1是單位元組編碼,且覆蓋單位元組所有取值範圍,以該編碼傳輸、存儲和轉換位元組流絕不會造成數據丟失。當以utf-8編碼轉換為字元串時,結果會略為複雜。通常,gbk編碼的位元組序列不符合utf-8格式,無法識別的位元組會被替換為U+FFFD"(REPLACEMENT CHARACTER)字元,再也無法恢復。以上示例中"漢字"對應四個U+FFFD,即一個漢字對應兩個U+FFFD。但某些gbk編碼恰巧"符合"utf-8格式,例如:

    >>> su_gbk = u"肖字輩".encode("gbk")>>> s_utf8 = su_gbk.decode("utf-8", "replace")>>> print su_gbk, repr(su_gbk), s_utf8, repr(s_utf8)肖字輩 "xd0xa4xd7xd6xb1xb2" Ф??? u"u0424ufffdu05b1ufffd"

    由前文可知,utf-8規則可簡記為(0),(110,10),(1110,10,10)。"肖字輩"以gbk編碼轉換的位元組序列中,0xd0a4因符合(110,10)被解碼為U+0424,對應斯拉夫(Cyrillic)大寫字母Ф;0xd7d6因部分符合(110,10)規則,0xd7被替換為U+FFFD,並從0xd6開始繼續解碼;0xd6b1因符合(110,10)被解碼為U+05b1,對應希伯來(Hebrew)非間距標記;最後,0xb2因不符合所有utf-8規則,被替換為U+FFFD。此時,"肖字輩"對應兩個U+FFFD。也可看出,若原始字元串為"肖",其實是可以恢復亂碼的。總體而言,將gbk編碼的位元組序列以utf-8解碼時,可能導致無法恢復的錯誤。3)兩個漢字對應六個EASCII或三個其他漢字當以utf-8編碼將Unicode字元串轉換為位元組序列時,由於一個Unicode字元對應三個位元組,再以latin1編碼轉換為字元串時,將會出現三個EASCII字元。當以gbk編碼轉換為字元串時,由於兩個位元組對應一個漢字,因此原始字元串中的兩個漢字被轉換為三個其他漢字。

    2.6 中文處理建議

    Python2.x中默認編碼為ASCII,而Python3中默認編碼為Unicode。因此,如果可能應儘快遷移到Python3。否則,應遵循以下建議:1) 源代碼文件使用字元編碼聲明,且保存為所聲明的編碼格式。同一工程中的所有源代碼文件也應使用和保存為相同的字元編碼。若工程跨平台,應盡量統一為UTF-8編碼。2) 程序內部全部使用Unicode字元串,只在輸出時轉換為特定的編碼。對於源碼內的字元串常量,可直接添加Unicode前綴("u"或"U");對於從外部讀取的位元組序列,可按照"Decode early->Unicode everywhere->Encode late"的步驟處理。但按照"due"步驟手工處理文件時不太方便,可使用codecs.open()方法替代內置的open()。此外,小段程序的編碼問題可能並不明顯,若能保證處理過程中使用相同編碼,則無需轉換為Unicode字元串。例如:

    >>> import re>>> for i in re.compile("測試(.*)").findall("測試一二三"): print i 一二三

    3) 並非所有Python2.x內置函數或方法都支持Unicode字元串。這種情況下,可臨時以正確的編碼轉換為位元組序列,調用內置函數或方法完成操作後,立即以正確的編碼轉換為Unicode字元串。4) 通過encode()和decode()編解碼時,需要確定待轉換字元串的編碼。除顯式約定外,可通過以下方法猜測編碼格式:a.檢測文件頭BOM標記,但並非所有文件都有該標記;b.使用chardet.detect(str),但字元串較短時結果不準確;c.國際化產品最有可能使用UTF-8編碼。5) 避免在源碼中顯式地使用"mbcs"(別名"dbcs")和"utf_16"(別名"U16"或"utf16")的編碼。"mbcs"僅用於Windows系統,編碼因當前系統ANSI碼頁而異。Linux系統的Python實現中並無"mbcs"編碼,代碼移植到Linux時會出現異常,如報告AttributeError: "module" object has no attribute "mbcs_encode"。因此,應指定"gbk"等實際編碼,而不要寫為"mbcs"。"utf_16"根據操作系統原生位元組序指代"utf_16_be"或"utf_16_le"編碼,也不利於移植。6) 不要試圖編寫可同時處理Unicode字元串和位元組序列的函數,這樣很容易引入缺陷。7) 測試數據中應包含非ASCII(包括EASCII)字元,以排除編碼缺陷。

    三. 參考資料

    除前文已給出的鏈接外,本文還參考以下資料(包括但不限於):

  • 字符集和字元編碼(Charset&Encoding)
  • 程序員趣味讀物:談談Unicode編碼
  • 字元,位元組和編碼
  • Unicode、GB2312、GBK和GB18030中的漢字
  • Character Set Encoding Basics
  • Understanding Unicode A general introduction to the Unicode Standard
  • Short overview of ISO-IEC 10646 and Unicode
  • 亂碼-維基百科
  • 推薦閱讀:

    泰坦尼克號生還率影響因素分析
    【阿里雲短消息服務】每天一條問候簡訊
    推薦一些相見恨晚的 Python 庫 「二」
    黃哥Python推薦免費Python電子書
    解構國內首個函數計算(從概念、入門再到實戰)

    TAG:Python | 字元編碼 | 編碼 | 字元 |