【Python】 編碼,en/decode函數以及print語句的一些探索


   昨天晚上在整理hashlib和hmac模塊的時候,又看到了編碼這塊的內容。越看越覺得之前的理解不對,然后想研究一下自己想出來,但是越陷越深。。總之把昨晚+今天一個上午的這些自己想到的東西寫下來

   ●  幾個概念(あくまで是我為了統一本篇中的術語,至於業界是不是這么說我不敢保證。。)

  編碼: 計算機認識的其實只有二進制數據,而我們之所以能夠在計算機上處理自然語言和自然字符主要是因為有一種對應關系可以映射二進制數據和自然字符。這種映射就是廣義上的編碼。具體到中文上來,計算機發展源於英美,最開始的編碼只要記住26個字符以及一些簡單的標點等在計算機中如何存儲,可是中國漢字那么多該怎么辦。於是很多解決方案出現,縱覽所有解決方案,其要義都是把一個中文字分解成若干個小字節,通過小字節間的排列組合來實現二進制數據到漢字的映射。這種映射是漢字編碼,也是這篇里會提到的狹義上的編碼。

  編碼格式: 不同的編碼格式雖然都能實現漢字編碼,但是對於同一個漢字內容,其映射的二進制數據是不同的。比如gbk和utf8就是兩個不同的編碼格式

 

   ●  關於unicode,ascii,gbk和utf8

  知乎這個回答寫得很好

  概括一下,最早有的肯定是ascii編碼格式。gb2312,gbk等這一批是一個系列的編碼格式,從前一個版本的基礎上不斷擴充編碼。他們來自ascii的一脈相承,是中國人民對ascii編碼無法支持漢字顯示這一問題的解決方案(原有ascii編碼格式的映射不變,額外加上漢字編碼)。但是如果全世界各種語言都按照自己的標准來進行編碼的創作的話,很快就會亂套的。所以某些國際標准組織退出了unicode(Universal Multiple-Octet Coded Character Set,簡稱UCS),它是一些(甚至可能是所有?)編碼方式的並集。除了ascii的內容繼續維持不變外,把其他所有字符都進行了國籍標准的編碼,意思是不讓不同的字符編碼成為計算機領域中的巴別塔。

  在舊時代的gb系列中,英文字母沿襲了ascii的習慣,一個字母字符占一個字節;而一個漢字字符都被認為是兩個英文字符的組合,占兩個字節。而到了新時代的unicode之后,包括英文字母和漢字在內的所有字符都被統一認為是一個字符,並被存儲成兩個字節。這么一來就出現了一個問題,本來美帝的程序員們寫一個字只花一個字節的空間,但是如果他們要和國際接軌,用了unicode的話,一個字就得花兩個字節,這浪費了很多資源。這也是為什么unicode在很長一段時間里沒有得到推廣。

  后來互聯網的興盛催生了面向傳輸的UTF(UCS Transformation Format)標准。其中utf-8是比較有名的一種。utf-8的意思是每次以8個位傳輸數據。utf8是一種所謂“變長的編碼格式”,它靈活地運用1-4個字節來存儲不同的字符。當字符處於ascii的范圍內就用ascii的一個字節來儲存,更復雜的字符則按照unicode的規定用更多的字節去存儲。可以說是一種對傳輸成本的妥協。(是不是感覺歷史倒退了w)這也在冥冥之中讓utf-8和gb系列又像起來了,它們都對ascii原生的那些字符不做處理,而自己再做一些各自的擴展。gb系列是中國特色,針對漢字的,而utf是面向全世界的。這種相似性也體現在程序中,程序中經常感覺,gbk和utf-8好像是同一個級別的東西吧!很顯然,unicode的編碼和utf-8的編碼並不是完全一一對應的,需要一些算法和操作來進行轉換。

  utf8,UTF-8在程序中和utf-8都是等價的,程序會自動識別,其他帶有-等的編碼格式名稱也都是一個意思。

 

  ●  print語句的試驗

  我猜測,當print語句碰到一個unicode目標的時候,會用當前python shell環境的默認編碼格式首先對unicode對象進行encode(此時unicode對象已經變成了一個str對象了),然后再以默認編碼格式為基礎,根據其包含的漢字和編碼的對應規則,把這個str對象解釋成中文並顯示出來。但是當print語句碰到的直接是個str目標的時候,就不管其從unicode轉到str時用的編碼格式是什么,直接用默認編碼格式的對應規則來解釋成中文。所以,當unicode對象轉換成str時的編碼格式和print語句的默認編碼格式不一致的時候就會出現亂碼現象。比如在cmd的python shell里面:

