天天看點

掰碎了講中文編碼

電腦用0和1存儲資料,而存儲的資料主要有兩種:數字和字元(還有運算符什麼的暫時不讨論),數字存儲的方法比較簡單,沒什麼問題,這裡要說的是如何存儲字元。

1編碼方式的大曆史

1.1  ASCII

最早對于發明計算機的美國人來說,字元隻有大小寫的字母,于是他們使用一種簡單的編碼方式——ASCII,一個字母對應一個8位二進制碼(ASCII碼),或者說數字0-255,或者說8比特,或者說1個位元組。存儲的内容其實就是這組8位01碼,當使用ASCII編碼方式的軟體被告知用字元的方式顯示這組8位01碼時,它便顯示成字母的樣子,這其實就跟用01碼存儲的數字一樣,當你告訴軟體用數字的方式顯示這組01碼時候,它便顯示數字。

1.2  GBK

但是顯然對于中國人來說,這個256個空間是放不下所有的中文字元的,于是中國人采用另一個針對中文的編碼方式——GBK(最早是GB2312,後來擴充為GBK),用兩個位元組的01對應1個中文字元,也就是說,當使用GBK編碼方式的軟體被告知用字元的方式顯示這組01碼時,它便從GBK的表中讀取對應中文字元并顯示出來(後文會對這個過程詳細讨論)。

1.3  Unicode編碼

但是全世界有上百種語言,日本把日文編到Shift_JIS裡,南韓把韓文編到Euc-kr裡,各國有各國的标準,就會不可避免地出現沖突,結果就是,在多語言混合的文本中,顯示出來會有亂碼。是以,Unicode編碼應運而生。Unicode把所有語言都統一到一套編碼裡,這樣就不會再有亂碼問題了。Unicode用兩個位元組表示一個字元(如果要用到非常偏僻的字元,就需要4個位元組)。現代作業系統和大多數程式設計語言都直接支援Unicode,這裡要說一下,windows7中文系統的預設編碼方式是GBK(起碼txt中的字元是GBK編碼,可能系統核心是Unicode,或者說隻有輸入輸出視窗是GBK,暫時不管)。(另外提一下簡單的編碼轉換,用ASCII編碼的字元顯然是可以轉成Unicode編碼,反過來則不可以,如果把ASCII編碼的A用Unicode編碼,隻需要在前面補0就可以,是以,A的Unicode編碼是00000000 01000001)。也就是說,當使用Unicode編碼方式的軟體被告知用字元的方式顯示這組01碼時,它便從Unicode表中讀取對應中文字元并顯示出來。

1.4  UTF-8編碼

但是新的問題又出現了:如果統一成Unicode編碼,亂碼問題從此消失了。但是,如果你寫的文本基本上全部是英文的話,用Unicode編碼比ASCII編碼需要多一倍的存儲空間,在存儲和傳輸上就十分不劃算。是以,本着節約的精神,又出現了把Unicode編碼轉化為“可變長編碼”的UTF-8編碼。UTF-8編碼把一個Unicode字元根據不同的數字大小編碼成1-6個位元組,常用的英文字母被編碼成1個位元組,漢字通常是3個位元組(其實比GBK占用記憶體更多),隻有很生僻的字元才會被編碼成4-6個位元組。如果你要傳輸的文本包含大量英文字元(對吧,英文),用UTF-8編碼就能節省空間。UTF-8編碼有一個額外的好處,就是ASCII編碼實際上可以被看成是UTF-8編碼的一部分,是以,大量隻支援ASCII編碼的曆史遺留軟體可以在UTF-8編碼下繼續工作。也就是說,當使用UTF-8編碼方式的軟體被告知用字元的方式顯示這組01碼時,它便從UTF-8表中讀取這組01碼對應中文字元并顯示出來。

這裡要強調的是,為了相容所有語言,并給大部分是英文的程式提供便利,在程式員世界,UTF-8碼是通用編碼方式,檔案一般也用這種形式存儲,是以Linux系統使用的就是UTF-8,但是可惜的是,我們現在使用的windows7使用時GBK的編碼方式,這就注定程式設計之路會走得比别人彎。

2編碼、解碼和轉碼

2.1  編碼解碼

上面的用詞都是編碼,編碼就是1建立字元和01碼的一一對應關系2把字元存儲為這個01碼。其中也提到了解碼,解碼就是把這個01碼再變成字元(下文會詳解這個在過程中都發生了些什麼事)。

