Python學習之路day3-字符編碼與轉碼


一、基礎概念

  • 字符與字節

    字符是相對於人類而言的可識別的符號標識,是一種人類語言,如中文、英文、拉丁文甚至甲骨文、梵語等等。
    字節是計算機內部識別可用的符號標識(0和1組成的二進制串,機器語言),屬於機器語言。
    人與計算機交互就需要在人類語言和機器語言之間來回轉換,因此當把各種各樣的字符存儲或輸入到計算機時,最終都必須以字節形式來表示;反之當計
算機輸出相應信息給人類用戶時,最終也需要以人類可識別的字符形式來傳遞。
    綜上所述,字符與人類更為接近,而字節則與計算機(機器)更為接近。
   
上面的概念區分看起來非常清楚了,但實際應用尤其是在進行python開發又很容易犯迷糊,各種編碼什么的一旦有些許模糊就會出現意想不到的結果。那么怎么區分字符與字節呢?

    通用原則:
    Unicode才是真正的字符串,而ASCII、UTF-8、GBK等編碼格式表示的都是字節碼。

     原因是:字符串是由字符構成,字符在計算機硬件中通過二進制形式存儲,這種二進制形式就是字節碼(編碼)。如果直接使用 “字符串↔️字符↔️二進制表示(編碼)”,會增加不同類型編碼之間轉換的復雜性。所以引入了一個抽象層,“字符串↔️字符↔️與存儲無關的表示↔️二進制表示(編碼)” ,這樣,可以用一種與存儲無關的形式表示字符,不同的編碼之間轉換時可以先轉換到這個抽象層,然后再轉換為其他編碼形式。在這里,unicode 就是 “與存儲無關的表示”,是一種字符集,而ASCII、UTF-8、GBK等就是字符串對應的“二進制表示”。(以上文字絕大部分引自
鏈接:https://www.zhihu.com/question/23374078/answer/28710945,作者:flyer,原文博客成死鏈接了 https://link.zhihu.com/?target=http%3A//flyer103.diandian.com/post/2014-03-09/40061199665
    因此,Unicode解決了人類識別計算機語言的兼容性和可用性問題,是一種字符串(只是一種字符集,而沒有編碼格式),而ASCII、UTF-8、GBK等是解決字符信息(Unicode)如何在計算機內部存儲和表示的,是二進制形式的字節碼。
   
關於這點,我們可以在Python的官方文檔中經常可以看到這樣的描述"Unicode string" , " translating a Unicode string into a sequence of bytes",我們寫代碼是寫在文件中的,而字符是以字節形式保存在文件中的,因此當我們在文件中定義個字符串時被當做字節串也是可以理解的。但是,我們需要的是字符串,而不是字節串。(這段文字引用自http://www.cnblogs.com/yyds/p/6171340.html)Python2.0沒有對此作嚴格區分,好在Python 3里已經開始嚴格區分了,並且默認的字符集就是Unicode。換句話講,我們通過pycharm進行python 3.0開發時,編輯器里呈現的代碼都是字符串,都是Unicode,無論我們在文件頭聲明使用什么文件格式(文件頭的申明決定了源代碼文件以什么樣的二進制格式存儲)。

  • 編碼

    前面章節所述從人類可識別的字符到計算機可識別處理(存儲傳輸)的字節的轉換,就是一個編碼的過程,即encode。而ASCII、UTF-8、GBK等,則決定了如何把字符轉換為二進制字節碼(轉換規則)和轉換后的二進制碼的內容,所以它們是具體的編碼格式(同一個符號可以有多種二進制字節碼的表示)。借用網上某大神搬出來的通信理論強化一下,unicode是信源編碼,對字符集數字化;utf8是信道編碼,為更好的存儲和傳輸。

  • 解碼

    解碼就是編碼的逆過程,把計算機中存儲運行的二進制字節碼轉換為字符的過程,即decode。
簡單總結:
     編碼(encode):將Unicode字符串轉換特定字符編碼對應的字節串的過程和規則
     解碼(decode):將特定字符編碼的字節串轉換為對應的Unicode字符串的過程和規則

可見,無論是編碼還是解碼,都需要一個重要因素,就是特定的字符編碼。因為一個字符用不同的字符編碼進行編碼后的字節值以及字節個數大部分情況下是不同的,反之亦然。

    還是來一張圖示吧:

  image

  • Python源代碼的執行過程

    首先明確一下基本概念,我們借助Pycharm來進行Python程序開發時,需要與編輯器和解釋器打交道,Pycharm作為IDE就是編輯器,提高我們編寫代碼的效率;而我們安裝的來自官方的Python程序(在Pycharm中運行時顯示的python.exe)則是解釋器。
    結合上面章節對字符串和字節碼相互轉換的過程講解,可以確定在開發過程中會在以下兩個地方用到字符編碼:

  1. 磁盤文件的存儲寫入和讀取
    這是對源代碼文件編寫后的保存和讀取處理過程,由編輯器Pycharm自動完成。保存時需要encode,讀取時則需要decode,這是由編輯器指定的工程或文件的字符編碼決定的(Pycharm中還是推薦用默認的UTF-8)。
  2. 程序執行時的輸入和輸出
    這是程序執行時的信息的表示過程,由解釋器Python.exe完成。同理輸入時需要encode(轉換成機器碼),輸出時需要decode。請注意,這里的程序執行時的輸出不僅包括程序運行后的各種print,更重要的是包括程序啟動運行時Python解釋器將源代碼文件中的字節碼讀取后轉換為Unicode(decode)的過程,在此基礎上Python解釋器才會進行后續的處理(如print一個字符串或者變量)。這個過程需要我們指定源代碼文件中保存字節時所使用的字符編碼。
    總結如下:
    通過文件頭指定字符編碼:
      1 # -*- coding:utf-8 -*-

        Python源代碼執行過程:
        image
         圖片和上述思想主要摘自http://www.cnblogs.com/yyds/p/6171340.html
         補充一下,Python解釋器通過指定的字符編碼把字節碼轉換為Unicode只是程序執行的剛剛開始,后續解釋器還需要把Unicode字符串翻譯為c代碼(默認的cpython),然后轉換成二進制流通過操作系統的相應接口調用CPU來執行二進制數據(機器只識別二進制數據),最后以Unicode方式返回我們需要的結果。

二、Unicode的由來與UTF-8的關系

     理清了字符串和字節碼各自的概念和轉換過程, 現在不得不引入Unicode的由來以及它與UTF-8的關系了。
     Unicode的由來
    
眾所周知計算機由美國人發明, 由於英文字母相對固定簡單, 美國大叔僅僅通過不到128位的ASCII就解決了字符編碼問題. 隨着計算機技術的不斷發展和推廣,其他國家也陸續開始使用計算機.遺憾的是對於非英語母語國家而言,由於母語相對於英語字母的復雜性和多樣性,不到128位的ASCII沒法扛起字符編碼的重擔.於是很多多家都結合自己的母語開發了適用於本國語言的字符編碼體系,我們現在熟知的GBK,GB2312等國標都是那個時期的產物。
     由於字符編碼的各自為政,使得不同國家之間的交流變得很不順暢,一不小心就踩到亂碼了. 於是ISO集全球之力量,經過充分調研和科學設計,制定了可以包羅萬象、能搞定世界上基本所有語言編碼問題的Unicode,人稱萬國碼。
     請注意,Unicode本身並不是一種字符編碼,而是一種字符集。如果直接使用 “字符串↔️字符↔️二進制表示(編碼)”,會增加不同類型編碼之間轉換的復雜性。所以引入了一個抽象層,“字符串↔️字符↔️與存儲無關的表示↔️二進制表示(編碼)” ,這樣,可以用一種與存儲無關的形式表示字符,不同的編碼之間轉換時可以先轉換到這個抽象層,然后再轉換為其他編碼形式。在這里,unicode 就是 “與存儲無關的表示”,是一種字符集,而ASCII、UTF-8、GBK等就是字符串對應的“二進制表示”。

     Unicode與UTF-8的關系
    
很多地方會很簡單地講UTF-8就是Unicode的壓縮和優化版,經過前面章節的分析我們發現這種說法不嚴謹。還是先復習下通信理論,unicode是信源編碼,對字符集數字化;utf8是信道編碼,為更好的存儲和傳輸。Unicode是一張字符與數字的映射,但是這里的數字被稱為代碼點(code point), 實際上就是十六進制的數字。Python官方文檔中對Unicode字符串、字節串與編碼之間的關系有這樣一段描述:
     Unicode字符串是一個代碼點(code point)序列,代碼點取值范圍為0到0x10FFFF(對應的十進制為1114111)。這個代碼點序列在存儲(包括內存和物理磁盤)中需要被表示為一組字節(0到255之間的值),而將Unicode字符串轉換為字節序列的規則稱為編碼

     簡單來講,Unicode字符集可以將字符用一定范圍的數字來進行數字化,至於怎么把數字化表示為二進制,一千個人眼里有一千個哈姆雷特,這個過程就交給字符編碼來顯神通了,UTF-8剛好是其中一種且通用性很強的一種。因此可以講utf8是對unicode字符集進行編碼的一種編碼方式,是對unicode字符集的二進制體現;Unicode是解決如何向人類展現可被識別的字符問題,而UTF-8是解決如何將人類可識別的字符轉換為可被機器識別的字節碼問題。

三、Python中的字符編碼

  • 默認字符編碼

    設想一個問題,如果我們在編寫Python代碼文件時沒有指定編碼格式,程序在執行時解釋器又怎么把字節碼轉換成Unicode呢?這里由Python中默認的字符編碼來決定。當我們的默認字符編碼與編輯器里設置的字符編碼不一致時,可通過文件頭來進行字符編碼格式的聲明來解決,而如果此時沒有文件頭里面的聲明,則會直接導致解碼失敗。Python2中解釋器默認的字符編碼是ASCII,Python3中解釋器默認的字符編碼是UTF-8。這就是Python2里默認情況下不支持中文字符的原因(沒有通過文件頭申明文件編碼就調用默認的ASCII進行解碼,顯然ASCII碼里沒有中文對應的字符)。

    Python解釋器的默認編碼可通過以下方式確認:

[root@node1 ~]# python
Python 2.6.6 (r266:84292, Aug 18 2016, 15:13:37)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-17)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>>

C:\Users\Beyondi>python
Python 3.5.2 (v3.5.2:4def2a2901a5, Jun 25 2016, 22:18:55) [MSC v.1900 64 bit (AM
D64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getdefaultencoding()
'utf-8'
>>>

Python2下未聲明字符編碼情況下對中文字符的支持情況:

  1 [root@node1 python]# cat test.py
  2 #! /usr/bin/env python
  3 s='你好,世界!'
  4 print(s)
  5 [root@node1 python]# python test.py
  6   File "test.py", line 2
  7 SyntaxError: Non-ASCII character '\xe4' in file test.py on line 2, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

上面的報錯很明顯了,第2行中需要打印的字符串ASCII不支持,並且也沒有對字符編碼進行顯示聲明。
現在在文件頭增加字符編碼的顯示聲明:

  1 [root@node1 python]# cat test.py
  2 # -*- coding:utf-8 -*-
  3 #! /usr/bin/env python
  4 s='你好,世界!'
  5 print(s)
  6 [root@node1 python]# python test.py
  7 你好,世界!

增加正確的字符編碼聲明后,程序在執行時解釋器會通過指定的字符編碼UTF-8來對保存的字節碼進行解碼,然后進行后續的print處理,由於Linux環境下默認的字符編碼也是UTF-8,所以中文字符就能正常顯示了。如果把這段程序放到windows下運行(Python3解釋器),情況又是怎樣的呢?
通過記事本復制代碼后保存為test.py,然后在cmd下運行:

  1 D:\python\S13\Day3>type test.py
  2 # -*- coding:utf-8 -*-
  3 #! /usr/bin/env python
  4 s='你好,世界!'
  5 print(s)
  6 D:\python\S13\Day3>python test.py
  7   File "test.py", line 3
  8 SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0xc4 in position 0:
  9  invalid continuation byte

從輸出結果來看程序還是報錯了,何故?我們不是在文件頭中聲明了字符編碼了嗎?這里的問題在於我們通過記事本保存代碼文件時默認的字符編碼是windows中文版下的GBK,而程序執行時Python解釋器會用我們在文件頭中聲明的UTF-8去解碼成Unicode來進行后續的處理,用UTF-8去解碼GBK編碼,當然不能解了。所以要注意的是,需要保持解釋器的字符編碼與編輯器的字符編碼一致。所以強烈建議在Pycharm中把默認的字符編碼設置為UTF-8,免得給自己挖坑。
實際測試表明Pycharm是一個比較智能的IDE,如果通過頁面右下角的按鈕改變了文件編碼,運行程序時文件頭部的編碼聲明會自動調整以保持兩者一致,此時你會發現右下角的改變文件編碼按鈕會有如下提示:
image

此時我們需要把聲明的字符編碼改成與編輯器一致的字符編碼GBK即可(或者修改編輯器保存文件時的字符編碼也OK,總之要保持一致):

  1 D:\python\S13\Day3>type test.py
  2 # -*- coding:GBK -*-
  3 #! /usr/bin/env python
  4 s='你好,世界!'
  5 print(s)
  6 D:\python\S13\Day3>
  7 D:\python\S13\Day3>
  8 D:\python\S13\Day3>python test.py
  9 你好,世界!
 10 
 11 D:\python\S13\Day3>

補充一點,這里如果把需要打印輸出的字符串內容改為英文字符,那么不修改文件頭部的字符編碼也可以正常輸出:

  1 D:\python\S13\Day3>type test.py
  2 __author__ = 'Maxwell'
  3 #!/usr/bin/env python
  4 # -*- coding: utf-8 -*-
  5 s = 'Hello, world!'
  6 print(s)
  7 D:\python\S13\Day3>
  8 D:\python\S13\Day3>
  9 D:\python\S13\Day3>python test.py
 10 Hello, world!

這是因為英文字符在UTF-8和GBK下的二進制碼是相同的。

  1 __author__ = 'Maxwell'
  2 #!/usr/bin/env python
  3 # -*- coding: utf-8 -*-
  4 s = 'Hello, world!'
  5 print(s.encode('utf-8'))
  6 print(s.encode('GBK'))
  7 print(type(s.encode('utf-8')))
  8 print(type(s.encode('GBK')))
  9 
 10 
 11 輸出:
 12 b'Hello, world!'
 13 b'Hello, world!'
 14 <class 'bytes'>  #注意encode后類型變為二進制了
 15 <class 'bytes'>

所以,除非特別需要,強烈建議在代碼一貫保持用英文字符。

        綜上所述,強烈建議遵循以下原則進行Python開發:

    1. 將IDE的字符編碼設置為UTF-8
    2. 習慣性地在代碼文件頭部分明確聲明使用UTF-8字符編碼 -*- coding:utf-8 –*-
      這樣可以保持程序對Python2解釋器的良好兼容性。
  • Python 2 VS Python 3
    從事Python開發一定要了解Python2與Python3的差異性,在此從字符編碼相關的角度略微展開。
    首先,Python2的默認編碼是ASCII,而Python3的默認編碼是UTF-8。
    其次,Python2和Python3在字符串與字節碼的處理上截然不同
    Python2中字符串有str和Unicode兩種類型,但str表示的是經過各種字符編碼編碼后的二進制字節碼,Unicode則是沒有編碼的標准文本。Unicode經過編碼轉換為為str,str經過解碼后還原成Unicode。這里把字符串和字節碼混在一起了。
    Python3中str類型即為Unicode字符串,單獨設立的bytes表示經過編碼處理后的二進制字節碼。因此Python3中對字符串和字節碼作了嚴格的區分。str經過編碼后轉換為bytes類型的字節碼,反過來bytes類型的字節碼經過解碼則可以還原為Unicode字符串。
    還是來一張形象的圖示吧:
    Horizontal Cross Functional Template (1)

    通過上圖可看出,Python3中的str類似於Python2中的Unicode,而Python3中的bytes則類似於Python2中的str,但請注意僅僅是類似而不是等同於,因為還存在其他層面的區別:
    (1)Python2中可直接查看str的Unicode字節序列
      1 [root@node1 python]# python
      2 Python 2.6.6 (r266:84292, Aug 18 2016, 15:13:37)
      3 [GCC 4.4.7 20120313 (Red Hat 4.4.7-17)] on linux2
      4 Type "help", "copyright", "credits" or "license" for more information.
      5 >>> s=u'你好,世界!'
      6 >>> s
      7 u'\u4f60\u597d\uff0c\u4e16\u754c!'

    (2)Python3中open函數加上了encoding參數,默認傳遞的字符編碼是UTF-8,對文件進行read或write時,只接受包含Unicode的字符串。對於二進制文件,則需要以rb,wb或ab模式打開。
    借用大神的一張形象展示一下(引用自 http://www.cnblogs.com/yuanchenqi/articles/5956943.html):
    image

四、實戰字符編碼轉碼

    經過前面章節的理論鋪墊,是時候來展開字符編碼與轉碼的重點了。
    先從Python3開始:
    前面章節闡述過,Unicode是一個抽象層,與存儲無關的表示,同時又包羅萬象,可以很方便地與其他字符編碼進行轉換,因此它可以作為中間代理人來進行不同編碼之間的轉換,以簡化不同編碼之間直接轉換的復雜度。實際應用中正是這樣:

 
    通過例子來實戰一下:

  1 __author__ = 'Maxwell'
  2 #!/usr/bin/env python
  3 # -*- coding: utf-8 -*-
  4 s = '你好,世界!'
  5 print(s.encode('utf-8'))
  6 print(s.encode('GBK'))
  7 s_to_gbk = s.encode('GBK')
  8 print(s_to_gbk.decode('GBK').encode('utf-8'))
  9 print(s_to_gbk.decode('GBK').encode('utf-8').decode('utf-8')) #連續轉碼處理,GBK解碼后以utf-8方式編碼,最后解碼還原成Unicode
 10 
 11 輸出:
 12 b'\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8c\xe4\xb8\x96\xe7\x95\x8c\xef\xbc\x81'
 13 b'\xc4\xe3\xba\xc3\xa3\xac\xca\xc0\xbd\xe7\xa3\xa1'
 14 b'\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8c\xe4\xb8\x96\xe7\x95\x8c\xef\xbc\x81'
 15 你好,世界!

  注意:
  1. 以上程序輸出表明,Unicode字符串經過編碼處理后立即變成bytes類型(輸出結果以b開頭,當然也可以通過type函數來驗證確定),需要打印時必須解碼轉換為Unicode字符串;
  2. 文件頭部的顯式字符編碼聲明僅僅決定了Python解釋器在執行程序的開始階段,會通過什么字符編碼去打開解碼保存的二進制字節碼代碼文件,它不改變程序代碼中定義的str類型是Unicode字符串這一事實(此時定義的字符串s仍然可以被各種encode)。Python3中內嵌原生地支持Unicode,定義的str類型默認就是Unicode字符串。

  Python2版本下的情況:

  1 [root@node1 python]# python
  2 Python 2.6.6 (r266:84292, Aug 18 2016, 15:13:37)
  3 [GCC 4.4.7 20120313 (Red Hat 4.4.7-17)] on linux2
  4 Type "help", "copyright", "credits" or "license" for more information.
  5 >>> s='你好,世界!'
  6 >>> s_to_unicode=s.decode('utf-8')
  7 >>> s_to_gbk=s_to_unicode.encode('GBK')
  8 >>> print(type(s))
  9 <type 'str'>
 10 >>> print(type(s_to_unicode))
 11 <type 'unicode'>
 12 >>> s
 13 '\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8c\xe4\xb8\x96\xe7\x95\x8c!'
 14 >>> s_to_gbk
 15 '\xc4\xe3\xba\xc3\xa3\xac\xca\xc0\xbd\xe7!'
 16 >>> print(s)
 17 你好,世界!
 18 >>> print(s_to_gbk.decode('GBK'))
 19 你好,世界!
 20 >>> print(type(s_to_gbk.decode('GBK'))) # decode之后類型變為Unicode
 21 <type 'unicode'>

Python2下的字符串是二進制字節碼形式,可以通過str= u’Hello,world!’直接轉換為Unicode。打印時可以對str本身打印,也可以對其轉換后的Unicode打印,因為Python2下沒有對字節和字符作嚴格區分。

  1 >>> s='你好,世界!'
  2 >>> print(s)
  3 你好,世界!
  4 >>> h=u'你好,世界!'
  5 >>> print(h)
  6 你好,世界!
  7 >>> h
  8 u'\u4f60\u597d\uff0c\u4e16\u754c!'
  9 >>> s
 10 '\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8c\xe4\xb8\x96\xe7\x95\x8c!'
 11 >>> print(s.decode('utf-8'))
 12 你好,世界!


免責聲明!

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



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