python編碼的意義

發表於2016-10-09

python2的直鉤——編碼異常

當你用python開啟一篇中文文件,準備讀取裡面的資料開始實驗…
當你處理好你的資料,打算列印出易於閱讀的結果給boss檢查…
甚至當你剛剛開始編寫自己的程式碼,就寫了一句話…

只要你開始執行自己的程式碼,信心滿滿期待搞定回寢時

UnicodeDecodeError: ‘ascii’ codec can’t decode byte 0xc3 in position 0: ordinal not in range(128)

以及

SyntaxError: Non-ASCII character ‘xe5’ in file test.py on line 3, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

於是你10點前回到寢室以及之後的一系列計劃全部泡湯了,垂頭喪氣的坐下來,你看到兩個動詞格外亮眼——decodeencode。而他們的中文釋義,就是python2對新手的最大陷阱——編碼

當我們談論編碼時我們在談論什麼

python中有關編碼問題的物件有basestring, str, unicode, 標準庫有codecs等,在這篇文章裡我們基本上不會提到標準庫,而僅僅簡單的對物件們進行分析。因為這就夠了!

事實上,我們常犯的編碼問題,從丟擲異常的角度來說分為兩種,很明顯,本文一開頭也列出這兩種異常的列印情形,它們分別是

  • py檔案編譯時未指定檔案字符集導致的解碼異常
  • 字串物件互相轉換時使用預設編碼導致的異常

之後將會分別對兩類異常的處理方法做說明。實際上, 第一類錯誤本質上則是 python 自己執行時開啟檔案進行解碼造成的異常, 就是第二類錯誤! 我們所犯的解碼異常,就是

字串物件互相轉化時沒有指定字元編碼

黃金原則

本文章之所以比其他寫編碼的文章稍微多一點價值的原因,在於本文在這裡——第一章的最後一小節——就用最大的字型寫了處理這類異常的黃金原則

不要驚慌

以及在此之下的,你真正可以掌握的,避免這類異常的黃金原則

只有在IO的時候,才進行轉換

這意味著

  1. 因為某些原因, python 開啟流讀取出的是str,所以用你知道的每一種編碼把它解碼成unicode
  2. 大概是因為同樣的原因,python 的輸出也是str, 但是任何一個unicode 只有到要輸出的時候才編碼成str
  3. 在此之間,放棄該死的str,忘了它,當你開始處理的時候,確保你的每一個字串物件都是unicode

掌握了以上原則,會避免99%的編碼異常發生。當然,正在閱讀這篇文章的人中有80%肯定犯過了1000次以上這種錯誤,去避免剩下1%的發生,而還有20%的人剛開始準備寫python,他們會在看完這篇文章後犯完100%的錯誤,本文的作者正在和80%的人一起微笑著等他們第二遍來看這篇文章。
順便說一下,這篇文章到這裡主要內容就結束了,如果你想找到解決方法和原因,上面已經說的清清楚楚了,接下來主要是各種重複和閒談,幫助你瞭解這之後的內幕,不過第二次來看的同學們記得往下看哦

第一類異常

一點小trick

第一類異常是python 自己開啟你寫的原始檔時丟擲的解碼異常,這句話被說了兩遍說明它一定——很不重要,不過你也可以當做一個冷知識儲備一下。

SyntaxError: Non-ASCII character ‘xe5’ in file test.py on line 3, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

所有的這類異常都是因為你在原始檔寫程式碼時中直接使用了國際化文字——也就是你沒有辦法在ascii碼錶裡找到的字元。同時你“聰明”的沒有做下面說的這一件事

在檔案的開頭使用註釋宣告檔案編碼

pep263

如果你有審慎的閱讀出錯資訊,你一定會注意到一個網址出現在其中。沒錯,那就是python社群的技術提案 PEP(python enhancement proposals), 涵蓋了從版本更新特性至python格式指南的一切東西,如果你有一個昏昏欲睡的下午的話,可以浪費一點時間看看它。

pep263裡,詳細的介紹了某種異常發生的原因,以及它提出的一種宣告註釋的解決方案。接下來我們簡要介紹的一些內容你都可以在上面找到,當然它是英文的

原因

自從pep263成為python標準後,python的編譯器或者說是編碼器在開始解釋前,先要經過以下幾個步驟:

  1. 讀出檔案內容
  2. 將內容根據檔案編碼解碼成為unicode
  3. 分詞標註
  4. 解釋它,並把每一個直接寫出的unicode(u’什麼鬼’)建立一個unicode物件,對str物件,將會從unicode按照檔案編碼再編碼成為str物件

異常原因在於,python的預設檔案編碼,不是utf-8,不是gbk,而是 ascii

快出來看上帝

他們彼此商量說,來吧,我們要作磚,把磚燒透了。他們就拿磚當石頭,又拿石漆當灰泥。