所有的編碼、解碼和轉碼過程都是通過軟體實作的,而編碼的對應關系也是軟體自己帶有的,是以帶了哪種,預設又是哪種,是各不一樣的,下文會用在win7上寫python程式舉例。

2.2  不同編碼間的轉碼

對于英文字母和數字的字元來說,他們可以直接用ASCII、GBK、Unicode、UTF-8編碼,而這些字元的ASCII編碼與UTF-8編碼一樣,是以對這些字元來說,存在着三種編碼之間的轉換,哪些轉換是可以進行的呢?用圖檔表示簡單友善,左圖左邊四個雙向箭頭表示編碼和解碼,右邊兩個雙向箭頭表示Unicode分别可以與GBK和UTF-8雙向轉碼,而GBK和和UTF-8之間不可進行轉碼。

對于四條轉碼途徑用右圖表示,Python代碼如圖所示,其中也把UTF-8碼和GBK碼到Unicode的過程叫做解碼。

掰碎了講中文編碼
掰碎了講中文編碼

而對于中文字元來說,他們可以直接用GBK、Unicode、UTF-8編碼,不能用ASCII編碼,過程除了左圖第一個雙向箭頭,其餘的跟上圖完全一樣。

3在Win7上寫Python程式

這篇文章的緣起就是寫了個用wxpython庫的Python程式,是以以下的内容就是詳解這個過程中遇到的一些問題。

3.1  Python對編碼的支援特性

因為Python的誕生比Unicode标準釋出的時間還要早,是以最早的Python隻支援ASCII編碼,普通的字元串'ABC'在Python内部都是ASCII編碼的,直接用 ’…’ 來聲明。Python在後來添加了對Unicode的支援,用Unicode編碼的字元串用u'...' 聲明。(而普通的解碼輸出直接用print語句)

Python中實際上有兩種字元串,分别是str類型和unicode類型,這兩者都是basestring的派生類。str類型的編碼方式與源碼檔案完全一緻(py檔案本身也必須被編碼),預設情況下便是标準的ASCII編碼,你可以通過在第一行寫此語句來更改源碼檔案編碼方式:#coding: UTF-8/GBK(此次兩種方式隻可寫一種,而且不可設定為Unicode,而ASCII不用寫,另外值得一提的是這個語句為了尊重其他語言的習慣,被設定成正規表達式識别,是以你可以看到這句話很多其他的寫法),而Unicode類型采用的是自然是Unicode編碼。于是Python通過這樣的方式對四種編碼方式提供了支援。

另外因為py源檔案預設是用ASCII編碼儲存的,如果其中加入了中文注釋或是讓變量存了中文字元,就肯定無法儲存,是以一般情況下,你都會在第一行更改源檔案編碼方式,最好是用UTF-8編碼。

3.2  在Win7上寫Python程式時編碼和解碼過程

下面詳細介紹不同的編碼和解碼過程,編碼和解碼發生在廣義的輸入和輸出情況下。下面輸入介紹四種方式,第一種是直接寫進代碼,第二種是從控制台輸入視窗(标準輸入),第三種是從win7的TXT文檔中讀取資料(從存儲器中讀取),第四種是從現在我用wxpython做的視窗界面的文本框。輸出介紹三種方式,第一種是從控制台輸出視窗(标準輸出),第二種是寫入win7的TXT文檔(把資料寫入存儲器),第三種是從現在我用wxpython做的視窗界面的文本框。

3.2.1  代碼輸入、控制台輸入視窗輸入(标準輸入)、控制台輸出(标準輸出)

(1)代碼輸入

直接寫進代碼的時候是這樣的,我們先建立一個變量,然後定義變量的資料類型(友善在記憶體中劃出空間存儲變量資料),最後把相應的字元串賦給這個變量(當然在程式中這三步是一次完成的),如S = ‘abc’(Python中是動态類型,是以不事先定義變量類型,全靠這對引号來聲明Python S是str類型,這個聲明告訴要賦給變量S的是以源碼編碼方式(前面說了預設是ASCII)編碼的字元串)。于是這個語句讓Python做了這麼幾個事(編碼過程):

1查詢到現在源碼的編碼方式

2按現在源碼的編碼方式在記憶體中開辟對應str類型大小的空間;把字元abc用源碼的編碼方式編碼成01的字元

3把編好的01碼放入開辟的空間中

4把變量S的指針指向這個空間的位址

(2)控制台輸入視窗輸入(标準輸入)

