python2.x源代碼中文編碼報錯原因分析和解決方案

目錄

1 python執行程序哪裏會涉及到編碼解碼?

2 預備知識:python2.x程序中的字符類型及編碼處理   

2.1 2.x中的str和unicode  

2.2 python2.x解釋器對.py文件解碼解的實際上是unicode對象

2.3 python2.x解釋器默認編碼方式爲ASCII編碼

3 源代碼出現中文觸發的編碼問題原因分析及解決辦法

3.1 直接寫入中文,沒有進行程序的編碼聲明

3.2 對str對象和unicode對象誤用encode和decode函數

3.3 編輯器保存源代碼時的編碼方式和解釋器解碼方式不一致

3.4 直接操作str對象導致顯示亂碼或報錯


       

       對於IO過程中的編碼解碼,核心一點就是解碼方式和編碼方式保持一致,比如用utf8編碼的文件,讀取後就應該用utf8方式解碼。在python2.x中,所有被解碼後得到的對象,都是Unicode對象。所以本文要講的是在源代碼中的編碼解碼問題,這是很多程序員容易出錯的地方。

1 python執行程序哪裏會涉及到編碼解碼?

      python程序從源代碼到被執行這個過程,有兩個地方涉及到編碼和解碼。首先是我們在編輯器中寫入源代碼後,被保存成.py文件這個過程,編輯器會依據某種編碼方式對源代碼進行編碼,源代碼被編碼成字節碼後保存到.py文件中;然後當程序文件被python解釋器執行的時候,python解釋器會對.py文件中的字節碼進行解碼,這個過程python會使用解釋器中設定的解碼方式對其進行解碼。總體過程如下圖所示。

2 預備知識:python2.x程序中的字符類型及編碼處理   

2.1 2.x中的str和unicode  

       在分析具體的問題之前,還需要注意python2.x對字符類型的怪異處理。在python2.x中,我們通常所理解的字符串類型並不是python2.x中的str,而是unicode對象。對於ascii字符,可以認爲str和unicode是等價的,那是因爲2.x中對str和unicode對於ascii字符做了一些對用戶封裝的底層處理,這些處理使得對於ascii字符來說,str是正常的str,但是一旦出現非ascii字符,這種底層處理便無能爲力,會出現報錯。這裏如何定義正常的str?我們通常所理解的字符串,其應該是字符的最高層的人類可理解的形式,是字節碼解碼後的對象,也是爲了得到字節碼而被編碼的直接對象;但是在2.x中不是這樣的,在2.x中,真正的我們所理解的具有正常字符串那種特性的對象是unicode對象,不是str對象。即在2.x中,字節碼解碼後得到的對象是unicode對象,而且unicode對象也是爲了得到字節碼而被直接編碼的對象,而不是str對象。實際上,str對象在2.x中,底層是一個字節碼序列。但是2.x爲了讓str看起來和我們正常所理解的字符串一樣,其實際上在底層對用戶實現了進一步的封裝。所以,當我們嘗試對一個str對象進行encode時,由於本質上其是一個字節碼,而且2.x直接被編碼的對象是unicode對象,所以2.x中python會先對str使用默認的ascii編碼方式對str進行decode,得到unicode對象,然後再使用ascii編碼方式對unicode對象進行encode,得到str.encode的結果,這裏無論encode函數中指定的編碼方式是什麼,都需要經歷ascii編碼對str字節decode的過程,然後對得到的unicode進行encode時,纔會使用最初encode函數中參數指定的編碼方式。

       如下圖所示,當我們對一個str對象調用encode函數,並指定編碼方式爲utf8,我們所期望的過程是虛線所示的過程;但是對於2.x,實際對用戶封裝的過程是上面的實線過程:先使用ascii編碼方式對str解碼得到unicode對象,然後再直接對unicode對象使用utf8方式進行編碼得到最終的字節碼。

       所以,2.x中,str不是我們所通常認爲的字符串對象,其實際上是一個字節碼序列,unicode對象纔在2.x中扮演了我們通常所理解的字符串的角色:即直接被編碼的對象和解碼後得到的對象。此外,爲了讓str看起來是正常的字符串類型,2.x對str的encode函數做了上述的封裝。而且,爲了使得被解碼後的對象看起來也是str,2.x還讓對於ascii碼的str和unicode在==運算符下返回True的結果。這樣,對於ascii字符來說,str就具備了正常的特點和行爲,但我們要知道,其實際上只是一種對ascii字符有效的封裝而已。

       同理,對於unicode對象,由於其是被解碼後得到的對象,因此實際上不應該再有decode方法。但是2.x中,還是對其封裝了decode方法,如下圖所示,當調用unicode.decode('utf8')函數時,實際過程是上面的實線過程,即先通過ascii編碼方式對其編碼得到字節碼後,再進一步對字節碼進行解碼,得到新的unicode對象。

