Python2.7字符編碼詳解

Python2.7字符編碼詳解

標籤: Python 字符編碼html


聲明

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

一. 字符編碼基礎

爲明確概念,將字符集的編碼模型分爲如下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旨在成爲通用編碼,其字符清單自己是開放的,以便週期性的添加新的可編碼字符。shell

1.2 已編碼字符集(CCS)

已編碼字符集是從抽象字符清單到非負整數(範圍沒必要連續)的映射。該整數稱爲抽象字符被賦予的碼點(code point,或稱碼位code position),該字符則稱爲已編碼字符。注意,碼點並不是比特或字節,所以與計算機表示無關。碼點的取值範圍由編碼標準限定,該範圍稱爲編碼空間(code space)。在一個標準中,已編碼字符集也稱爲字符編碼、已編碼字符清單、字符集定義或碼頁(code page)。數據庫

在CCS中,須要明肯定義已編碼字符相關的任何屬性。一般,標準爲每一個已編碼字符分配惟一的名稱,例如「拉丁小寫字母A(LATIN SMALL LETTER A)」。當同一個抽象字符出如今不一樣的已編碼字符集且被賦予不一樣的碼點時,經過其名稱可無歧義地標識該字符。但實際應用中廠商或其餘標準組織未必遵循這一機制。Unicode/10646出現後,其通用性使得該機制近乎過期。express

某些工做在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-四、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-196八、iso-ir-六、ANSI_X3.4-198六、ISO_646.irv:199一、ISO646-US、US-ASCII、IBM36七、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,其別名爲cp81九、csISO、Latin一、ibm81九、iso_8859-一、iso_8859-1:198七、iso8859-一、iso-ir-100、l一、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、GB23十二、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库函数的应用示例")。注意,經過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())
    #轉換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    : 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幫助文檔中關於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的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字符串則表示爲unicode類型的實例,可視爲字符的序列(對應C語言裏真正的字符串)。

Python內部以16比特或32比特的整數表示Unicode字符串,這取決於Python解釋器的編譯方式。可經過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([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)編碼形式的字節序列\xd6\xd0\xce\xc4顯然超出ASCII範圍,所以觸發UnicodeDecodeError。

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

>>> def ParseStr(s):
    print '%s: %s(%s), Len: %s' %(type(s), s, repr(s), len(s))
>>> zs = '肖'; ParseStr(zs)
<type 'str'>: 肖('\xd0\xa4'), 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'>: 肖('\xe8\x82\x96'), 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, name
def 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'项目ä­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 Mark
10  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)
'\xd6\xd0wen' 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: u8
print u'漢字', unicode('漢字','u8'), repr(u'漢字')
print '漢字', repr('漢字')
print '中文', repr('中文')
import sys
si = raw_input('漢字$')
print si, repr(si),
print si.decode(sys.stdin.encoding),
print repr(si.decode(sys.stdin.encoding))

運行後Shell裏的結果以下:

漢字 漢字 u'\u6c49\u5b57'
奼夊瓧 '\xe6\xb1\x89\xe5\xad\x97'
中文 '\xe4\xb8\xad\xe6\x96\x87'
奼夊瓧$漢字
漢字 '\xba\xba\xd7\xd6' 漢字 u'\u6c49\u5b57'

顯然,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.encoding
su = u'中文'; print su

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

E:\PyTest\stuff>python test.py
cp936
中文
E:\PyTest\stuff>python test.py > test.txt
UnicodeEncodeError: '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-8

def 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   :', strQw

if __name__ == '__main__':
    ReprCn()

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

[wangxiaoyuan_@localhost ~]$ python reprcn.py 
Enter Chinese: 漢字
漢字 ->
  Unicode : u'\u6c49\u5b57'
  UTF8    : '\xe6\xb1\x89\xe5\xad\x97'
  GBK     : '\xba\xba\xd7\xd6'
  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 codecs
fileObj = codecs.open(r'E:\PyTest\data_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\r\n'
'\xef\xbb\xbf' u'\ufeff'
First line - u'abc\r\n'

使用codecs.open()建立文件時,若編碼指定爲utf-16,則BOM會自動寫入文件,讀取時則自動跳過。而編碼指定爲utf-八、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'\u6d4b\u8bd5.txt']
>>> os.listdir(dir)
['abcu.txt', 'dir1', '\xb2\xe2\xca\xd4.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=gbk
s = '漢字編碼'
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:    漢字編碼('\xba\xba\xd7\xd6\xb1\xe0\xc2\xeb') --->
[Mike(latin1)] Recv:    ºº×Ö±àÂë(u'\xba\xba\xd7\xd6\xb1\xe0\xc2\xeb') ---messy!
[Mike(latin1)] Convert  ('\xba\xba\xd7\xd6\xb1\xe0\xc2\xeb') --->
[Mike(latin1)] to gbk:  漢字編碼(u'\u6c49\u5b57\u7f16\u7801') ---right!

對於utf-8編碼的源文件,將解碼使用的'gbk'改成'utf-8'也可產生和恢復亂碼("æ±‰å­—ç¼–ç ")。

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

#coding=utf-8
def 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'ºº×Ö'
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'新浪博客'.encode('latin_1').decode('utf8')
print u'惨事'.encode('cp1252').decode('utf8')
print u'奼夊瓧緙栫爜'.encode('gbk').decode('utf8')

經過正確地編解碼,能夠徹底消除亂碼:

u'\xba\xba\xd7\xd6' '\xba\xba\xd7\xd6' '\xba\xba\xd7\xd6' '\xba\xba\xd7\xd6'
漢字 漢字 漢字
新浪博客
慘事
漢字編碼

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

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'a\u6c49\u5b57b') ==>
  latin1       :a??b(0x613f3f62)
  gbk->latin1  :aºº×Öb(u'a\xba\xba\xd7\xd6b')
  g->l->g      :a漢字b(u'a\u6c49\u5b57b')
  gbk->utf8    :a����b(u'a\ufffd\ufffd\ufffd\ufffdb')
  g->u->g      :a錕斤拷錕斤拷b(u'a\u951f\u65a4\u62f7\u951f\u65a4\u62f7b')
  utf8->latin1 :a汉字b(u'a\xe6\xb1\x89\xe5\xad\x97b')
  utf8->gbk    :a奼夊瓧b(u'a\u59f9\u590a\u74e7b')

至此,可簡單地總結中文亂碼產生與消除的場景:
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)
肖字輩 '\xd0\xa4\xd7\xd6\xb1\xb2' Ф�ֱ� u'\u0424\ufffd\u05b1\ufffd'

由前文可知,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)字符,以排除編碼缺陷。

三. 參考資料

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

相關文章
相關標籤/搜索