這裡用最常見的raw_Input()内建函數來實作标準輸入,這個函數讀取你在控制台輸入的所有字元,并當做str類型賦給指定的變量,也就是說以源碼編碼方式編碼。代碼如下:

S = raw_input(‘The words what will show to you’)

在執行這行代碼時,控制台輸入視窗便會提示你輸入字元,在你輸入完字元abc并回車之後,你輸入的字元便賦給了變量S,之後發生的事情如同在執行 S=’abc’ 一樣。

(3)控制台輸出(标準輸出)

當從控制台輸出視窗輸出的時候,也就是接到訓示print S的時候它做這麼幾個事(解碼過程):

1定位S指向的位址;查詢到S是str類型

2查詢到現在源碼的編碼方式

3取出記憶體中開辟對應str類型大小的空間中的01碼

4按照現在源碼的編碼方式對01碼進行解碼,也就是從編碼表中找出對應的字元

5把這個字元顯示在輸出視窗即:abc

(4)

在上面這三個過程容易出現這麼幾個錯誤:

1用ASCII編碼方式對中文字元編碼,這顯然會失敗,ASCII做不到

2用UTF-8解碼方式對用GBK編碼了的01碼進行解碼,顯然會顯示出亂碼(由于對應的類型空間都會取錯,這可能就會讀取到不該讀取的内容,有些遊戲BUG就是類似情況産生的)

而在平時我們經常會在沒有聲明源碼檔案用UTF-8編碼時進行中文注釋,在編譯的時候就會發生第1個錯誤,編譯之前要對整個程式編碼儲存,于是便出錯了,同樣這個時候給變量賦予中文字元串時也會出錯。

而當在第一行聲明源碼檔案用UTF-8編碼之後,用中文寫注釋便沒有問題了,另外如果給變量賦予中文字元串也沒有問題(UTF-8編碼),同時列印該變量也沒有問題(UTF-8解碼),但是這個時候如果你從外部檔案中讀取了用GBK編碼的字元進入變量後,再列印,雖然不會報錯,但是輸出會直接出現亂碼,如果你想要顯示正确,就需要進行轉碼。

3.2.2  從win7的TXT文檔中讀取資料(從存儲器中讀取)、寫入win7的TXT文檔(把資料寫入存儲器)

(1)從win7的TXT文檔中讀取資料(從存儲器中讀取)

在說明編碼過程之前先介紹一下在Python中簡單的檔案IO操作,我們知道,在磁盤上讀寫檔案的功能都是由作業系統提供的,現代作業系統不允許普通的程式直接操作磁盤,是以,讀寫檔案就是請求作業系統打開一個檔案對象(通常稱為檔案描述符,一般給位址和名字),然後,通過作業系統提供的接口從這個檔案對象中讀取資料(讀檔案),或者把資料寫入這個檔案對象(寫檔案)。對應代碼如下(為了放在一行顯示,用中文分号隔開):

test1hand = open('test1.txt',’r’);test1 = test1hand.read();test1hand.close()

第一句先用open()函數打開同一目錄下的test1.txt檔案并且傳回這個檔案對象的接口或者叫句柄(就是使用權限,這裡用了參數r便是獲得了讀的權限)賦給變量test1hand。

第二句通過這個句柄的.read()方法,一次讀取檔案的全部内容,注意,讀取的檔案内容為01碼(這裡多說一句,本來對于讀取01碼使用的打開方式是’rb’,但是對于類UNIX系統來說,文本檔案本身就是二進制檔案,這個b是可有可無的,但是windows并不是類unix系統,奇怪的是這裡也并不需要加上b),這個01碼是用什麼編碼方式産生的是由檔案系統本身決定的,讀取的隻是01碼,然後把所有的01碼傳回,在這裡是賦給變量test1,再注意,此時也是str類型賦給它的,也就是說test1的變量類型是str,也就是說系統認為這些01碼是以源碼編碼方式編碼而成的,而假如檔案本身是有GBK編碼的,但是源碼編碼方式是UTF-8,這就出現了一個錯誤,當然這個并不會報錯,繼續注意,在解碼的時候,仍舊不會報錯,但是你會看到出現了錯誤,也就是如果你直接print解碼,它便會按照系統認為的編碼方式來解碼,于是它輸出的字元便會出現亂碼,如果要正确顯示,就需要進行轉碼。(注意:一個.read()方法會把檔案内容獨占,如果在一次打開和關閉檔案之間多次使用這個方法,後面的方法讀取到的是空檔案,傳回的字元是 ‘ ‘)