>>>uni = u'你好'    #存入一個unicode對象
>>>print uni
你好    #可以正常顯示
>>>uni.encode("gbk")
'\xc4\xe3\xba\xc3'        #顯示的是個str對象了,如果type(uni.encode("gbk"))得到的就是str對象
>>>print uni.encode("gbk")
你好       #可以正常顯示,因為在cmd下的pythonshell里默認個編碼格式就是gbk
>>>uni.encode("utf-8")
'\xe4\xbd\xa0\xe5\xa5\xbd'        #可以看到,encode用的編碼格式不同,編成的字符串也是不同的
>>>print uni.encode("utf-8")
浣犲ソ    #亂碼,因為用了gbk中漢字和字符串編碼格式對應規則去解釋了用utf-8編碼成的字符串。
#######さらに######
>>>print '\xc4\xe3'    #自己寫出來的這么個字符串(前面不加r)的話也會被print解釋成中文
>>>print uni.encode("utf-8").decode("gbk")
浣犲ソ    
'''
亂碼,而且和上面的亂碼一樣,這是因為,在uni被utf-8 encode之后,這個對象變成了str對象,是'\xe4\xbd\xa0\xe5\xa5\xbd' 這個。
后來,它又被按照gbk的規則解碼,又變回了unicode,但是此時它在內存里的二進制數據已經和最初的uni不一樣了。
最初的uni,應該是'\xc4\xe3\xba\xc3'.decode("gbk"),而現在的這個東西,他decode之前的字符串已經變過了。
這么一個東西再拿去print,又把它編碼成了gbk格式,相當於前面那步decode沒有做,變回了'\xe4\xbd\xa0\xe5\xa5\xbd'。
再解釋成漢字,當然就和最開始用uni編碼成utf-8格式再解釋成漢字的亂碼一樣了
'''

 

  ●  關於不同python shell中對不同編碼的支持

  python shell這個東西可以再很多地方存在。比如在cmd.exe里鍵入python來進入windows中的python shell(下也稱cmd的python shell),或者在某些IDE比如pycharm中自帶一個python shell,還有linux自帶的python shell等等。基於上面的猜測,我覺得在cmd的python shell里面,print的默認編碼格式是gbk,而在pycharm這種編輯器界面里面默認編碼格式是由腳本上方的# coding=xxx來決定的,通常這里寫utf8的話pycharm的pythonshell就是一個utf8環境的shell了。比如:

