看完上面的UNICODE編碼,大家是不是想編碼問題已經解決了呢?既然UNICODE能夠相容所有已知的語言和文字,那就全部按照UNICODE來編碼就行了唄。如果你這樣想的話,就too young too native了。由於UNICODE實際上是使用更多的位元組來儲存除英文外的其他國家的複雜語言文字,所以對於中文字元這樣的文字是非常合適的。比如,中文漢字的“中”字,用UNICODE編碼兩個位元組就可以這樣表示:01001110 00101101,這樣一點問題都沒有。但如果是英文字母呢?本來英文字母只需要一個位元組就可以表示,比如大寫字母A,用二進位制表示為0100 0001,而用UNICODE的話,就必須用0來補足多出來的一個位元組,即表示為00000000 01000001。大家看出問題所在了嗎?對了,對於英文來說,UNICODE編碼太浪費空間了,足足大了一倍的空間。特別是在網路上進行傳輸時,這種浪費就極其明顯,會大大降低我們的傳輸效率。為了解決這個問題,就出現了一些中間格式的字符集,他們被稱為通用轉換格式,即UTF(Unicode Transformation Format)。而我們最常用的UTF-8就是這些轉換格式中的一種。UTF-8編碼其實是一種可“變長”的編碼格式,即把英文變長為1個位元組,而漢字用3個位元組表示,特別生僻的還會變成4-6位元組。所以如果是傳輸或儲存大量英文的話,UTF編碼格式優勢非常明顯。
要說在整個程式設計領域中最難的問題有哪些的話,字元編碼的問題,也就是亂碼問題,絕對算得上很多程式設計師寫程式碼時的一個“噩夢”。以至於在IT界有個著名的笑話,“手持一把錕斤拷,口中直呼燙燙燙”,如果你笑了,那麼你肯定是做IT的,哈哈哈。而在python這門語言中,因為python2和python3本身編碼機制完全不一樣,所以這個問題又尤其突出。包括我本人在內,也被這個編碼問題困擾了很久,一直沒有完全搞明白。後來,為了徹底解決編碼問題,專門查詢了很多的書籍和資料,終於搞清楚了關於編碼問題的來龍去脈以及各種情況下存在的問題以及解決方式,今天這篇文章就來做一個總結,相信大家只要認真看了之後,媽媽再也不會擔心你的編碼問題了。
要徹底弄清楚亂碼是怎麼來的,有兩個大的關鍵因素必須要了解:一個是究竟有哪些編碼型別,各種型別有哪些不同的特點,這些必須爛熟於心。二是你的程式碼執行環境是什麼。比如是在命令列執行?還是在編輯器中執行?在python2中還是在python3中?在linux系統裡面?還是在windows系統裡面?弄清楚這兩個問題,亂碼問題便會迎刃而解。接下來我們來一一解決這些問題。
一、編碼型別很多人想不明白為什麼計算機中有這麼多亂七八糟各種各樣的編碼,比如什麼ASCII啊,GBK,GB2312,UNICODE,UTF8,這些都是什麼鬼?為什麼要有這麼多不同的編碼格式?要想搞清楚這些編碼問題,必須先了解一下關於字元編碼的歷史,這些都是祖上留下來的“孽債”。
1. 什麼是字元編碼
首先我們來了解下究竟什麼是字元編碼,為什麼要有字元編碼這個東西出現?原因很簡單,計算機從本質上來說只認識二進位制中的0和1,可以說任何資料在計算機中實際的物理表現形式也就是0和1,如果你將硬碟拆開,你是看不到所謂的數字0和1的,你能看到的只是一塊光滑閃亮的磁碟,如果你用足夠大的放大鏡你就能看到磁碟的表面有著無數的凹凸不平的元件,凹下去的代表0,突出的代表1,我們用bit(位)來表示每個這種二進位制的數,這就是計算機用來表現二進位制的方式。而我們在處理資料時,一般並不是按位來進行處理,而是按照位元組(byte)來進行處理的,一個位元組byte=8bit。那現在我們面臨了第一個問題:如何讓人類語言能夠被計算機正確理解呢?我們以英文為例(因為計算機是美國佬發明的,所以最開始當然只考慮英文的情況),英文中有英文字母(大小寫)、標點符號、特殊符號。如果我們將這些字母與符號給予固定的編號,然後將這些編號轉變為二進位制用位元組來表示,那麼計算機明顯就能夠正確讀取這些符號,同時透過這些編號,計算機也能夠將二進位制轉化為編號對應的字元再顯示給人類去閱讀。所以,基於這種思想,便產生了ASCII碼。
2. ASCII編碼
ASCII碼是人類計算機歷史上最早發明的字符集,大家都知道 ,計算機是美國佬發明的,他們只用英文,所以可以說ASCII碼是專門為表示英文、數字以及英文標點符號而生。由於英文字身比較簡單,就是由26個字母組成,加上0-9十個數字以及一些英文的標點符號。而在計算機中,1byte=8bit,也就是說有從0000000-11111111共2的8次方共256種不同的組合,這些組合已經足夠儲存所有的這些英文字母、數字以及標點了,所以早期的編碼只有ASCII編碼。
3. GB2312以及其他編碼
如果全世界的人都使用英文的話,今天我們就不必這麼費神來研究編碼問題了。正因為全世界的語言太多,大家都想使用自己熟悉的語言來使用計算機,比如華人用計算機當然使用中文了。那麼問題來了,在中文中光常用的漢字就已經達到了6000多個了,很明顯之前的ASCII碼已經完全無法滿足漢字儲存的需求了。怎麼辦?既然使用ASCII碼這樣一個位元組無法搞定,那麼我們自然想到能不能多用1個位元組是不能就能搞定了呢?所以,為了滿足國內在計算機中使用漢字的需要,中國國家標準總局釋出了一系列的漢字字符集國家標準編碼,統稱為GB碼,或國標碼。其中最有影響的是於1980年釋出的《資訊交換用漢字編碼字符集 基本集》,標準號為GB 2312-1980,因其使用非常普遍,也常被通稱為國標碼。GB2312編碼通行於中國內地;新加坡等地也採用此編碼。幾乎所有的中文系統和國際化的軟體都支援GB 2312。所以,大家可以理解為,GB系列的編碼是為了適應複雜的中文編碼而對ASCII碼的一種擴充。
4. UNICODE標準編碼
既然咱們華人能夠對ASCII碼進行擴充,以便於顯示更復雜的中文,那麼其他國家呢?比如日本、南韓,其實也面臨著同樣的問題。所以,他們自然也會對ASCII碼擴展出自己的一套編碼。假設每種語言都自己搞一套,工作量上去了不說,還為不同編碼之間的轉換和顯示造成了巨大的困難,這也行不通啊。所以,為了簡化不同編碼之間的顯示和轉換問題,很有必要搞一套統一的編碼格式出來。基於這種情況一種新的編碼誕生了:Unicode。Unicode又被稱為統一碼、萬國碼;它為每種語言中的每個字元設定了統一併且唯一的二進位制編碼,以滿足跨語言、跨平臺進行文字轉換、處理的要求。Unicode支援歐洲、非洲、中東、亞洲(包括統一標準的東亞象形漢字和南韓表音文字)。這樣不管你使用的是英文或者中文,日語或者韓語,在Unicode編碼中都有收錄,且對應唯一的二進位制編碼。這樣大家都開心了,只要大家都用Unicode編碼,那就不存在這些轉碼的問題了,什麼樣的字元都能夠解析了。
5. UTF-8編碼
看完上面的UNICODE編碼,大家是不是想編碼問題已經解決了呢?既然UNICODE能夠相容所有已知的語言和文字,那就全部按照UNICODE來編碼就行了唄。如果你這樣想的話,就too young too native了。由於UNICODE實際上是使用更多的位元組來儲存除英文外的其他國家的複雜語言文字,所以對於中文字元這樣的文字是非常合適的。比如,中文漢字的“中”字,用UNICODE編碼兩個位元組就可以這樣表示:01001110 00101101,這樣一點問題都沒有。但如果是英文字母呢?本來英文字母只需要一個位元組就可以表示,比如大寫字母A,用二進位制表示為0100 0001,而用UNICODE的話,就必須用0來補足多出來的一個位元組,即表示為00000000 01000001。大家看出問題所在了嗎?對了,對於英文來說,UNICODE編碼太浪費空間了,足足大了一倍的空間。特別是在網路上進行傳輸時,這種浪費就極其明顯,會大大降低我們的傳輸效率。為了解決這個問題,就出現了一些中間格式的字符集,他們被稱為通用轉換格式,即UTF(Unicode Transformation Format)。而我們最常用的UTF-8就是這些轉換格式中的一種。UTF-8編碼其實是一種可“變長”的編碼格式,即把英文變長為1個位元組,而漢字用3個位元組表示,特別生僻的還會變成4-6位元組。所以如果是傳輸或儲存大量英文的話,UTF編碼格式優勢非常明顯。
6. 不同編碼格式和UNICODE之間的轉換
為了在不同的編碼格式之間進行轉換,我們必須對字元進行編碼和解碼的工作。任何非UNICODE格式的字元(串),我們都可以使用decode方法將其解碼為UNICODE編碼的字元(串),這種轉換過程叫“解碼”。同樣道理,UNICODE格式的字元(串),也可以透過encode()方法將其編碼為其他編碼格式的字元(串),這個過程叫“編碼”。後面我們會頻繁使用到編碼和解碼的操作,大家都應該明白什麼時候應該使用編碼,什麼時候應該解碼。
到此為此,大家應該對編碼型別有一定了解了,總結一下就是:
1.為了處理英文字元,產生了ASCII碼。
2.為了處理中文字元,產生了GB2312。
3.為了處理各國字元,產生了Unicode。
4.為了提高Unicode儲存和傳輸效能,產生了UTF-8,它是Unicode的一種實現形式。
二、執行環境的影響搞清楚了上面介紹的各種編碼格式之後,接下來我們就開始詳細講解為什麼會出現亂碼了。關於亂碼,大家記住兩個要點:
(1)所謂亂碼的本質是字元的編碼格式與顯示字元的環境編碼格式不一致引起的。這句話告訴我們要解決亂碼問題,我們需要知道兩個資訊,一個是字元本身是什麼編碼,另一個就是顯示字元的環境編碼是什麼,兩者必須一致,才能顯示出正確的內容。
(2)由於Unicode編碼是標準編碼格式,也可以看做是沒有任何特定編碼格式的“無編碼”模式。所以,對於任何Unicode型別編碼的字元,列印時python會自動根據環境編碼轉為特定編碼後再顯示。
上面兩個要點大家一定要記住,接下來,我們來看看字元在python程式碼中是怎麼被編碼的。在不同的python版本中,字元編碼的方式也不一樣。先來說說比較麻煩的py2版本。如果你用py2來寫指令碼的話,因為預設py2是用ascii來編碼指令碼的,所以如果你的指令碼中出現了中文,就必須在指令碼的開始位置註明支援中文的編碼格式,否則會報錯。所有支援中文的編碼格式都是可以的,比如宣告為#coding:utf8或#coding:gbk都是可以的。註明以後,我們就可以在指令碼中隨意使用中文了。例如下面這個例子:
宣告編碼格式#coding:utf8或#coding:gbk以後可以正常工作。如下:
在py2中,所有字串的編碼方式預設是用ascii來進行編碼的,如果透過coding:xxx的方式聲明瞭指令碼的編碼方式,則字串會按照宣告的字元編碼格式來進行編碼,而字串變數型別是為str型別的。這裡大家要記住py2中str一定是有特定編碼的,不是Unicode格式(這裡為什麼要講這一句,因為待會介紹的py3字串預設是Unicode編碼的,待會我們會細講)。比如上面的a變數中儲存的“中國”這兩個中文字元的編碼就是gbk格式了。那麼當我們列印這個a變數的時候,會出現什麼情況呢?我們現在IDE中列印來看看,比如pycharm,打印出來結果如下。納尼?居然出現了亂碼,這是為什麼呢?
如果記住了我之前說的關於亂碼的那兩個要點的同學,應該很容易明白這裡為什麼會出現亂碼。原因很簡單,這裡a變數的編碼是gbk的,而我們執行指令碼的編輯器pycharm設定的環境編碼卻是utf8,兩者編碼方式並不一致,所以必定會出現亂碼。那麼怎麼解決呢?解決方式有幾種,一種是修改#coding:gbk為#coding:utf8,二是可以在"中國"前面加一個u,即a=u"中國"。在前面加u是將“中國”強制轉換為unicode編碼,即“無編碼”,此時變數的type將會變為unicode。前面已經說過,對於unicode編碼的字元,python將自動根據環境編碼進行顯示,所以也就是會自動幫我們編碼為utf8進行顯示。還有一種方式是透過encode和decode函式,比如像下面:
使用decode方法可以將字串進行解碼,解碼後格式就是Unicode了,所以a.decode("gbk")這句跟u"中國"效果是等價的,打印出來當然是沒問題的。當然,我們也可以明確寫出要編碼的型別,比如a.decode("gbk").encode("utf8"),這樣將Unicode明確地編碼為utf8,也是一樣的效果。這裡大家要注意一點,我們對所有非Unicode型別的字元只能進行decode操作,不能進行encode操作。對Unicode型別的只能是encode而不能decode,這個大家要注意。
搞明白了pycharm裡面的行為後,我們再看看如果這個指令碼不是在pycharm裡面執行,而是直接在命令列裡面執行,又會發生什麼問題呢?就將就上面這個檔案,我們在命令列裡面執行,結果如下:
果不其然,b1正常顯示了,b2卻出現了亂碼。這次出現亂碼的原因又是什麼呢?這裡大家要知道,命令列裡面的環境編碼是gbk格式,由於b1是Unicode編碼,Unicode編碼的字元會自動隨著環境編碼來輸出,所以不管在什麼環境下,b1都能正常顯示輸出。而b2由於被encode成了utf8格式,所以它只能在環境編碼為utf8的環境中才能正常顯示,在命令列這種環境下就會出現由於編碼不一致而導致的亂碼。大家可以試試直接print a,由於檔案是coding:gbk的,所以a是可以直接正常顯示的。比如程式碼如下:
在pycharm中無法正常顯示a的值,但在命令列中卻可以,如下圖:
如果是py3的指令碼的話,則要簡單得多。因為py3中,所有的字串不再受系統環境編碼的影響,統一使用Unicode來進行編碼,型別統一為str,所以不再需要在中文前面加u來使中文字元變為Unicode這種寫法。而且所有py3的指令碼預設都是utf8來編碼的,所以我們也不需要在指令碼開頭指定coding:xxxx了。列印顯示的時候也會方便很多,由於是字串都是Unicode格式,所以不管在命令列中還是pycharm中,都會正常顯示而不會出現亂碼。
上面是透過指令碼來執行的情況,那麼如果是直接在命令列中寫指令碼,又會出現什麼問題呢?其實不管在哪裡執行,上面說的兩個原則始終不變,大家永遠記住無非我們就是要弄清楚字元本身的編碼和環境編碼,只要這兩者一致了,那一定不會出現亂碼。在python shell(即命令列)中直接寫程式碼執行時,大家只需注意在windows下,命令列的預設編碼是gbk的,而在Linux環境下,命令列的預設編碼是utf8的,其他沒什麼區別。所以我們接下來分別來看看。
在windows環境下,我們在命令列中寫一段程式碼來看看,執行效果如下:
大家注意,第一行我們在定義a="中國"時,並不會報錯,因為在命令列中預設是gbk編碼,所以此時其實a的編碼已經是gbk了,支援中文沒有任何問題。直接顯示a變數時,打印出來的不是亂碼,而是該字串的位元組碼錶示方式,大家可以理解成給計算機看的,不是給人看的,只有print出來的內容才是給人看的。print a也不會報錯,因為按照gbk方式編碼並且在gbk環境中執行,不可能會出問題。下面直接將a進行decode解碼時,解碼方式必須跟編碼方式是一致的,所以gbk方式編碼的內容不能解碼為utf8格式,只能decode為gbk。decode之後,字串會變為Unicode,也可以正常顯示。最後,我們將Unicode編碼為utf8時,字元的編碼格式又跟環境編碼不一致了,所以再次出現了亂碼。py3同理,就不再贅述了,如果掌握了之前說的原則,應該完全不會出現問題。如果是在Linux下面的命令列中執行,道理也是一樣,只是需要注意linux下命令列預設的編碼格式是utf8的就可以了。
看完上面的內容,我相信大家應該已經掌握了字元編碼的所有秘密。不管編碼格式是什麼,在什麼地方執行,大家始終記住那兩個關於亂碼的原則,問題一定會迎刃而解。接下來,我們再看看更多實際的例子。
比如,我們在使用爬蟲爬取網頁時,也會經常遇到亂碼,如果結合上面講的原則,大家是否能夠知道問題出在哪,並且解決這些問題呢?我們以網易和百度這兩個網站為例,給大家看看會有什麼樣的問題。首先來看看網易的首頁,開啟原始碼,我們可以看到,網頁首頁的編碼格式是gbk的。
編碼格式gbk意味著,如果我們需要對抓取的網頁內容進行解碼的話,必須指定解碼方式為gbk才能正常解碼為Unicode型別的字串。假定我們使用的是py3,如果使用預設的decode()方法,將預設解碼為utf8,肯定是會報錯的。比如下面的程式碼,我們先抓取163的首頁內容,並用正則取出頁面的title,程式碼如下:
這裡為什麼會報錯呢?因為resp.content實際上是抓取的網頁的原始字串,是以gbk編碼的二進位制內容,所以我們需要知道這個字串的編碼方式才能正確地進行解碼。從網頁中我們可以知道,該網頁的編碼為gbk編碼方式,所以我們decode時必須指定gbk作為解碼的方式(如果decode中不指定解碼方式的話,預設以utf8來解碼),所以我們應該改為下面這樣就可以正確拿到我們的結果:
而對於百度首頁,其網頁編碼方式是utf8的,所以我們在解碼時就不用再專門指定utf8格式了,直接decode即可,大家可以自己試試。
本文到這裡終於可以結束了,內容確實不少,因為要搞明白編碼的問題,我們需要知道很多東西,這是我們必須要掌握的。另外,很多資料和書籍上都會寫到,在py2的指令碼中指定編碼方式時,必須在指令碼開頭的位置寫coding:utf8,想必大家讀完此文應該知道這種說法是對的還是錯的了。這就是學習的價值,為什麼我們要摳原理、抓本質,就是讓我們有足夠的能力和底氣去判斷和質疑一個問題的對和錯,只有這樣,你的技術才能真正進步,讓你去解決更多的問題。最後,希望大家以後再也不會受到亂碼問題的困擾了。