第三句是調用close()方法關閉檔案。檔案使用完畢後必須關閉,因為檔案對象會占用作業系統的資源,并且作業系統同一時間能打開的檔案數量也是有限的。

是以這個過程沒有發生任何編碼或者解碼,完全是對01碼的操作。

而讀檔案方法除了.read()常用的還有.readline()和.readlines(),甚至還有專用的linecache庫,下面再講一下.readlines()方法,其他的暫時不講。

.readlines()方法其實跟.read()方法差不多,差別隻是它傳回的01碼是按換行符分成一段段,然後組成一個str清單傳回,也就是分行讀取,對清單中的每一個字元串來說,情況跟上面一樣。

(2)寫入win7的TXT文檔(把資料寫入存儲器)

把資料寫入txt文檔的過程跟讀取差不多,代碼如下(為了放在一行顯示,用中文分号隔開):

test1hand = open('test1.txt',’w’); test1hand.write(‘…’/valuename);test1hand.close()

這個過程是一個編碼過程或者不發生任何解碼編碼完全01碼操作過程,如果test1hand.write(‘…’/valuename)中是‘…’,那麼會先把‘…’内容按照源碼編碼方式編碼,然後把生成的01碼寫入檔案,如果其中直接是變量名,那麼它會直接把變量指向的對應01碼寫入檔案。

這裡有個小錯誤不知道為什麼,因為理論上.write()方法是直接把01碼寫入檔案,然後文本檔案被打開時是用作業系統的預設解碼方式對01碼進行解碼顯示,是以理論上對于我這個win7系統,每次打開文本檔案就是它對01碼進行GBK解碼并顯示,如果01碼是用GBK編碼的話,就可以正确顯示字元,如果01碼是用UTF-8或者Unicode編碼的話,就會直接顯示亂碼。這個理論對于GBK編碼和UTF-8編碼都沒錯,但是對于Unicode編碼,在寫入的時候就直接報錯:UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)。

3.2.3從wxpython生成的簡單視窗界面中的文本框

向wxpython生成的簡單視窗界面中的文本框輸出,和把文本框内容當做輸入取得文本框資料,代碼示例如下:

文本框名.SetValue(字元串/字元串變量名)

字元串變量名 = 文本框名.GetValue()

第一句是把右邊括号中的資料傳輸到左邊文本框中,文本框解碼顯示,第二句是把右邊文本框中的資料傳輸進右邊字元串變量中,存儲起來。

這個過程其實并不是特别清楚,因為這有好幾個過程,一個SetValue過程,一個文本框解碼過程,一個GetValue過程,經過測試,已知的情況有這麼幾種,并對過程進行猜測:

(1)GBK編碼的資料經過SetValue過程進入文本框,文本框能解碼并正确顯示,GetValue過程擷取文本框内容傳回Unicode編碼的資料。

過程猜測:GBK編碼的資料經過SetValue過程進入文本框,文本框進行GBK解碼并顯示,GetValue過程擷取文本框字元并進行Unicode編碼并傳回。

(2)Unicode編碼的資料經過SetValue過程進入文本框,文本框能解碼并正确顯示,GetValue過程擷取文本框内容傳回Unicode編碼的資料。

過程猜測:Unicode編碼的資料經過SetValue過程進入文本框,文本框進行Unicode解碼并顯示,GetValue過程擷取文本框字元并進行Unicode編碼并傳回。

(3)UTF-8編碼的資料經過SetValue過程進入文本框,文本框不能解碼并直接報錯,而不是顯示亂碼

總結就是,什麼編碼的資料都能傳輸過去,但是文本框隻能進行GBK和Unicode解碼,而傳輸回來的資料是直接對文本框字元進行Unicode編碼。是以假如你傳輸的GBK碼,正确顯示之後傳回來的就是Unicode碼。

PS:本文在寫作過程中參看了大量網絡資源和書籍,感謝各位程式員無私的分享,在此基礎上加上自己的了解分析和實踐,以更加詳細的方式重新補充整理,希望能解決大家實際問題。在這個過程中其實還遇到了幾個知其然不知其是以然的問題在文章中有标明,另外本人學習程式設計尚無多少時日,才疏學淺,文章定有疏漏,望不吝賜教。另外,本部落格将盡量每周一篇原創技術文章,敬請關注,希望多多交流。