精品伊人久久大香线蕉,开心久久婷婷综合中文字幕,杏田冲梨,人妻无码aⅴ不卡中文字幕

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
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標準旨在提供統一的編碼格式,以期支持所有字符集(尤其是中日韓等東亞文本)。但其數據解釋的控制機制相當復雜,且

早期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和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的Lib\site-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'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-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'>: 肖('\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, 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)'\xd6\xd0wen' 5 u'\u4e2dwen' 4>>> print s[0], su[0]? 中

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

在源代碼文件中,若字符串常量包含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'\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.encodingsu = u'中文'; print su

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

E:\PyTest\stuff>python test.pycp936中文E:\PyTest\stuff>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'\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 codecsfileObj = 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-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'\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=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:    漢字編碼('\xba\xba\xd7\xd6\xb1\xe0\xc2\xeb') --->[Mike(latin1)] Recv:    oo×?±à??(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-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'\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  :aoo×?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)字符,以排除編碼缺陷。

三. 參考資料

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

本站僅提供存儲服務,所有內容均由用戶發布,如發現有害或侵權內容,請點擊舉報
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
Python 編碼錯誤的本質原因
SyntaxError: Non-ASCII character Python、Unicode和中文
Python 編碼為什么那么蛋疼?
黃聰:解決python中文處理亂碼,先要弄懂“字符”和“字節”的差別
python編碼問題大終結
《源碼探秘 CPython》19. 字符集和字符編碼
更多類似文章 >>
生活服務
分享 收藏 導長圖 關注 下載文章
綁定賬號成功
后續可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點擊這里聯系客服!

聯系客服

主站蜘蛛池模板: 云南省| 饶平县| 安图县| 昆明市| 通化县| 丹东市| 宜城市| 南开区| 菏泽市| 广宁县| 麟游县| 鄂托克前旗| 万源市| 积石山| 南靖县| 炎陵县| 格尔木市| 平陆县| 固镇县| 连城县| 夏邑县| 慈利县| 鲁山县| 泽普县| 得荣县| 德安县| 邯郸市| 习水县| 巨野县| 手机| 靖边县| 衡南县| 当涂县| 金阳县| 伊春市| 乌鲁木齐县| 平利县| 嘉兴市| 金昌市| 柘城县| 边坝县|