1. 程式人生 > 其它 >python 之路,致那些年,我們依然沒搞明白的編碼

python 之路,致那些年,我們依然沒搞明白的編碼

摘自:金角大王https://www.cnblogs.com/alex3714/articles/7550940.html

本節內容

  編碼回顧

  編碼轉換

  Python的bytes型別

 

編碼回顧

在備編碼相關的課件時,在知乎上看到一段關於Python編碼的回答

這哥們的這段話說的太對了,搞Python不把編碼徹底搞明白,總有一天它會猝不及防坑你一把。

不過感覺這哥們的答案並沒把編碼問題寫明白,所以只好親自動筆了。 

折騰編碼問題,有很多次,我以為自已明白了,最終發現,那隻不過是自圓其說而已,這一次,終於100%確定,動筆即不再改!

 

 

看這篇文章前,你應該已經知道了為什麼有編碼,以及編碼的種類情況

  • ASCII 佔1個位元組,只支援英文
  • GB2312 佔2個位元組,支援6700+漢字
  • GBK GB2312的升級版,支援21000+漢字
  • Shift-JIS 日本字元
  • ks_c_5601-1987 韓國編碼
  • TIS-620 泰國編碼

由於每個國家都有自己的字元,所以其對應關係也涵蓋了自己國家的字元,但是以上編碼都存在侷限性,即:僅涵蓋本國字元,無其他國家字元的對應關係。應運而生出現了萬國碼,他涵蓋了全球所有的文字和二進位制的對應關係,

  • Unicode 2-4位元組 已經收錄136690個字元,並還在一直不斷擴張中...

Unicode 起到了2個作用:

  1. 直接支援全球所有語言,每個國家都可以不用再使用自己之前的舊編碼了,用unicode就可以了。(就跟英語是全球統一語言一樣)
  2. unicode包含了跟全球所有國家編碼的對映關係,為什麼呢?後面再講

Unicode解決了字元和二進位制的對應關係,但是使用unicode表示一個字元,太浪費空間。例如:利用unicode表示“Python”需要12個位元組才能表示,比原來ASCII表示增加了1倍。

由於計算機的記憶體比較大,並且字串在內容中表示時也不會特別大,所以內容可以使用unicode來處理,但是儲存和網路傳輸時一般資料都會非常多,那麼增加1倍將是無法容忍的!!!

為了解決儲存和網路傳輸的問題,出現了Unicode Transformation Format,學術名UTF,即:對unicode中的進行轉換,以便於在儲存和網路傳輸時可以節省空間!

  • UTF-8: 使用1、2、3、4個位元組表示所有字元;優先使用1個字元、無法滿足則使增加一個位元組,最多4個位元組。英文佔1個位元組、歐洲語系佔2個、東亞佔3個,其它及特殊字元佔4個
  • UTF-16: 使用2、4個位元組表示所有字元;優先使用2個位元組,否則使用4個位元組表示。
  • UTF-32: 使用4個位元組表示所有字元;

總結:UTF 是為unicode編碼 設計 的一種 在儲存 和傳輸時節省空間的編碼方案。

 

字元在硬碟上的儲存 

無論以什麼編碼在記憶體裡顯示字元,存到硬碟上都是2進位制。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ascii編碼(美國):     l   0b1101100     o   0b1101111     v   0b1110110     e   0b1100101 GBK編碼(中國):     老   0b11000000 0b11001111     男   0b11000100 0b11010000     孩   0b10111010 0b10100010   Shift_JIS編碼(日本):     私   0b10001110 0b10000100     は   0b10000010 0b11001101   ks_c_5601-1987編碼(韓國):     나   0b10110011 0b10101010     는   0b10110100 0b11000010       TIS-620編碼(泰國):     ฉัน  0b10101001 0b11010001 0b10111001 ...
1 要注意的是,存到硬碟上時是以何種編碼存的,再從硬碟上讀出來時,就必須以何種編碼讀,要不然就亂了。。

  

編碼的轉換 

雖然國際語言是英語 ,但大家在自己的國家依然說自已的語言,不過出了國, 你就得會英語
編碼也一樣,雖然有了unicode and utf-8 , 但是由於歷史問題,各個國家依然在大量使用自己的編碼,比如中國的windows,預設編碼依然是gbk,而不是utf-8

基於此,如果中國的軟體出口到美國,在美國人的電腦上就會顯示亂碼,因為他們沒有gbk編碼。
若想讓中國的軟體可以正常的在 美國人的電腦上顯示,只有以下2條路可走:

  1. 讓美國人的電腦上都裝上gbk編碼
  2. 把你的軟體編碼以utf-8編碼

第1種方法幾乎不可能實現,第2種方法比較簡單。 但是也只能是針對新開發的軟體。 如果你之前開發的軟體就是以gbk編碼的,上百萬行程式碼可能已經寫出去了,重新編碼成utf-8格式也會費很大力氣。

