本文淺顯易懂,綠色純天然,手工制作,請放心閱讀。
編碼問題是一個很大很雜的話題,要向徹底的講明白可以寫一本書了。導致亂碼的原因很多,系統平台、編程語言、多國語言、軟件程序支持、用戶選擇等都可能導致無法正確的解析編碼。
導致亂碼的主要原因可以簡單歸結於文本的編碼方式和解碼方式不同導致的。本文將通過在win7(zh-cn)系統下分析python2.7的編解碼問題來簡單窺探一下編碼的冰山一角。
今后遇到編碼問題時能夠多一點分析解決思路,要是能起到一個拋磚引玉的作用,那就再好不過了。
1.為什么需要編碼
物理存儲介質上的基本存儲單元只能有兩種狀態,使用0和1區分。一段連續的0,1序列可以表示特定的信息。
理論上任何字符都可以被唯一的一個連續0,1組成的bit序列表示。如果這樣的一個bit序列也唯一的代表一個字符,那么我們可以由此建立一個bit序列和字符之間的轉換關系。
數字0 - 9和字母a - z(A - Z)等是人類可識別的字符,但是無法存入到計算機的存儲介質中。
十進制 | 二進制(bit 序列) | 字符 |
48 | 0011 0000 | 0 |
65 | 0100 0001 | A |
97 | 0110 0001 | a |
為此,我們將這些人類可識別的有意義的字符與特定0,1組成的bit序列建立起一一對應的關系。
由字符轉成對應的bit序列的過程稱為編碼,將bit序列解釋成對應的字符則稱之為解碼。
這樣的一個bit序列可以用於存盤和網絡傳輸等所有只能使用二進制表示的環境中。當需要閱讀文件時再解碼並在顯示屏上顯示出人類可識別的字符。
編碼是人和機器之間的傳遞和表示信息的一種方式。
2.編碼方式
字符與bit序列之間的轉換隨之帶來的兩個問題是:
- 怎么確定每個字符對應的編碼?
- 每個字符的編碼長度應該是多少?
第一個問題可以用一個大家公認的標准來解決。
第二個問題則主要是節約的角度考慮,在可以表示特定字符集的情況下,需要用盡可能短的二進制序列。
問題看似簡單,但由於歷史原因,不同國家不同語種的編碼標准並不相同。而且即使同一個標准也在不斷的發展。
我們常見的編碼標准ASCII,UTF-8,Unicode,GBK(中文編碼標准)等。
編碼長度由編碼方式決定,如ASCII碼表示的字符都是一個字節(8 bits),Unicode編碼的字符一般用兩個字節。
同一種編碼中不同字符的編碼長度也可能不同。如UTF-8,對字符編碼盡量壓縮以節儉空間,是一種“可變長編碼”。
既然存在這么多種編碼方式,那么對於一段經過編碼的二進制序列,如果以其他的編碼方式解碼,顯然會得到錯誤的解碼信息。
這就是我們所遇到的亂碼問題。
那有沒有一種編碼方式可以將世界上所有的可表示字符都賦予一個唯一的編碼?Unicode便是這樣一種編碼方式。
但是表示范圍包羅萬象的一個代價就是字符編碼長度的增加。這樣數據傳輸和存放時占用的網絡和空間資源就會更多。
UTF-8是在Unicode基礎上發展而來的可變長的編碼方式。
Character | ASCII | Unicode | UTF-8 | GBK |
0 | 00110000 | 00000000 00110000 | 00110000 | 00110000 |
a | 01100001 | 00000000 01100001 | 01100001 | 01100001 |
字 | 無法表示 | 01011011 01010111 (u'\u5b57') | 11100101 10101101 10010111 ('\xe5\xad\x97') | 11010111 11010110 ('\xd7\xd6') |
注意:
上面的0是字符0,對應程序中“0”,而不是數字0。
數字是不使用字符編碼的,數字可以使用原碼、反碼和補碼表示,在內存中一般使用補碼表示,其字節序有大小端模式決定。
3.編碼轉換
不同的編碼有各自的特點,下面是一種可能的字符編輯顯示、加載傳輸和存儲對應的各階段編碼。
由於Unicode可以和任何編碼相互轉換,可以借助Unicode實現不同編碼之間的變換。
python2中有兩種表示字符串的類型:str 和 unicode。basestring是二者的共同基類。
Unicode對象包含的是unicode字符,而str對象包含的是字節(byte)。
漢字“中”的編碼三種編碼方式
'\xd6\xd0'(GBK) <==> u'\u4e2d'(UNICODE) <==> '\xe4\xb8\xad'(UTF-8)
在win7 + python2.7的環境下,Python 自帶的IDE中輸入的中文默認編碼方式為GBK
字符串 '3132' 的編碼是 '\x31\x32\x33\x34'
對應第二節表中的數值可以清楚的看到相同的字符對應的不同的編碼。
也可以說明UTF-8和GBK的編碼兼容ASCII。而ASCII表示的字符在Unicode中則需要兩個字節表示。
一個以ASCII編碼的文檔(注意不是ANSI編碼)可以使用UTF-8和GBK編碼方式打開,而不能使用Unicode。
為了進一步驗證,新建notepad++文檔,選擇編碼方式為ANSI(即GBK),用喜歡的輸入法以最快的方式鍵入漢字“中”。
使用HEX-Editor插件查看文檔的GBK編碼二進制表示:
“中”的UTF-8編碼二進制表示:
“中”的Unicode(對應notepad++中的USC-2 Big Endian)編碼二進制表示:
和python2中的結果相比可以看出
除了Unicode字符外,文件中存儲的二進制數據和對應的編碼是一一對應的。而這也驗證了先前說的Unicode對象本身包含的並不是字節。
\u是unicode編碼的轉義字符,上面起始的0xfeff可以看做unicode編碼文件的一個起始標記。
例如
Unicode: 前兩個字節為FFFE;
Unicode big endian: 前兩字節為FEFF;
實際讀取文件時會根據文件的前兩個字節就可以判定出文件的具體格式。
使用gbk的方式讀取utf-8編碼文件(將utf-8文件編碼方式改為ANSI),將會以GBK的方式解讀字utf-8的節序列'\xe4\xb8\xad',結果如下:
反之,使用utf-8的方式讀取GBK編碼文件(以utf-8的方式解讀字GBK的節序列'\xd6\xd0'):
下面再看一個編解碼不一致導致的顯示問題:
看一下我們IDLE默認編碼方式的確為GBK方式
下面是一個字符串,我們平時常見的編碼錯誤大概就是這種形式。
'宸茬敤鏃墮棿'
這個可以看做亂碼,因為組合沒有字面意思。其實這是一個utf-8編碼按照gbk方式解碼的結果,因此正確的方式先按照utf-8方式解碼,然后編碼成當前環境默認編碼方式然后輸出。
上面第二種錯誤的解碼導致了更深一層次的亂碼問題;解決這個問題是對字符串(非unicode)進行逆向的轉換
>>> '瀹歌尙鏁ら弮鍫曟?'.decode('utf-8').encode('gbk').decode('utf-8').encode('gbk') '\xd2\xd1\xd3\xc3\xca\xb1\xbc\xe4' >>> print _ 已用時間
上面的轉換過程如下:
'\xe5\xae\xb8\xe8\x8c\xac\xe6\x95\xa4\xe9\x8f\x83\xe5\xa0\x95\xe6\xa3\xbf', # 瀹歌尙鏁ら弮鍫曟? '\xe5\xb7\xb2\xe7\x94\xa8\xe6\x97\xb6\xe9\x97\xb4', #宸茬敤鏃墮棿 '\xd2\xd1\xd3\xc3\xca\xb1\xbc\xe4' #已用時間
4.文件顯示標注編碼類型
在python文件中我們可以使用下面的方式標記文件的編碼類型:
# -*- coding:utf-8 -*-
#coding:utf-8
關於python文件編碼類型的聲明可以參考官網: PEP 263
網上有很多人認為上面的編碼聲明定義了python文件的編碼方式,但與其說定義python文件的編碼方式,不如說是這僅僅是對python解釋器的一種編碼方式聲明。即python 文件的編碼聲明 # -*- coding: utf-8 -*- 只是給python解釋器看的。當python解釋器執行py文件時會根據這個聲明來解析文件編碼格式。
也就是說這個聲明和文件本身實際的文件編碼沒有關系。但兩者最好保持一致,否者python解釋器會以錯誤的方式去解碼文件內容並執行。
上面的文件聲明 # -*- coding:gbk -*- 誤導python解釋器以gbk的編碼方式去解析一個utf-8編碼的文件。
實際開發過程中只需要統一將文件聲明為utf-8即可。
再解釋一下文件相關的編碼概念:
文件本身的編碼方式,該編碼決定了文件數據以怎樣的二進制格式保存在存儲介質中。
文件內容顯示的格式(解碼方式),該編碼方決定了文件內容是以怎樣的解碼方式被顯示出來的。
一般文本顯示類的程序打開一個文本時會根據文本文件起始字節判斷該文本的編碼方式,當然也可以有用戶手動改變文件的解碼方式。
如果文本程序或用戶選擇和編碼方式不兼容的錯誤的解碼方式,可能就會出現亂碼。
但是對於可執行py文件而言,python解釋器會以py文件開頭的編碼聲明方式來解釋文件內容,如果沒有聲明,python解釋器會以默認的ASCII編碼解析文件。
也就是只要python可執行文件的聲明編碼只要和實際文件的編碼方式一致,就沒有任何問題。因此可以使用# -*- coding:gbk -*-作為聲明,但考慮到兼容性和可移植性,最好編碼和聲明均采用utf-8。
以上結果均在特定的平台和環境下得出,僅為個人見解。如有錯誤歡迎指正,共同探討學習。