2.2 python2.x解釋器對.py文件解碼解的實際上是unicode對象

       第一部分中,我們說過,從.py文件到被執行,python解釋器會對其進行解碼,這個過程中,對於字符串的解碼實際上只會對unicode對象有解碼操作,對於str對象,python解釋器會保留其字節序列,不會對其進行解碼,這實際上是符合unicode對象在2.x中扮演的角色的,因爲其本身就扮演着正常的字符串角色,所以該解碼過程只對unicode解碼也是預期之中的。並且,在程序中,str對象也始終是字節序列,而這個字節序列實際上是在編輯器中源代碼被保存成.py文件時生成的,所以這個字節序列的內容取決於編輯器中源代碼被保存成.py文件時的編碼方式。

2.3 python2.x解釋器默認編碼方式爲ASCII編碼

       python2.x中,解釋器對py文件進行解碼時,使用的默認解碼方式是ASCII解碼方式,所以只要我們沒有特別聲明,那麼python2.x解釋器都會使用默認的ascii編碼方式對py文件進行解碼。這點在3.x版本中有所變化,3.x版本中默認的編碼方式是utf8,不再是ascii編碼。

3 源代碼出現中文觸發的編碼問題原因分析及解決辦法

       由於幾乎所有的編碼方式都是兼容ascii編碼的,而且python2.x也做了相應的底層封裝以適應用戶對str的理解,所以在只出現英文以及相應符號的代碼中,無論用戶在哪個過程用的是什麼編碼,基本都不會出現編碼問題,但是一旦源代碼中出現非ASCII字符,那麼由於python2.x的默認處理方式,以及不同編碼之間的不完全兼容性,各種問題就會暴露出來了。本部分,將基於上述的儲備知識 ,對python2.x源代碼出現中文編碼報錯問題進行逐一分析。

       總體來說,在源代碼中寫入中文容易出現報錯的原因可以歸爲下面幾類:1、直接寫入中文,沒有進行程序的編碼聲明;2、由於對python2.x的str對象和unicode對象不瞭解,誤用encode和decode函數;3、編寫源代碼的編輯器對源代碼進行編碼時的編碼方式和python解釋器對.py文件解碼時的編碼方式不一致;4、直接打印str對象,顯示臺對str字節序列的解碼和程序中對str的編碼不一致,導致亂碼或者報錯。

3.1 直接寫入中文,沒有進行程序的編碼聲明

       由於python2.x解釋器默認的編碼方式是ascii編碼,因此一旦python檢測到源碼中出現了執行環境下(註釋不會在執行環境下被執行,因此註釋不受此限制)的非ASCII字符時,就會報類似下面的錯誤,但是腳本使用utf8有BOM格式的編碼除外,因爲有BOM的utf8可以直接被python識別爲utf8編碼,從而不聲明也可以。該錯誤提示腳本中出現了非ASCII字符,而默認的ascii編碼是無法解碼非ascii字符對應的字節碼的,因此,只要檢測到非ASCII字符,就會報錯。

SyntaxError: Non-ASCII character '\xc4' in file C:\Users\KK\Desktop\test.py on line 5, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

       解決辦法很簡單,就如提示所示,只要在腳本中聲明一下解釋器對腳本文件的解碼方式就好了。具體的聲明方式就是在源代碼的最開始寫入# -*- coding: utf-8 -*- ,python在解碼時,遇到這條聲明語句,就會自動將解釋器的解碼方式轉爲utf8,這樣便不再是默認的ascii編碼,從而可以解碼非ascii字符的字節碼。當然,這裏的聲明方式也可以是其他形式,讀者可以自行上網查,其中解碼方式也可以是gbk等其他編碼。

3.2 對str對象和unicode對象誤用encode和decode函數

       從2.1節中我們知道,python2.x中的str不是我們通常所理解的str,尤其對於非ascii字符,這種str的本質就會暴露出來。如果str是一箇中文,那麼對其調用encode函數時,必然會調用str.decode('ascii'),由於ascii編碼無法解碼中文字符對應的字節碼,所以會報類似下面的錯誤,即ascii編碼無法解碼相應的字節碼。

# -*- coding: utf-8 -*- 
s = '你好'.encode('utf8')
print(s)

#如果執行包含上述源代碼的文件,會發生如下報錯
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc4 in position 0: ordinal not in range(128)

       同樣的,通過2.1節,如果對unicode對象調用decode函數,底層會先調用unicode.encode('ascii')函數,如果這時unicode對象是非ascii字符,那麼也會報錯,如下所示。這裏要注意的是,由於unicode也會在解釋器中被解碼,因此需要先保證保存源碼的編碼方式和解釋器解碼的編碼方式是一致的,不然沒等decode函數報錯,首先會因爲上述的兩個編碼不一致導致解釋器對unicode解碼失敗而先報錯。