#在cmd的python shell中
>>>print u'你好'.encode("utf-8")    #用gbk解釋utf8編碼成的字符串,當然是亂碼,下同
(亂碼
>>>print u'你好'.encode("gbk")
你好

#在pycharm的python shell中
>>>print u'你好'.enocde("gbk")
(亂碼
>>>print u'你好'.encode("utf-8")
你好

  某個環境的的編碼可以看sys.stdout.encoding就可以看了。cmd中顯示的是cp936其實就是gbk。

  這里我覺得還要區別一個概念就是系統的默認編碼和python shell的默認編碼。系統的默認編碼可以通過sys.getdefaultencoding()來查看,基本上都是"ascii"。如果想要改變可以通過import sys;reload(sys);sys.setdefaultcoding("...")來實現。(必須要有reload函數,否則可能報錯從sys中找不到setdefaultcoding方法)。那么系統默認編碼的作用是什么,舉例如下:在代碼中如果寫'xxx'.encode('yyy'),此時xxx是一個str類型的話,首先xxx有一個編碼類型,然后其直接調用encode方法,默認是用系統默認編碼去decode這個xxx然后再執行后面的encode,此時就是用到系統默認編碼的時候。簡單來說,setdefaultencoding之后可以讓encode更加方便地被使用。

  而python shell的默認編碼正如上面說的cmd里的是gbk而pycharm里的是utf8。系統默認編碼什么用下面會說。

 

   ●  python中的unicode和str

  unicode把二進制數據用更加好看的十六進制表現出來(當然僅限於中文和其他一些特殊字符比如"不"就是u'\u4e0d',英文的話u'nihao'就是以u'nihao'存着的):

  比如“你”字的unicode編碼是u'\u4f60'其實的意思是內存中的01001111(4f) 01100000(60)這樣的內容罷了。u'\u4f60'只是把二進制轉化成十六進制方便我們看。

  在習慣了u'\u....'這種格式的unicode同時也不要忘了還有可能有其他形式的unicode,比如latin-1編碼格式decode出來的unicode往往是u'\x....'這種形式的。一個字符可能有多個unicode,比如"不"既可以是u'\u4e0d'也可以是u'\xb2\xbb'(有gbk編碼格式的str通過latin1編碼格式decode而來),還可以是u'\xe4\xb8\x8d'(由utf8編碼格式的str通過latin1編碼格式decode而來)。第一種unicode是通用的,可以被encode成gbk或者utf8編碼格式的編碼,但是后兩者和第一種不能相互通用,u'\u...'形式的編碼無法有效地用latin1來encode,同樣后兩者最好也別用gbk或者utf8來encode,會引起亂碼或encode錯誤。

 

  str是python中默認字符串的類型,對於含有非英文或者說非ascii組成的字符串,在內存中的本質是一個編碼和內容的對應。

  當要進行運算比如加減的時候,應該盡量統一類型,str+str,unicode+unicode。如果實在是會出現混合的情況,那么python默認的動作是用系統默認編碼(sys.getdefaultcoding的那個)來decode算式中的str類型,把它變成unicode,然后再將其與其他的unicode相加,最終返回一個unicode。

 

  ●  encode和decode

   unicode和文本是對應的,這種對應和encode,decode沒有關系,就是說這兩個函數是無法改變這種對應關系的。但他們能改變的是unicode或者說文本到字符串的對應關系。比如對於“你”這個字:

  

  encode和decode的工作,說白了,就是進行\u4f60到\xc4\xe3或者\xe4\xbd\xa0的操作以及反操作。一般,encode的操作對象是unicode,而decode的對象是str。但是這不絕對,畢竟他們進行轉化的對象是內存里的數據,只要你有數據,en/decode就會嘗試去轉化。這個過程中有可能會報錯。另一方面也要注意真正decode的是編碼而不是內容,這點很容易忽視是因為在常用的gbk和utf8的環境下,同樣內容用環境的編碼格式decode出來總是相同的unicode,致使下意識地感覺decode像是直接對內容的decode。然而在分別在gbk和utf8的環境中"不".decode("latin1")得到的unicode並不一樣,因為在兩個環境中,'不'本身的編碼就不一樣。內存中不一樣的數據去執行同一個方法得到的結果當然是不一樣的。

  可以有'Hello'.encode("xxx"),也可以有u'Hello'.decode("xxx")。str再encode的默認動作是,先把str用系統默認編碼(上面提到過其默認為ascii且改變的方法是setdefaultcoding),decode掉再做相應的encode動作。這解釋當沒有改變默認編碼時'English'.encode("xxx")不報錯,但是"中文".encode("xxx")一定會報錯的現象。因為此時decode用的默認編碼還是ascii,無法解析中文。

  另一方面,為啥英文的unicode還可以再decode我是還沒找到答案。那篇文章里也沒明說,不過實用過程中肯定不會有人這么用的。。姑且算是鑽個牛角尖吧= =

  總而言之,en/decode之所以會報錯是因為en/decode用的那個編碼方式沒有辦法理解內存中的二進制數據(沒有相關二進制到目標數據類型的映射)。
 

  ●  程序第一行或者第二行寫的#coding

  通常在程序第一行或者第二行會寫上一句#coding=utf-8或者gbk或者什么什么的,這是在干啥呢?

  當python文件寫上了這句話,並且確實是按照這句話中的coding為編碼方式保存了文件的話,那么首先,程序中就可以在注釋里寫中文,不報錯了。這是因為語法解釋器在碰到中文的時候會以這種編碼方式把它存進內存了。第二所有用中文寫的str變量如"中文"就直接被指定成是由那個編碼方式編碼而成的了,不指定的話中文str對象會報錯的。

 

  ●  關於latin-1編碼格式

  最近在處理mp3文件的時候碰到了latin-1編碼格式。對於這個格式有幾點想說的

  1. latin-1就是ISO-8859-1的別名,在不同場合下這種編碼格式有這兩種不同的叫法

  2. 如上面unicode一節中提到的,用latin-1編碼格式decode得到的unicode和gbk,utf8等得到的不同,不是u'\u...'而是u'\x....'。不過這也不是絕對的,用utf8來decode一些str比如"é"時得到的是u'\xe9'而不是u'\u00e9'。

  3. 通過latin1而decode得來的unicode,處理時最好還是能夠通過latin1 encode回來,並不是說用其他的編碼格式encode一定會報錯,但是也可能是亂碼。這和之前我們熟知的“u'\u...'類型的unicode不論其是通過gbk還是utf8 decode而來,都可以通過gbk和utf8 encode,然后用相應環境的shell打印出來都可以顯示正確的內容”這種模式不同是一個需要改變觀念的地方。

 

===================基本上就是這么多了,寫了這么多感覺還是不是很清楚。。唉,能力有限,如果要真的搞清楚,可能得去看python源碼了。下次弄本更加高階向一點的相關的書來看看吧。周六就要考專八了,現在還在水這個我也真是夠了【捂臉】=========


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM