1. 程式人生 > >Python 和字元編碼

Python 和字元編碼

Python 2.* 的程式設計師肯定遇到過這樣那樣的字元編碼問題:
  • 為什麼從網站上爬取的html 在本地顯示的就不正常?
  • 為什麼會顯示 UnicodeEncodeError: 'ascii' codec can't encode character 這樣的錯誤
  • python 中encode(),decode() 方法如何使用?
這樣的字元編碼問題總會讓新手、老手都頭痛,有時候即使解決了問題也不能完全知道原理。然後似乎這樣的問題似乎在Python 3 中卻又解決了?Python 3 又是如何解決字元編碼問題?。寫在python 2.* 即將要退出歷史舞臺時候,不知道以後python 程式設計師還會不會遇到這樣的問題。但即使python 不會再有編碼問題的困擾,更低階的語言可能還是會遇到,字元編碼的原理也不會變化。

一、從編碼的歷史談起

將任何一門課程(專業)都要從這個課程所研究的歷史講起。這是因為了解了歷史問題,才能夠明白課程需要解決的真正問題是什麼?知道了這些年解決歷史問題的方法,才能夠反映出最新解決方法的先進性,也明白了為什麼我們(課程)要這麼做。 字元是資訊的一種體現,字元編碼也是資訊編碼的一個子集。如果考慮到古代的繩結記事、甲骨文和壁畫也是資訊的一種編碼,我們這裡所講的字元編碼指的是:自然語言字元(文字)在現代計算機上的對映方法。字元編碼的來源是人類自然語言(文字),編碼結果是儲存在現代計算機上的二進位制串。之所以是對映,因為每一個自然語言字元所對應的二進位制串是唯一的。 large

1.1 ASCII 碼錶

現代計算機起源於英語為母語的美國,所以最初美國工程師在字元編碼中首先考慮的是自己母語的使用方便,即26個英文字母(大小寫),0-9數字和常用字元的編碼。在1963年頒佈了ASCII (American Standard Code for Information Interchange)字元編碼表,如下所示: [caption id="" align="aligncenter" width="715"]ASCII 表
ASCII table[/caption] ASCII 表包含字元個數128(2^7)個,因此任何一個字元都可以在一個位元組記憶體儲下來,不會出現超過一個位元組的字元。計算機解碼時按位元組解碼即可,非常方便。ASCII 表不僅定義了像空格(space)、字母等可見字元,還定義了一些不可見字元。使用下面的程式碼可以輸出0-127 的ASCII 碼錶。
for ii in xrange(0,128) print chr(ii)+'\t'
ASCII碼錶很好的解決了英語為母語的計算機使用者的資訊儲存問題,但人類的科技進步的成果也必須全人類共享啊。但如果僅靠一個位元組來儲存全世界所有語言的字元肯定是不合適的。如果法國人要將自己語言中的 é, è, ê or ë 輸入到計算機怎麼辦呢?更不用說中韓日語以龐大數量漢字為基礎了。

1.2 GB2312 中文字元編碼

為相容本地語言,各個國家開始制定了自己語言的編碼方法。但為了向下相容英語,各種編碼方法都是在ASCII碼上進行了擴充套件。我國常使用的GB2312 是最常見的中文編碼方法,GB2312 於1980 年頒佈,所以其標準號是GB2312-1980。GB2312 按兩個位元組編碼一箇中文字元,共收集了6763 個漢字和682 個非漢字圖形。GB2312 不僅收錄了中文,還收錄了拉丁字母、希臘字母、日文片假名、平假名字母等。 GB2312 將兩個高、低位元組(第一個位元組為高位元組,第二個為低位元組)分別稱為區位元組和位位元組,一個區包含94個字,共計94個區。GB2312 規定區位元組僅 0xA1-0xF7 (01-87加上0xA0)儲存漢字,位位元組 0xA1-0xFE 儲存漢字。GB2312之所以從A0 開始編碼可以避免與ASCII的衝突,比如開啟一個GB2312 編碼的檔案,如果位元組值小於128,則這是ASCII字元;如果位元組值大於128,則這個位元組屬於一個漢字。 下面的方法給出了中國字元“您好!”和GB2312 編碼後結果:
>>> ("你好").decode('gb2312').encode('gb2312')
'\xc4\xe3\xba\xc3'
>>>
這裡需要解釋下decode() 和encode() 函數了。首先需要明白Python 中有兩種字串型別,一種是str 字串,一種是unicode 字串。比如 '你好' ,'news','나는 당신을 사랑합니다' 是str 字串,而 u'你好' 則是str 字串。

decode() 是將str 字串轉化為unicode 字串

encode() 是將unicode 字串轉化為str 字串

以GB2312 為代表的區域性字元編碼方法還是存在著通用性的問題。
  1. 因為不同國家指定了不同的字元編碼方法,相同的0/1 字串在不同的字元編碼方法中很可能對應不同的字元。那麼開啟一個檔案,首先得知道它的編碼方法才行。
  2. 為全世界語言字元進行統一的字元編碼(或者制定一套各地域字元編碼方法的轉換規則),兩個位元組肯定是不夠的,那麼需要更多位元組來儲存字元麼?

1.3 unicode

unicode 可以看做一個終極的字元編碼方法,它給出了地球上常用字元的二進位制對映,而且所有的二進位制字串唯一地表示一個字元。當然unicode 也向下相容ASCII,下面給出了一些字元所對應的二進位制、十六進位制值。
字元   十六進位制    二進位制
I      49        01001001
J      4A        01001010
日     65e5      01101001 11100101
ᅱ      FFD6      11111111 11010110
♀      2640      00100110 01000000
♬      266C      00100110 01101100
unicode包含了人類常用的語言字元、標識等字元編碼,目標是統一全球字元編碼,它也似乎正在向這終極目標邁進。但也可以看出unicode 只給出了字元和二進位制串的對應關係,並沒有給出儲存形式。而不同字元所佔用的儲存空間可能不同,比如ASCII 在unicode 中只佔用了一個位元組即可,而常用漢字在unicode 中需要佔用兩個位元組,還有一些羅馬字元可能需要三個或以上位元組。如果直接儲存的話可能導致無法分割字串,也無法正確解碼出字元,比如計算機讀到了“65e5”,這是中文的日字還是兩個字元('65' 和 'e5'在unicode 中對對應的字元)呢?
關於unicode 問題:如果計算機讀到一個位元組,如何判斷這個位元組是新的字元的開始,還是一個未讀完字元的繼續呢?
這個問題在ASCII 碼錶和GB2312 中是不存在的,因為所有字元都是固定長度。那麼unicode 可不可以也使用最大的長度字元的位元組數來表示呢?當然可以,比如最大unicode 用四個位元組可以儲存,那麼所有的字元都佔用四個位元組,但問題就是需要的儲存空間變得很大。比如unicode 對應字元的儲存如下(這麼多0 是不是浪費了很多儲存空間呢?):
I    00000049
J    0000004A
日   000065e5
ᅱ    0000FFD6
♀    00002640
♬    0000266C
一個可以想到的辦法就是在一個8 bits 的位元組中,第一位用來儲存是否是字元的開始(1表示這是一個新字元,0表示這個字元的unicode 還沒有結束),後面七位用來儲存unicode 的值。這是一種變長編碼方法,即每個字元的編碼後所佔空間是不同的(下面稱作首位編碼)。
字元   十六進位制     二進位制               首位編碼
I       49       01001001              11001001
J       4A       01001010              11001010
日      65e5     01101001 11100101     10000001 01010011 01100101
ᅱ       FFD6     11111111 11010110     10000011 01111111 01010110
♀       2640     00100110 01000000     11001100 01000000
♬       266C     00100110 01101100     10010011 00011011
每次解碼時,將字元每個位元組除去第一位(紅色標示)的其他位拼接得到的值作為其unicode 值。但這種方法我能想到的存在問題就是效率低,處理器每次只能一個位元組一個位元組的處理,讀取到第一個位元組時並不能知道這個字元佔多少個位元組,而且沒有辦法知道中間資料是否發生損壞。而我們常見的UTF-8 卻可以很好解決這個問題。

1.4 UTF-8

該UTF-8 出場了,簡單地說,UTF-8 是unicode 在計算機中的一種實現方式。和我們上節提到的首位編碼類似,也是一種變長編碼,每個字元佔1-4 個位元組。UTF-8 將位元組分為數值位和標識位,數值位真正儲存字元編碼數值標識位表示這個位元組是屬於哪個字元的、或者該字元佔多少個位元組。UTF-8 編碼方法的: 單位元組,首位為標識位0;多位元組字元首位元組標誌位1··10開頭,字元佔多少位元組則有多少1,其他位元組標識位10開頭;
  • 單位元組字元: 0xxxxxxx (以0 開頭標誌位,數值位用x 表示)
  • 雙位元組字元: 110xxxxx 10xxxxxx
  • 三位元組字元: 1110xxxx 10xxxxxx 10xxxxxx
  • 四位元組字元: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
解碼時,如果位元組首位是0,那麼這個位元組是個字元;如果位元組以i 個1開頭,那麼這個是一個i 位字元。那麼unicode 又是如何填入到UTF-8 的空缺(x)中呢?首先來看看每個多位元組UTF-8 編碼對應unicode 值的範圍:
UTF-8二進位制                           unicode二進位制
0xxxxxxx                              00-7F
110xxxxx 10xxxxxx                     0080-07FF
1110xxxx 10xxxxxx 10xxxxxx            0800-FFFF
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx   00010000-10FFF
unicode 變為UTF-8 編碼非常簡單,unicode 二進位制按照從低到高,填充UTF-8的數值位,除去那些不真正表示數值的標識位(位元組開頭的0,10,110,1110和11110),順序也是由低到高。以漢字“你”為例,看看unicode 如何轉換成UTF-8 編碼。
>>> (u"你").encode('utf-8')
'\xe4\xbd\xa0'
>>> (u"你")
u'\u4f60'
“你”字unicode 編碼為 '4f60' (二進位制 '01001111 01100000')。從“你”的unicode 值範圍可以看到需要三個位元組,接著從低位位元組向高位位元組填充得到“你”的UTF-8 編碼(高位沒有填充完則用0補充)。
字元    unicode十六進位制   unicode二進位制        UTF-8二進位制                  UTF-8十六進位制
 你        4f60        01001111 01100000   11100100 10111101 10100000    e4bd60
可以看到將UTF-8 用於標記位(紅色)的位去掉,合併可以得到原始的unicode 碼。

二、檔案和終端編碼格式

所有的文字檔案在計算機中儲存的都是一串有限長度的二進位制串,只有使用合理的編碼方式才能正確地顯示檔案,但檔案本身又是如何告訴編輯器它是如何編碼的呢?

2.1 py 檔案的字元編碼

實際上如果檔案本身不申明,編輯器是不知道一個檔案所採用的編碼方法的。所以編輯器有一個預設的開啟方式,比如記事本(notepad)和notepad++ 都預設用ASCII 開啟檔案。作為Python 初學者可能會遇到下面的一個問題: 故障1:新建一個test.py 檔案,用記事本預設開啟,輸入下面內容:
import os
print "你" 
儲存後在終端執行"python test.py" 發現提示錯誤:
>>python test.py
File "test.py", line 2
SyntaxError: Non-ASCII character '\xc4' in file test.py on line 3, but no encoding 
declared; see http://python.org/dev/peps/pep-0263/ for details
發現提示存在非ASCII 碼字元,這是因為在記事本中中文預設使用GB2312編碼,"你"的GB2312 編碼是"\xc4\xe3",於是在執行test.py 檔案時,出現了無法識別的字元'\xc4'。如果將檔案修改為'UTF-8' 編碼時,則可以看到因為"\xc4\xe3"是不合法的字串,所以顯示為它的二進位制內容。 utf8save 寫過Python 程式的都知道,為了避免py 檔案內的編碼問題,需要在檔案首做一個申明:
#_*_encoding:utf-8_*_
這樣,編輯器在開個你這個檔案的時候就會預設按照UTF-8 格式開啟。當然,UTF-8 也可以換做其他格式。

2.2 python 與檔案的字元編碼

除了py 檔案本身要儲存為UTF-8 格式,以避免出現py 檔案出現的非ASCII 字元無法被正確的識別。當py 訪問其他檔案,將資料寫入檔案時也需要注意字元編碼。 故障2:Python2 預設編碼是ASCII 碼
>>> example = u'你好'
>>> str(example)
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
str(example)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: 
ordinal not in range(128)
>>> example = u'你好'
>>> open('temp.txt','w').write(example)
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
open('temp.txt','w').write(example)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: 
ordinal not in range(128)
當用u'字串' 的形式申明這個字串變數時,也就指明瞭該字串是使用unicode 字元編碼。當如果要將unicode 字串轉換為str 字串(python2 認為unicode 字串和str 字串是不同的)或者寫入檔案時,python 將預設使用ASCII 碼儲存資料,而ASCII 碼無法識別大於128 的字元,於是報了上面的錯誤。 類似這樣的錯誤還有:
>>> unicode('abcdef' + chr(255))
Traceback (most recent call last):
...
UnicodeDecodeError: 'ascii' codec can't decode byte 0xff in position 6: 
ordinal not in range(128)

2.3 終端編碼

上linux 作業系統課程的時候,我們被教導到:一切都是檔案,終端作為一種裝置也是檔案,當需要輸出內容到終端時,實際上也是按照終端對應的字元編碼顯示相應的內容。 故障3:如果你在windows 終端cmd.exe 執行下面程式碼(需要安裝requests 包)。
import os,sys
import requests
ss = requests.Session()
res = ss.get("http://www.hust.edu.cn/")
con = res.content
print con
這段程式碼是獲取http://www.hust.edu.cn/ 頁面的內容,並在終端顯示,(如下圖)這裡出現了亂碼。 cmd 但如果你進一步將con 中內容寫入到檔案或者在linux 下執行這段程式碼,可能則沒有亂碼的問題。問題出在哪裡呢?原因是因為頁面內容是UTF-8 編碼(可以在頁面原始碼<meta charset="utf-8">中看到 ),而windows 命令提示符cmd 中卻使用的是mbcs 字元編碼。可以通過下面的命令檢視該終端的字元編碼
print sys.getfilesystemencoding()
熟悉Linux 的應該知道,終端也是要設定語言格式的,否則在此終端上顯示的檔案,檔名以及輸出到終端的內容會存在亂碼。可以通過下面命令設定linux 終端字元編碼為UTF-8:
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LANGUAGE=en_US.UTF-8

2.4 str 和unicode

計算機儲存的資料,以及從檔案中讀取的資料都可以理解為str 字串,str 字串是需要字元編碼才能夠被計算機所理解,而unicode 字串採用統一的字元編碼方法。為節省空間等原因計算機並不會直接儲存unicode 型別。但在Python2 處理字元資料時,建議全部轉化為unicode。 但如果不知道str 字串編碼型別,按照Python2 預設的ASCII 碼轉換出現大於128 的位元組的錯誤怎麼辦(UnicodeEncodeError: 'ascii' codec can't encode characters)? 有兩種方法:一是用特定的數值(magic number)替代錯誤的位元組;二是乾脆忽略錯誤位元組

>>> unicode('\x80abc', errors='replace')
u'\ufffdabc'
>>> unicode('\x80abc', errors='ignore')
u'abc'
同樣地,encode() 和decode() 也可以這樣使用。 str 字串和unicode 字串進行比較,或者合併兩個字串時,首先將它們轉換為unicode 格式,再計算它們的值。比如合併str 字串'你' 和unicode 字串u'你',因為'你' 按照預設的ASCII 碼發現無法解碼'你',於是報錯了~~~
>>> '你'+u'你'
Traceback (most recent call last):
 File "<pyshell#0>", line 1, in <module>
 '你'+u'你'
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc4 in position 0: 
ordinal not in range(128)
看下面的例子比較str 和unicode 字串,python2 按照預設的ASCII 碼解碼為unicode 過程中發現無法識別的位元組,然後就返回不相等(False),並丟擲一個異常。
>>> if '你' == u'你':
...    print True
>>> 
>>> '你' == u'你'
__main__:1: UnicodeWarning: Unicode equal comparison failed to convert both argu
ments to Unicode - interpreting them as being unequal
False
正確比較方法應該是:
>>>'你'.decode('gb2312') == u'你':
True
>>> 
如果你在百度或者Google 上搜索解決str 字串在轉碼為unicode 時不知道字元編碼型別的“問題”時,大部分時候你會看到大家提出在py 檔案上加上兩句:
reload(sys)
sys.setdefaultencoding('utf-8')
這樣py 檔案中str 字串預設以UTF-8 編碼。但一位有經驗的Python 程式設計師會告訴你儘量不要這樣做,因為這樣在對於不是UTF-8 編碼的字元時,還是會出現亂碼,下節再談這點。

2.5 _*_encoding:utf-8_*_ 和 reload(sys) sys.setdefaultencoding('utf-8')

有了上面一節的基礎,我們大概知道了
#_*_encoding:utf-8_*_
放在py 原始碼之前申明,表示py 原始檔是UTF-8 編碼(即檔案中所有的字元都是UTF-8 編碼)。這樣的好處是如果py 原始檔中存在非ASCII 碼的字元(你寫程式碼時所輸入的UTF-8 字元),python 程式也能夠正常的識別為你所輸入的字元,而不是按預設為ASCII 碼(這樣在執行時會丟擲無法識別為ASCII 碼的錯誤)。 這種申明檔案編碼型別是我們推薦的,而下面
reload(sys)
sys.setdefaultencoding('utf-8')
這兩句話申明py 檔案中所有str 都是UTF-8 編碼的,而這種強制定義所有str 字串為UTF-8 編碼是我們不推薦的。比如會遇到下面的問題,在一個測試檔案test.py 中有內容:
#_*_encoding:utf-8_*_
import os
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

sym1 =u'♀'+'你'
sym2 = '你'.decode('utf-8').encode('gb2312')
ff = open('temp.txt','w')
ff.write(sym1)
ff.write(sym2)
ff.close()
然後用notepad++開啟temp.txt 檔案,看到了什麼內容呢? setdefault 不難看出,'♀' 和' 你' 顯示都沒有問題,但後一個'你' 就因為使用的是GB2312 編碼,寫入檔案後,前兩個字元都是UTF-8 編碼,後一個字元GB2312 編碼,用UTF-8 格式開啟時,最後一個編碼肯定就顯示不正常了。 當然,上面只是一個例子,很多時候你不會在程式中寫出這麼buggy 的程式碼,但是很多時候用python 處理一些非UTF-8 編碼的檔案或者資料時,就會因為你強制申明str 為UTF-8 編碼導致很多問題,比如讀取非UTF-8 編碼的Oracle 的資料庫,修改、新增非UTF-8 編碼的檔案時,就會產生很多字元編碼混亂。因此也有人建議python2 程式設計師在處理str 時能夠顯示地使用特定合適的字元編碼,而不是預設地使用UTF-8。 Python3 只有unicode 字串,而沒有str 字串,上面所遇到的字元編碼的坑不再有了。 所以—— Bravo~ 學Python3 去吧,騷年!  

參考文章:



檢視原文:http://blog.foool.net/2016/07/python-%e5%92%8c%e5%ad%97%e7%ac%a6%e7%bc%96%e7%a0%81/