寫python的過程中經常出現各種蛋疼的編碼問題,于是通過上網查資料,自己做實驗,想徹底搞清楚這個問題。
編碼和解碼的理解
計算機是不認識字符的,計算機只認識二進制的01串,那么字符要存儲在計算機中,首先要做的就是把字符用二進制的01串來表示,這就是所謂的編碼(encode)
;當我們要閱讀存儲在計算機中的字符時,計算機就需要把二進制的01串轉換成我們可以讀的字符,這就是解碼(decode)
;所以,我們遇到encode error
, 一般是計算機需要把某個字符進行存儲時,使用的編碼找不到該字符對應的01串,而我們遇到decode error
時,一般是計算機讀取以01串存儲的數據,準備轉化成我們可識別的字符時,使用的編碼識別不了01串。不論是從字符轉化成二進制的01串進行存儲,還是從二進制的01串轉化成我們可讀的字符,都需要一個對應的轉化表,也就是哪個字符對應哪個01串,關于這樣的對應關系有很多種(比如utf-8, gbk等等),就稱為編碼方式(簡稱編碼,名詞)。顯然,每種編碼方式可以編碼的字符都是有限的,那么一種編碼方式可以編碼的字符集就稱作字符集吧。
如果我們用一種編碼方式 A 進行encode,用另一種編碼方式B進行decode,以“ 我 ” 這個漢字為例,那么就會出現三種情況:一,A和B所使用的字符集中都可以找到“ 我 ” 這個字符,而且A和B表示“ 我 ” 這個字的01串也一樣,所以“ 我 ” 這個字的編碼和解碼就不會存在問題;二,A和B所使用的字符集中都可以找到“ 我 ” 這個字符,但是A和B表示“ 我 ” 這個字符的01串不一致,這時候A按照自己的編碼方式將" 我 “ 存儲到計算機中,B按照自己的編碼方式解讀A對“ 我 ” 這個字表示的01串時,就會出現兩種情況,一是可以解釋,但是顯然解釋是錯誤的,可能會對應到另外的字符,這就稱之為亂碼,我們會看到一堆無意義的字符,另一種是直接不能解釋,這時候程序會直接報錯,python中就是decode error
; 第三種是B所使用的編碼集種直接無法找到“ 我 ”, 其表現和第二種一樣。
python中的編碼
一、python文件的編碼
python文件是由python語言解釋器進行解釋執行的,默認情況下python解釋器對python文件用ascii編碼方式進行解碼,因此如果python文件中包含中文字符,就會報錯,如下面test.py的代碼
# main 程序
def main():
print('hello, world!')
main()
執行python test.py
會得到
File "test.py", line 1
SyntaxError: Non-ASCII character '\xe7' in file test.py on line 1, but no
encoding declared; see http://python.org/dev/peps/pep-0263/ for details
我們可以看到Non-ASCII character '\xe5' in file
,也就是說沒有聲明編碼的情況下,python解釋器按照ascii解碼的時候不認識'\xe5',現在在這個文件的頭部加入編碼聲明,那么聲明為什么編碼呢?python解釋器讀的是這個文件,因此這個文件是按照什么編碼存儲的就按照什么編碼聲明,我的編輯器是utf-8編碼的,因此我聲明為utf-8, 代碼如下
# coding: utf-8
# main 程序
def main():
print('hello, world')
main()
這次再執行這個文件,就正常輸出了 “hello, world", 剛才這個編碼問題就是我們的編輯器保存代碼使用的編碼和python解釋器解釋代碼使用的編碼不一致導致的,因此我們通過 # coding: utf-8
告訴python解釋器應該使用的編碼,這個問題就解決了。
二、python中的字符串和unicode
由于我使用的是python2.7, 因此,僅針對python2.7討論這個問題。python中有str和unicode兩種表示字符串的方式,他們均繼承自basestring, 但是卻是完全不同的兩個東東。str可以說并不是真正的字符串,而是已經經過編碼的二進制01串,而unicode確實真正的字符串。
# coding: utf-8
# main 程序
def main():
u=u'大家好'
s='大家好'
print(len(u))
print(len(s))
main()
這段代碼的輸出是3和9,3我們很好理解,本來就是3個漢字;為什么s的長度是9呢?就是因為s是'大家好'這三個漢字已經編碼得到的長度為9字節的01串。這三個漢字已經編碼了,那么使用什么編碼方式呢?就是編輯器所使用的編碼方式,在我這兒也就是utf-8, 我們再做一個實驗,對這個問題有更加深刻的理解。
# coding: utf-8
# main 程序
def main():
u=u'大家好'
print(u)
main()
我執行 python test.py
,正常輸出 “ 大家好 ", 然后我想讓這個輸出保存到文件中,因此我執行 python test.py > out.txt
, 于是問題出現了,輸出下面的錯誤
Traceback (most recent call last):
File "test.py", line 8, in <module>
main()
File "test.py", line 6, in main
print(u)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2:
ordinal not in range(128)
為什么我直接輸出到終端的時候一切正常,當我想重定向到文件的時候出問題了呢?當我直接輸出至終端的時候,按照終端所使用的編碼來編碼‘大家好’這3個字符,而我的終端所使用的編碼是utf-8,和我的編輯器所使用的編碼一致,所以不存在問題。但是當我把這個輸出要重定向至文件的時候,就出問題了,因為文件本身并不規定編碼方式,我也沒有對這個字符串進行顯式編碼,因此python將采用默認的編碼方式,而python2.7的默認編碼是ascii
,因此會出現UnicodeEncodeError
,搞清楚問題以后,也就有辦法解決問題了,一種是我們對這個unicode字符串進行顯式編碼, 如下所示
# coding: utf-8
# main 程序
def main():
u=u'大家好'
print(u.encode('utf-8'))
main()
另一種是改變python的默認編碼為我們這個字符串的編碼, 如下所示
# coding: utf-8
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
# main 程序
def main():
u=u'大家好'
print(u)
main()
一般推薦第一種方式,盡量避免第二種方式,因為第二種方式只有當我們需要處理的編碼方式只有一種時有效,如果我們使用多種編碼方式,那么仍然會存在問題。我們再看一段代碼
# coding: utf-8
# main 程序
def main():
s='大家好'
print(s)
main()
對這段代碼執行python test.py
直接輸出在終端和python test.py > out.txt
都可以得到預期效果,這又是為什么呢?這段代碼和之前代碼的區別是'大家好'這個字符串是str而不是unicode類型。我們知道str類型是已經編碼的01串,因此在輸出至終端或文件時不需要再進行編碼,所以不會出現之前遇到的問題。
三、思考
這樣看,貌似使用str類型更方便一些,省去了我們進行顯式的編碼了,實則恰恰相反,因為這種隱式的編碼方式很不利于代碼維護,雖然代碼暫時很僥幸,很容易的運行通過了,但是日后我們很難搞清楚這個str里面究竟是什么編碼,而且我們也看到通過len拿到的字符串長度也存在問題。而使用第一種unicode的方式,雖然我們寫代碼時需要多費些功夫,在輸出時需要顯式進行編碼,但是這樣也明確了這個字符串所采用的編碼,同時拿到的字符串長度也是準確的。