# -*- coding: utf-8 -*- 
s = u'你好'.decode('utf8')
print(s)

#如果執行包含上述源代碼的文件,會發生如下報錯
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

       對於上述問題的解決辦法就是在理解python2.x中str對象和unicode對象的基礎上,正確使用encode和decode函數,避免對str使用encode函數,避免對unicode對象使用decode函數。

3.3 編輯器保存源代碼時的編碼方式和解釋器解碼方式不一致

       從第一部分我們知道,從源代碼到被執行,有兩個過程設計到編碼解碼:1、源代碼被保存成.py文件這個過程,由編輯器指定編碼方式對源代碼進行編碼;2、python解釋器通過聲明的編碼方式對.py文件進行解碼。自然的,這兩個過程的編碼和解碼的方式必須一致,如果不一致,比如在對源代碼編碼成.py這個過程使用的是gbk編碼,而在解釋器中聲明的是utf8編碼,那麼對於中文來說,utf8自然是無法解碼經過gbk編碼後生成的字節碼的。如下所示,該源代碼文件以gbk編碼保存,運行後會報錯,報錯提示,utf8編碼無法解碼相應的字節碼。

# -*- coding: utf-8 -*- 
s = u'你好'
print(s)

#如果執行包含上述源代碼的文件,且用windows下的ansi編碼,即gbk編碼保存,運行該文件後會發生如下報錯
SyntaxError: (unicode error) 'utf8' codec can't decode byte 0xc4 in position 0: invalid continuation byte

       對此的解決辦法就是檢查源代碼文件的編碼方式,或者調整python解釋器的編碼聲明,使得對源代碼的編碼和解釋器對.py文件的解碼這兩個過程的編碼方式是一樣的。

3.4 直接操作str對象導致顯示亂碼或報錯

       我們在2.1節指出,str對象實際上就是字節碼,且對於ascii字符,str、unicode也是相等的;同時我們在2.2節講過,python解釋器進行解碼時,不會對str對象進行解碼,而是對unicode對象進行解碼。所以,如果源代碼中,我們對於中文字符串,直接用str對象進行保存,那麼該str在python執行程序時,不會被解碼,依然是一個字節碼。但是要注意的是,如果我們print該str對象,在控制檯顯示出結果這個過程,該字節碼是會被自動解碼的,這裏的解碼方式是控制檯決定的。比如,我們在cmd中運行腳本,由於中文windows系統下的cmd默認解碼方式是gbk,所以如果我們對源碼保存時的編碼方式不是gbk,那麼生成的字節碼用cmd下的gbk去解碼,是會亂碼或者報錯的,看下面的例子。

       下面的例子中,源代碼用utf8編碼保存,在cmd下運行後,由於utf8用三個字節保存一箇中文字符,所以s對象是一個有六個字節的字節碼,當最後print(s)時,由於顯示在cmd下,cmd的默認解碼方式是gbk,而gbk編碼方式是用兩個字節存儲一箇中文字符的,因此s的六個字節會被gbk解碼成3個字符,而且這三個字符是gbk編碼體系下對應的字符,所以會出現這樣的三個字符的亂碼。如果s對應的字節碼在gbk中找不到對應的字符,便會報錯,不再是出現亂碼。

# -*- coding: utf-8 -*- 
s = '你好'
print(s)

#如果執行包含上述源代碼的文件,且用utf8編碼保存,在cmd下運行該文件後會顯示下面的亂碼
浣犲ソ
 

       對此最直接的解決辦法自然就是保持編碼一致,可以讓源代碼用gbk保存,由於這裏沒涉及unicode對象,解釋器不會對str對象解碼,且utf8和gbk編碼對於ascii字符都是兼容的,因此可以依然保留開頭對解釋器utf8編碼的聲明,當然,最好同時也將聲明改爲gbk編碼,保持一致。

       上面的解決辦法實際上比較麻煩,除非對2.x的編碼和相關內容有較好的理解,不然改動的地方和要保持一致的地方太多,容易不小心出錯。因此,更好的解決辦法是避免直接對str對象操作,更具體的說,是避免在源代碼中直接使用str對象,而是使用unicode對象,因爲unicode對象和字符的對應關係是全世界統一的,所以不管什麼平臺,對於unicode對象轉到字符的結果都是一致的,是平臺獨立的,因此便可以完全的避免這種編碼平臺依賴的問題。因此,一個基本原則就是,源代碼中,保持字符對象都是unicode對象,而不是str對象。這也就是爲什麼很多人說要在定義中文字符的時候,使用u'string',而不是直接的'string'的原因。

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