他們說,來吧,我們要建造一座城和一座塔,塔頂通天,為要傳揚我們的名,免得我們分散在全地上。
耶和華降臨,要看看世人所建造的城和塔。
耶和華說,看哪,他們成為一樣的人民,都是一樣的言語,如今既作起這事來,以後他們所要作的事就沒有不成就的了。
我們下去,在那裡變亂他們的口音,使他們的言語彼此不通。
於是,耶和華使他們從那裡分散在全地上。他們就停工,不造那城了

本文作者之所以在這裡引用一段舊約(某知道里的答案),完全是因為作者想展示一下自己的逼格。事實上,本章關於第一類異常的處理在第一小節就已經結束了,後面完全是雜談,但其實也許是很重要的

上帝機智的攪亂了人類的語言的1000年後,本文作者覺得可能是上帝的第二次降臨,人類中最聰明的一群人,也許也是最蠢的,程式設計師,開始想要在自己的處理物件裡增加字元了。

考慮到轉換的問題,很容易就想到,如果把每一個字母,每一個標點,每一個符號與計算機中特殊的一位一一對應的話,就能夠實現對字元的處理了。那麼,這裡假設你已經有一定的計算機底層知識了,這樣一個唯一的對應的編碼至少需要多少位?

這裡提供一些資料, 所有大小寫字母一共52個,0~9數字需要10個,加上逗號,句號,感嘆號…

答案是 7

ascii碼,也就是美國資訊交換標準碼(American Standard Code for Information Interchange),1967年釋出,7位字元編碼中影響最大的一種。二進位制取值範圍0000000~1111111,十六進位制表示00h~7fh

事實上當時ascii碼主要是用於電傳打字機的,但是現在已經基本上一統計算機的天下。但它的問題同樣很嚴重,就在它的名字裡,它實在太美國化了。阿拉伯文,日語,當然還有我們的中文,通通找不到自己的位置,於是出現無窮多種擴充套件ascii編碼,它們的前7fh的編碼與ascii保持一致,而使用自己的擴充套件位實現對其他語言及符號的編碼

我們統稱這一類為ANSI編碼標準,在這裡各國的程式設計師們就開始各自發揮了:

  • gb大家族,我朝官方認證出品的一系列字符集
  • latin大家族,主要是對拉丁字母及西歐一些國家的字母編碼
  • Big 5,呆灣主要使用的針對繁體中文的編碼

你可以想象這是有多麼混亂,實際上都不用想象,現在還有無數人在求助,我的文件開啟亂碼怎麼辦

因此,Unicode響應時代的號召,橫空出世。Unicode使用16位編碼,編碼範圍0000h~ffffh,它對還在捉對廝殺的各國程式設計師說,別打了,我們一個字符集包括世界上所有字元就好啦

但是,Unicode只是給定了字元與編碼的對應關係,它的實現方式還是有很多種,其中就有UTF大家族(其實是美帝的程式設計師發現它們要為一輩子都可能見不到的中文,把英文編碼提高一個位元組時,wtf!)

於是就有了UTF-8,使用一個位元組表示英文,而三個位元組表示中文的編碼方式

註釋宣告

在一大段閒談之後,我們簡單的說明了各大字符集的由來,所以,現在問題來了,面對各國程式設計師的各種編碼的檔案,一門程式語言應該如何處理呢?

對於python,它的預設檔案編碼是ascii碼,在遇到國際化文字,也就是其他編碼字符集時,就會無法編碼(老天,這個編碼都超過ffh了!)

因此,呼應文章開頭,pep263指出,python的程式設計師們都應該在檔案的開頭寫上檔案的預設編碼,同時一個檔案只能有一種編碼!也就是:

至於為什麼與你平常所見到的模式:

不一樣,本文作者會輕易告訴你-*-是裝飾用的嗎

第二類異常

Unicode會夢見小綿羊嗎?

在python中,其實是python2中,與其他語言不同的是,有兩個經常被用來實際操作的字串物件

  • str
  • Unicode

要說明兩者之間的關係,實在不是一個——很難的問題。我們可以非常非常非常——容易的得到物件的繼承關係,如下圖:

可以看到,unicode物件與str物件都繼承自basestring。basestring是一個抽象類,字串及其操作由子類str及unicode各自實現。所以

基本上所有str能進行的操作unicode都能進行

編碼與解碼

在python中,我們所說的編碼encode,特指從unicode轉換成指定編碼的str物件

而所說的解碼decode,特指從指定編碼的str物件轉換為unicode物件

如果你有好好的閱讀來看上帝把那一節,就很容易理解這二者的轉換,相當於我們把不同字符集中對字元的編碼與Unicode全世界統一的編碼互相轉換。

而python2最大的直鉤也在於此,它的預設編碼是ascii

然而ascii早已看穿了一切

我們之所以要重複提ascii,是因為它真的很重要!理解它是python2預設編碼將會讓你真正理解第二類異常的原因:

進行編碼解碼時沒有指定字符集編碼,python預設使用ascii進行編碼解碼

因為ascii僅包含英文大小寫及幾十個常用符號,因此,當你的編碼解碼的物件裡包含中文或者其他亂七八糟東西的時候

