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