so , 針對已經用gbk開發完畢的專案,以上2種方案都不能輕鬆的讓專案在美國人電腦上正常顯示,難道沒有別的辦法了麼?
有, 還記得我們講unicode其中一個功能是其包含了跟全球所有國家編碼的對映關係,意思就是,你寫的是gbk的“路飛學城”,但是unicode能自動知道它在unicode中的“路飛學城”的編碼是什麼,如果這樣的話,那是不是意味著,無論你以什麼編碼儲存的資料 ,只要你的軟體在把資料從硬碟讀到記憶體裡,轉成unicode來顯示,就可以了。
由於所有的系統、程式語言都預設支援unicode,那你的gbk軟體放到美國電腦 上,載入到記憶體裡,變成了unicode,中文就可以正常展示啦。

 這個表你自己也可以下載下來 

unicode與gbk的對映表 http://www.unicode.org/charts/ 

 

Python3的執行過程

在看實際程式碼的例子前,我們來聊聊,python3 執行程式碼的過程

  1. 直譯器找到程式碼檔案,把程式碼字串按檔案頭定義的編碼載入到記憶體,轉成unicode
  2. 把程式碼字串按照語法規則進行解釋,
  3. 所有的變數字元都會以unicode編碼宣告

編碼轉換過程

實際程式碼演示,在py3上 把你的程式碼以utf-8編寫, 儲存,然後在windows上執行,

1 2 = '路飛學城' print(s)

so ,一切都很美好,到這裡,我們關於編碼的學習按說就可以結束了。

 

但是,如生活一樣,美好的表面下,總是隱藏著不盡如人意,上面的utf-8編碼之所以能在windows gbk的終端下顯示正常,是因為到了記憶體裡python直譯器把utf-8轉成了unicode , 但是這只是python3, 並不是所有的程式語言在記憶體裡預設編碼都是unicode,比如 萬惡的python2 就不是, 它的預設編碼是ASCII,想寫中文,就必須宣告檔案頭的coding為gbk or utf-8, 宣告之後,python2直譯器僅以檔案頭宣告的編碼去解釋你的程式碼,載入到記憶體後,並不會主動幫你轉為unicode,也就是說,你的檔案編碼是utf-8,載入到記憶體裡,你的變數字串就也是utf-8, 這意味著什麼你知道麼?。。。意味著,你以utf-8編碼的檔案,在windows是亂碼。 

 

亂是正常的,不亂才不正常,因為只有2種情況 ,你的windows上顯示才不會亂

  1. 字串以GBK格式顯示
  2. 字串是unicode編碼

既然Python2並不會自動的把檔案編碼轉為unicode存在記憶體裡, 那就只能使出最後一招了,你自己人肉轉。Py3 自動把檔案編碼轉為unicode必定是呼叫了什麼方法,這個方法就是,decode(解碼) 和encode(編碼)

1 2 UTF-8 --> decode 解碼 --Unicode Unicode --> encode 編碼 --> GBK / UTF-8 ..

 

decode示例

 

encode 示例

 

記住下圖規則

 

如何驗證編碼轉對了呢?

1. 檢視資料型別,python 2 裡有專門的unicode 型別
2. 檢視unicode編碼對映表

  unicode字元是有專門的unicode型別來判斷的,但是utf-8,gbk編碼的字元都是str,你如果分辨出來的當前的字串資料是何種編碼的呢? 有人說可以通過位元組長度判斷,因為utf-8一箇中文佔3位元組,gbk一個佔2位元組

靠上面位元組個數,雖然也能大體判斷是什麼型別,但總覺得不是很專業。

怎麼才能精確的驗證一個字元的編碼呢,就是拿這些16進位制的數跟編碼表裡去匹配。

“路飛學城”的unicode編碼的對映位置是 u'\u8def\u98de\u5b66\u57ce' ,‘\u8def’ 就是‘路’,到表裡搜一下。
“路飛學城”對應的GBK編碼是'\xc2\xb7\xb7\xc9\xd1\xa7\xb3\xc7' ,2個位元組一箇中文,"路" 的二進位制 "\xc2\xb7"是4個16進位制,正好2位元組,拿它到unicode對映表裡對一下, 發現是G0-4237,並不是\xc2\xb7呀。。。擦。演砸了吧。。

   

再查下“飛” \u98de ,對應的是G0-3749, 跟\xb7\xc9也對不上。

 

 

雖然對不上, 但好\xc2\xb7 和G0-4237中的第2位的2和第4位的7對上了,“飛”字也是一樣,莫非巧合? 

把他們都轉成2進位制顯示試試 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 C               2 8   4   2   1   8   4   2   1 <strong>1   1   0   0   0   0   1   0</strong>   B               7 8   4   2   1   8   4   2   1 <strong>1   0   1   1   0   1   1   1</strong>     B               7 8   4   2   1   8   4   2   1 1   0   1   1   0   1   1   1   C               9 8   4   2   1   8   4   2   1 1   1   0   0   1   0   0   1

這個“路”還是跟G0-4237對不上呀,沒錯, 但如果你把路\xc2\xb7的每個二進位制位元組的左邊第一個bit變成0試試呢, 我擦,加起來就真的是4237了呀。。難道又是巧合???  

 