UnicodeDecodeError: ‘ascii’ codec can’t decode byte 0xc3 in position 0: ordinal not in range(128)

Do you know your object?

在這一節,我們將會談到何時會觸發第二類異常,也就是所謂的情景檢查。事實上,在本文作者看來,所有的第二類異常都在一種情形下發生:

程式設計師混用了unicode與str物件

一旦開始錯誤的使用unicode或者str,都將很有可能導致第二類異常。然而,遺憾的是,直到它丟擲了異常,大部分沒讀過這篇文章的人依然沒有意識到問題在哪裡。其主要原因在於:

  1. str物件支援的方法與unicode基本完全一樣
  2. str與unicode都是繼承自basestring,大部分對字串操作的方法只會檢查是不是basestring類及其子類
  3. 任何一個類都來自object(這裡指新類),都預設包含內建方法__str__,該方法用於將例項轉換成str物件,換言之,你能夠print任何一個物件,都因為預設使用內建方法轉換了。各個類都可以改寫這個內建方法,而unicode改寫為使用預設編碼解碼

這就使得一個初學者的程式中,字串物件既有unicode,也有str,而他完全沒有意識到,當然也是由於大部分市面上的書在這一點上都及其不負責任。想象一下,當你以為自己的物件a是一個str,而實際上是一個unicode,你想當然的進行print輸出時,就會預設呼叫unicode的__str__進行轉化輸出,在這裡進行了預設編碼ascii的解碼,error!同理適用於當你把一個str當unicode用的時候

一旦你開始混用兩種物件,在你不注意的地方,就會發生預設編碼解碼!

另外一種稍微可以諒解的情況是,python2關於檔案流的封裝實在太過坑爹,基本上所有檔案流最終返回和寫入的都是str物件。簡單的舉個例子,你開啟一個檔案,按行讀取的每一行,都是一個str物件!那些只告訴你這樣可以讀,不告訴你返回型別(雖然寫了你也不大可能注意)的技術部落格都是在耍流氓!

所以在看到這裡的時候,請務必檢查你的程式,檢查你的每一個字串物件,確定它是你想要的型別,要知道,我們所接觸的大部分資料都會有中文,千萬不要等到報錯了才開始糾錯

Do you know your object?
No!?
Go to know your object!

放過str,請找unicode

為什麼我們要放棄str?

簡單的理由,str不僅需要我們知道它的編碼,還需要根據輸出編碼做轉換。假設你有一個utf8編碼的str物件,想要輸出到gbk編碼的控制檯上,你要這麼做:

  1. utf8解碼成unicode
  2. unicode編碼成gbk

為什麼我們不從一開始物件處理的時候就用unicode!

粗暴的理由,python3裡面已經沒有str這種東西了!

請記住黃金原則

只有在IO的時候,才進行轉換

這意味著

  1. 因為某些原因, python 開啟流讀取出的是str,所以用你知道的每一種編碼把它解碼成unicode
  2. 大概是因為同樣的原因,python 的輸出也是str, 但是任何一個unicode 只有到要輸出的時候才編碼成str
  3. 在此之間,放棄該死的str,忘了它,當你開始處理的時候,確保你的每一個字串物件都是unicode

是不是在哪裡看到過? 不要在意這些細節~

按照黃金原則編寫能確保你的每一個進行處理的字串物件都是unicode,同時只在io處進行轉換確保你只有在這個時候才需要考慮編碼的問題,也符合物件導向封裝的概念,也是最pythonic的做法

如果你還不知道什麼是pythonic,請直接執行以下python程式碼

上面所說的是最正確的解決方法,當然有同學就會問啦,下面這種為什麼不是最正確的呢?

這種方法是在飲鴆止渴,完全沒有解決你的實際程式碼問題。它只是將python預設編碼替換成了你想要的編碼(utf-8之類),一旦有新的編碼型別的str物件出現,你的程式就會重新開始報錯。所以不推薦這種方法,它會掩蓋掉你程式的大部分問題。

異常蛋疼的windows控制檯

簡單粗暴

就在不久前,本文作者在伺服器上部署爬蟲程式碼,就不得不在控制檯輸出(當然不是因為作者懶得用其他方式跑程式碼),結果是一連串的亂碼,自認不是新手的作者完全不能忍了,於是心平氣和的坐下來研究了下windows控制檯的編碼

事實上,windows的控制檯的字符集編碼不叫字符集編碼,而叫內碼表,多麼古怪的名字!於是我們很直接的查到了utf-8的內碼表是65001

然後再輸出的時候發現,每log一行就在報一行的error,看輸出資訊是log的流往控制檯寫的時候報的錯,不過既然能列印出log,本文作者決定忽略掉那些error

所以

  • 把內碼表設定為65001 chcp 65001
  • 如果列印出了log,忽略那些錯誤把~

本小節是真的沒有查資料,如有錯誤和更好的解決方法,請不吝指正

相關文章