Python: 字符編碼基礎及中文亂碼

一、字符編碼基礎

   字符編碼是計算機對字符的格式化,從而能夠在計算機系統中存儲與傳輸。

 1.ASCII碼

   在計算機內部,所有的信息最終都表示爲一個二進制的字符串。每一個二進制位(bit)有0和1兩種狀態,因此八個二進制位就可以組合出 256種狀態,這被稱爲一個字節(byte)。也就是說,一個字節一共可以用來表示256種不同的狀態,每一個狀態對應一個符號,就是256個符號,從 0000000到11111111。

   上個世紀60年代,美國製定了一套字符編碼,對英語字符與二進制位之間的關係,做了統一規定。這被稱爲ASCII碼,一直沿用至今。ASCII碼一共規定了128個字符的編碼,比如空格“SPACE”是32(二進制00100000),大寫的字母A是65(二進制01000001)。這128個符號(包括32個不能打印出來的控制符號),只佔用了一個字節的後面7位,最前面的1位統一規定爲0。

2.非ASCII編碼

   英語用128個符號編碼就夠了,但是用來表示其他語言,128個符號是不夠的。比如,在法語中,字母上方有注音符號,它就無法用ASCII碼錶示。 於是,一些歐洲國家就決定,利用字節中閒置的最高位編入新的符號。比如,法語中的é的編碼爲130(二進制10000010)。這樣一來,這些歐洲國家使 用的編碼體系,可以表示最多256個符號。

   但是,這裏又出現了新的問題。不同的國家有不同的字母,因此,哪怕它們都使用256個符號的編碼方式,代表的字母卻不一樣。比如,130在法語編碼 中代表了é,在希伯來語編碼中卻代表了字母Gimel (ג),在俄語編碼中又會代表另一個符號。但是不管怎樣,所有這些編碼方式中,0—127表示的符號是一樣的,不一樣的只是128—255的這一段。

   至於亞洲國家的文字,使用的符號就更多了,漢字就多達10萬左右。一個字節只能表示256種符號,肯定是不夠的,就必須使用多個字節表達一個符號。 比如,簡體中文常見的編碼方式是GB2312,使用兩個字節表示一個漢字,所以理論上最多可以表示256x256=65536個符號。

3.Unicode

   世界上存在着多種編碼方式,同一個二進制數字可以被解釋成不同的符號。因此,要想打開一個文本文件,就必須知道它的編碼方式,否則用錯誤的編碼方式解讀,就會出現亂碼。爲什麼電子郵件常常出現亂碼?就是因爲發信人和收信人使用的編碼方式不一樣。

   可以想象,如果有一種編碼,將世界上所有的符號都納入其中。每一個符號都給予一個獨一無二的編碼,那麼亂碼問題就會消失。這就是Unicode,就像它的名字都表示的,這是一種所有符號的編碼。Unicode當然是一個很大的集合,現在的規模可以容納100多萬個符號。每個符號的編碼都不一樣,比如,U+0639表示阿拉伯字母 Ain,U+0041表示英語的大寫字母A,U+4E25表示漢字“嚴”。具體的符號對應表,可以查詢unicode.org,或者專門的漢字對應表。

   Unicode的問題

   需要注意的是,Unicode只是一個符號集,它只規定了符號的二進制代碼,卻沒有規定這個二進制代碼應該如何存儲。比如,漢字“嚴”的unicode是十六進制數4E25,轉換成二進制數足足有15位(100111000100101),也就是說這個符號的表示至少需要2個字節。表示其他更大的符號,可能需要3個字節或者4個字節,甚至更多。這裏就有兩個嚴重的問題,第一個問題是,如何才能區別unicode和ascii?計算機怎麼知道三個字節表示一個符號,而不是分別表示三個符號 呢?第二個問題是,我們已經知道,英文字母只用一個字節表示就夠了,如果unicode統一規定,每個符號用三個或四個字節表示,那麼每個英文字母前都必 然有二到三個字節是0,這對於存儲來說是極大的浪費,文本文件的大小會因此大出二三倍,這是無法接受的。它們造成的結果是:

      1)出現了unicode的多種存儲方式,即有許多種不同的二進制格式,可以用來表示unicode。

      2)unicode在很長一段時間內無法推廣,直到互聯網的出現。

4.UTF-8

   互聯網的普及,強烈要求出現一種統一的編碼方式。UTF-8就是在互聯網上使用最廣的一種unicode的實現方式。其他實現方式還包括UTF-16和UTF-32,不過在互聯網上基本不用。重複一遍,這裏的關係是,UTF-8是Unicode的實現方式之一。

   UTF-8最大的一個特點,就是它是一種變長的編碼方式。它可以使用1~4個字節表示一個符號,根據不同的符號而變化字節長度。UTF-8的編碼規則很簡單,只有二條:

      1)對於單字節的符號,字節的第一位設爲0,後面7位爲這個符號的unicode碼。因此對於英語字母,UTF-8編碼和ASCII碼是相同的。

      2)對於n字節的符號(n>1),第一個字節的前n位都設爲1,第n+1位設爲0,後面字節的前兩位一律設爲10。剩下的沒有提及的二進制位,全部爲這個符號的unicode碼。下表總結了編碼規則,字母x表示可用編碼的位。

UTF-8範圍

   下面,還是以漢字“嚴”爲例,演示如何實現UTF-8編碼。

   已知“嚴”的unicode是4E25(100111000100101),根據上表,可以發現4E25處在第三行的範圍內(0000 0800-0000 FFFF),因此“嚴”的UTF-8編碼需要三個字節,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然後,從“嚴”的最後一個二進制位開始,依次從後向前填入格式中的x,多出的位補0。這樣就得到了,“嚴”的UTF-8編碼是 “11100100 10111000 10100101”,轉換成十六進制就是E4B8A5。

5.Windos下Unicode與UTF-8之間的轉換

  在Windows平臺下,有一個最簡單的轉化方法,就是使用內置的記事本小程序Notepad.exe。打開文件後,點擊“文件”菜單中的“另存爲”命令,會跳出一個對話框,在最底部有一個“編碼”的下拉條。

SaveAs

  裏面有四個選項:ANSI,Unicode,Unicode big endian 和 UTF-8。

   1)ANSI是默認的編碼方式。對於英文文件是ASCII編碼,對於簡體中文文件是GB2312編碼(只針對Windows簡體中文版,如果是繁體中文版會採用Big5碼)。

   2)Unicode編碼指的是UCS-2編碼方式,即直接用兩個字節存入字符的Unicode碼。這個選項用的little endian格式。

   3)Unicode big endian編碼與上一個選項相對應。我在下一節會解釋little endian和big endian的涵義。

   4)UTF-8編碼,也就是上一節談到的編碼方法。

  選擇完”編碼方式“後,點擊”保存“按鈕,文件的編碼方式就立刻轉換好了。

  其中:Little endian和Big endian:

  Unicode碼可以採用UCS-2格式直接存儲。以漢字”嚴“爲例,Unicode碼是4E25,需要用兩個字節存儲,一個字節 是4E,另一個字節是25。存儲的時候,4E在前,25在後,就是Big endian方式;25在前,4E在後,就是Little endian方式。

  計算機怎麼知道某一個文件到底採用哪一種方式編碼?

  Unicode規範中定義,每一個文件的最前面分別加入一個表示編碼順序的字符,這個字符的名字叫做”零寬度非換行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。這正好是兩個字節,而且FF比FE大1。如果一個文本文件的頭兩個字節是FE FF,就表示該文件採用大頭方式;如果頭兩個字節是FF FE,就表示該文件採用小頭方式。

--------------------------------------------------------------------------------------

二、python中的中文編碼

  1.python中的unicode、str和print

    unicode,一般指unicode對象,例'哈哈'的unicode對象爲u'/u54c8/u54c8'

    str,是一個字節數組,這個字節數組表示的是對unicode對象(可以是utf-8、gbk、cp936、GB2312)編碼後的存儲的格式。這裏它僅僅是一個字節流,沒有其它的含義,如果你想使這個字節流顯示的內容有意義,就必須用正確的編碼格式來解碼顯示

    print,語句print的的實現是將要輸出的內容傳送給操作系統,操作系統會根據系統的編碼對輸入的字節流進行編碼

  2.unicode、str和print之間的轉換

    我們以中文“哈哈”爲例,“哈哈”的編碼如下:

         <1>定義一個字符串變量test = '哈哈',並查看在計算機中的存儲:

                  

            我們已經看到,'哈哈'在計算機中按照某種編碼方式存儲了下來,而在print的時候,print函數對存儲的test進行了默認編碼輸出。

         <2>將test進行str操作:

                  

             與<1>相同,可見str只是對test編碼後的一個存儲格式。

         <3>將test進行gbk編碼

                  

              test的存儲形式發生了變化,這是因爲不同編碼方式造成的

          <4>將test進行utf-8編碼

      

            這時發生了錯誤,於是我們進行了以下測試:

                                                       

           由此看到,

              (1)我們無法直接對test進行utf-8的編碼,而需要經過對其特定編碼之後纔可以完成utf-8的編碼,這是因爲在python中str和unicode在編碼和解碼過程中,如果將一個str直接編碼成另一種編碼,會先把str解碼成unicode,採用的編碼爲默認編碼,一般默認編碼是anscii,所以在上面示例代碼中第一次轉換的時候會出錯,當設定當前默認編碼爲'gbk'後,就不會出錯了。當然,我們也可以把這個默認編碼給改了,如下:

                  

              (2)由於在對test進行utf-8編碼之後,print語句將要輸出的內容('/xe5/x93/x88/xe5 /x93/x88')傳送了操作系統,操作系統會根據系統的編碼對輸入的字節流進行編碼,從而輸出的是“鍝堝搱”(注:'/xe5/x93/x88/xe5 /x93/x88'用GB2312去解釋,其顯示的出來就是“鍝堝搱”,)

              (3)以上所有例子中print函數所輸出的'哈哈'(gbk編碼)與'鍝堝搱'(GB2312),而gbk是GB2312的擴展,前者兼容後者,可見,print默認編碼應該是gbk了。

               (4)test = u'哈哈'是定義了一個unicode類型的變量,所以可以直接encode,decode就是把其他編碼轉換爲unicode,encode就是把unicode編碼的字符串轉換爲特定編碼,這也是例子中直接從對str類型的test進行encode發生錯誤的原因。

  3.python文件的編碼格式和編碼聲明的作用

   
      (1)文件的編碼格式決定了在該源文件中聲明的字符串的編碼格式,例如:

      str = '哈哈',print repr(str)

      a.如果文件格式爲utf-8,則str的值爲:'/xe5/x93/x88/xe5/x93/x88'(哈哈的utf-8編碼)

      b.如果文件格式爲ANSCII,則str的值爲:'/xb9/xfe/xb9/xfe'(哈哈的ANSCII編碼)

       而在python中的字符串,只是一個字節數組,所以當把a情況的str輸出到 gbk編碼的控制檯時,就將顯示爲亂碼:鍝堝搱;而當把b情況下的str輸出utf-8編碼的控制檯時,就會報錯。

      (2)在py程序的開頭會用#coding=gbk 類似的語句聲明一下編碼,這句話作用是:

 

       a.聲明源文件中將出現非ascii編碼(通常也就是中文);

       b.在高級的IDE中,IDE會將你的文件格式保存成你指定編碼格式。

       c.決定源碼中類似於u'哈哈'這類聲明的將‘哈哈’解碼成unicode所用的編碼格式,例如:

           #coding:gbk

           ss = u'哈哈'

           print repr(ss)

           print 'ss:%s' % ss

       將這個些代碼保存成一個utf-8文本,運行,你認爲會輸出什麼呢?大家第一感覺肯定輸出的肯定是:

          u'/u54c8/u54c8'

          ss:哈哈

      但是實際上輸出是:

          u'/u935d/u581d/u6431'

          ss:鍝堝搱

      爲什麼會這樣,這時候,就是編碼聲明在作怪了,在運行ss = u'哈哈'的時候,整個過程可以分爲以下幾步:

      1) 獲取'哈哈'的編碼:由文件編碼格式確定,爲'/xe5/x93/x88/xe5/x93/x88'(哈哈的utf-8編碼形式)

      2) 轉成 unicode編碼的時候,在這個轉換的過程中,對於'/xe5/x93/x88/xe5/x93/x88'的解碼,不是用utf-8解碼,而是用聲明編碼處指定的編碼GBK,將'/xe5/x93/x88/xe5/x93/x88'按GBK解碼,得到就是''鍝堝搱'',這三個字的unicode編碼就 是u'/u935d/u581d/u6431',至止可以解釋爲什麼print repr(ss)輸出的是u'/u935d/u581d/u6431' 了。

  總結:

     1.任何字符在內存中都是以一種編碼形式存儲,而不是存儲它本身,我們只可能在存入內存之前或者在從內存中拿出來之後,適當的進行編碼與解碼,即可得到我們想要的結果。

     2.打印列表,打印的是所有元素在內存中的形式,而逐個打印列表的元素的時候,print將其傳送給操作系統進行了默認編碼,我們看到的是結果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章