必然不是,是因為,GBK的編碼表示形式決定的。。因為GBK編碼在設計初期就考慮到了要相容ASCII,即如果是英文,就用一個位元組表示,2個位元組就是中文,但如何區別連在一起的2個位元組是代表2個英文字母,還是一箇中文漢字呢? 中國人如此聰明,決定,2個位元組連在一起,如果每個位元組的第1位(也就是相當於128的那個2進位制位)如果是1,就代表這是個中文,這個首位是128的位元組被稱為高位元組。 也就是2個高位元組連在一起,必然就是一箇中文。 你怎麼如此篤定?因為0-127已經表示了英文的絕大部分字元,128-255是ASCII的擴充套件表,表示的都是極特殊的字元,一般沒什麼用。所以中國人就直接拿來用了。 

 

問:那為什麼上面 "\xc2\xb7"的2進位制要把128所在的位去掉才能與unicode編碼表裡的G0-4237匹配上呢?

這隻能說是unicode在對映表的表達上直接忽略了高位元組,但真正對映的時候 ,肯定還是需要用高位元組的哈。

 

Python bytes型別

在python 2 上寫字串

1 2 3 4 5 >>> s = "路飛" >>> print s 路飛 >>> s '\xe8\xb7\xaf\xe9\xa3\x9e'

雖說列印的是路飛,但直接呼叫變數s,看到的卻是一個個的16進製表示的二進位制位元組,我們怎麼稱呼這樣的資料呢?直接叫二進位制麼?也可以, 但相比於010101,這個資料串在表示形式上又把2進位制轉成了16進位制來表示,這是為什麼呢? 哈,為的就是讓人們看起來更可讀。我們稱之為bytes型別,即位元組型別, 它把8個二進位制一組稱為一個byte,用16進位制來表示。  

說這個有什麼意思呢?

想告訴你一個事實, 就是,python2的字串其實更應該稱為位元組串。 通過儲存方式就能看出來, 但python2裡還有一個型別是bytes呀,難道又叫bytes又叫字串? 嗯 ,是的,在python2裡,bytes == str , 其實就是一回事 

除此之外呢, python2裡還有個單獨的型別是unicode , 把字串解碼後,就會變成unicode

1 2 3 4 5 6 >>> s '\xe8\xb7\xaf\xe9\xa3\x9e' #utf-8 >>> s.decode('utf-8') u'\u8def\u98de' #unicode 在unicode編碼表裡對應的位置 >>> print(s.decode('utf-8')) 路飛 #unicode 格式的字元


由於Python創始人在開發初期認知的侷限性,其並未預料到python能發展成一個全球流行的語言,導致其開發初期並沒有把支援全球各國語言當做重要的事情來做,所以就輕佻的把ASCII當做了預設編碼。 當後來大家對支援漢字、日文、法語等語言的呼聲越來越高時,Python於是準備引入unicode,但若直接把預設編碼改成unicode的話是不現實的, 因為很多軟體就是基於之前的預設編碼ASCII開發的,編碼一換,那些軟體的編碼就都亂了。所以Python 2 就直接 搞了一個新的字元型別,就叫unicode型別,比如你想讓你的中文在全球所有電腦上正常顯示,在記憶體裡就得把字串存成unicode型別

1 2 3 4 5 6 7 8 >>> s = "路飛" >>> s '\xe8\xb7\xaf\xe9\xa3\x9e' >>> s2 = s.decode("utf-8") >>> s2 u'\u8def\u98de' >>> type(s2) <type 'unicode'>


時間來到2008年,python發展已近20年,創始人龜叔越來越覺得python裡的好多東西已發展的不像他的初衷那樣,開始變得臃腫、不簡潔、且有些設計讓人摸不到頭腦,比如unicode 與str型別,str 與bytes型別的關係,這給很多python程式設計師造成了困擾。
龜叔再也忍不了,像之前一樣的修修補補已不能讓Python變的更好,於是來了個大變革,Python3橫空出世,不相容python2,python3比python2做了非常多的改進,其中一個就是終於把字串變成了unicode,檔案預設編碼變成了utf-8,這意味著,只要用python3,無論你的程式是以哪種編碼開發的,都可以在全球各國電腦上正常顯示,真是太棒啦!

PY3 除了把字串的編碼改成了unicode, 還把str 和bytes 做了明確區分, str 就是unicode格式的字元, bytes就是單純二進位制啦。

最後一個問題,為什麼在py3裡,把unicode編碼後,字串就變成了bytes格式? 你直接給我直接列印成gbk的字元展示不好麼?我想其實py3的設計真是煞費苦心,就是想通過這樣的方式明確的告訴你,想在py3裡看字元,必須得是unicode編碼,其它編碼一律按bytes格式展示。 

 

好吧,就說這麼多吧。 

 

 

最後再提示一下,Python只要出現各種編碼問題,無非是哪裡的編碼設定出錯了
常見編碼錯誤的原因有:

    • Python直譯器的預設編碼
    • Python原始檔檔案編碼
    • Terminal使用的編碼
    • 作業系統的語言設定

掌握了編碼之前的關係後,挨個排錯就好啦