Python2.x 字符編碼終極指南

人機交互之字符編碼 一文中對字符編碼進行了詳細的討論,并通過一些簡單的小程序驗證了我們對于字符編碼的認識漓滔。但僅了解這篇文章的內容编饺,并不能幫我們在日常編程中躲過一些字符編碼相關的坑,Stackoverflow 上就有大量編碼相關的問題响驴,比如 1透且,[2](Python: Convert Unicode to ASCII without errors),3豁鲤。

圖1. 錯誤的編解碼

本文首先嘗試對編碼秽誊、解碼進行一個宏觀、直觀的解讀畅形,然后詳細來解釋 python2 中的str和unicode养距,并對常見的UnicodeEncodeError 和 UnicodeDecodeError 異常進行剖析。

如何理解編日熬、解碼棍厌?

如何去理解編碼、解碼竖席?舉個例子耘纱,Alice同學剛加入了機器學習這門課,想給同班的Bob同學打個招呼毕荐。但是作為人束析,Alice不能通過意念和Bob交流,必須通過某種方式憎亚,比如手語员寇、聲音、文字等來表達自己的想法第美。如果Alice選擇用文字蝶锋,那么他可能會寫下這么一段文字:My name is: boot …… 來學機器學習嘍,寫文字這個過程其實就是編碼什往,經(jīng)過編碼后的文字才能給Bob看扳缕。Bob收到Alice的文字后,就會用自己對文字的認知來解讀Alice傳達的含義,這個過程其實就是解碼躯舔。當然驴剔,如果Bob不懂中文,那么就無法理解Alice的最后一句了粥庄,如果Bob不識字丧失,就完全不知道Alice想表達什么了。

上面的例子只是為了方便我們理解編碼飒赃、解碼這個抽象的概念利花,現(xiàn)在來看看對于計算機程序來說,如何去理解字符的編碼载佳、解碼過程。我們知道絕大多數(shù)程序都是讀取數(shù)據(jù)臀栈,做一些操作蔫慧,然后輸出數(shù)據(jù)。比如當我們打開一個文本文件時权薯,就會從硬盤讀取文件中的數(shù)據(jù)姑躲,接著我們輸入了新的數(shù)據(jù),點擊保存后盟蚣,文本程序會將更新后的內容輸出到硬盤黍析。程序讀取數(shù)據(jù)就相當于Bob讀文字,必須進行一個解碼的過程屎开,解碼后的數(shù)據(jù)才能讓我們進行各種操作阐枣。同理,保存到硬盤時奄抽,也需要對數(shù)據(jù)進行編碼蔼两。

下圖方框 A 代表一個輸出數(shù)據(jù)的程序,方框 B 代表一個讀取數(shù)據(jù)的程序逞度。當然這里的程序只是一個概念额划,表示一個處理數(shù)據(jù)的邏輯單元,可以是一個進程档泽、一個函數(shù)甚至一個語句等俊戳。A 和 B 也可以是同一個程序,先解碼外部獲取的數(shù)據(jù)馆匿,內部操作后抑胎,再進行某種編碼。

圖2. 編碼甜熔、解碼的過程
圖2. 編碼圆恤、解碼的過程

值得注意的是,有的編碼方案不一定能表示某些信息,這時編碼就會失敗盆昙,比如 ASCII 就不能用來表示中文羽历。當然,如果以錯誤的方式去解讀某段內容淡喜,解碼也會失敗秕磷,比如用 ASCII 來解讀包含 UTF-8的信息。至于什么是 ASCII炼团,UTF-8等澎嚣,在人機交互之字符編碼 中有詳細的說明,這里不再贅述瘟芝。下面結合具體的例子易桃,來看看編碼、解碼的細節(jié)問題锌俱。

python2.x 中的字符串

在程序設計中晤郑,字符串一般是指一連串的字符,比如hello world!贸宏、你好或者もしもし(日語)等等造寝。各種語言對于字符串的支持各不相同,Python 2 中字符串的設計頗不合理吭练,導致新手經(jīng)常會出現(xiàn)各種問題诫龙,類似下面的提示信息相信很多人都遇到過(UnicodeEncodeError 或者 UnicodeDecodeError):

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

下面我們一起來解決這個疑難雜癥。首先需要搞清楚python中的兩個類型:<type 'str'><type 'unicode'>鲫咽,文檔中關于這兩個類型的說明其實挺含糊的:

There are seven sequence types: strings, Unicode strings, lists, ...

String literals are written in single or double quotes: 'xyzzy', "frobozz". Unicode strings are much like strings, but are specified in the syntax using a preceding 'u' character: u'abc', u"def".

上面并沒有給出什么有用的信息签赃,不過好在這篇文章講的特別好,簡單來說:

  • str:是字節(jié)串(container for bytes)浑侥,由 Unicode 經(jīng)過編碼(encode)后的字節(jié)組成的姊舵。
  • unicode:真正意義上的字符串妄荔,其中的每個字符用 Unicode 中對應的 Code Point 表示痕貌。

翻譯成人話就是,unicode 有點類似于前面 Alice 打招呼傳遞的想法枪萄,而 str 則是寫下來的文字(或者是說出來的聲音伶选,甚至可以是手語)史飞。我們可以用 GBK,UTF-8 等編碼方案將 Unicode 類型轉換為 str 類型仰税,類似于用語言构资、文字或者手語來表達想法。

repr 與終端交互

為了徹底理解字符編碼陨簇、解碼吐绵,下面要用 python 交互界面進行一些小實驗來加深我們的理解(下面所有的交互代碼均在 Linux 平臺下)。在這之前,我們先來看下面交互代碼:

>>> demo = 'Test 試試'
>>> demo
'Test \xe8\xaf\x95\xe8\xaf\x95'

當我們只輸入標識符 demo 時己单,終端返回了 demo 的內容唉窃。這里返回的內容是怎么得到呢?答案是通過 repr() 函數(shù) 獲得纹笼。文檔中對于 repr 函數(shù)解釋如下:

Return a string containing a printable representation of an object.

所以纹份,我們可以在源文件中用下面的代碼,來獲取和上面終端一樣的輸出廷痘。

#! /usr/bin/env python
# -*- coding: UTF-8 -*-
demo = 'Test 試試'
print repr(demo)
# 'Test \xe8\xaf\x95\xe8\xaf\x95'

對于字符串來說蔓涧,repr() 的返回值很好地說明了其在python內部的表示方式。通過 repr 的返回值笋额,我們可以真切體會到前面提到的兩點:

  • str:實際上是字節(jié)串
  • unicode:真正意義上的字符串

下面分別來看看這兩個類型元暴。

unicode 類型

unicode 是真正意義上的字符串,為了理解這句話兄猩,先看下面的一段代碼:

>>> unicode_str = u'Welcome to 廣州' # ''前面的 u 表示這是一個 unicode 字符串
>>> unicode_str, type(unicode_str)  # repr(unicode_str)
(u'Welcome to \u5e7f\u5dde', <type 'unicode'>)

repr 返回的 Welcome to \u5e7f\u5dde 說明了unicode_str存儲的內容昨寞,其中兩個\u后面的數(shù)字分別對應了廣、州在unicode中的code point:

  • 5e7f 對應字厦滤;
  • 5dde 對應字;

英文字母也有對應的code point歼狼,它的值等于ASCII值掏导,不過repr并沒有直接輸出。我們可以在站長工具中查看所有字符對應的code point羽峰。也可以用 python 的內置函數(shù) ord 查看字符的 code point趟咆,如下所示(調用了 format 將code point轉換為十六進制):

>>> '{:04x}'.format(ord(u'廣'))
'5e7f'
>>> '{:04x}'.format(ord(u'W'))
'0057'

總結一下,我們可以將 <type 'unicode'> 看作是一系列字符組成的數(shù)組梅屉,數(shù)組的每一項是一個code point值纱,用來表示相應位置的字符。所以對于 unicode 來說坯汤,其長度等于它包含的字符(a 都是一個字符)的數(shù)目虐唠。

>>> len(unicode_str)
13
>>> unicode_str[0], unicode_str[12], unicode_str[-1]
(u'W', u'\u5dde', u'\u5dde')

str 類型

str 是字節(jié)串(container for bytes),為了理解這句話惰聂,先來看下面的一段代碼:

>>> str_str = 'Welcome to 廣州'       # 這是一個 str
>>> str_str, type(str_str)
('Welcome to \xe5\xb9\xbf\xe5\xb7\x9e', <type 'str'>)

python中 \xhh(h為16進制數(shù)字)表示一個字節(jié)疆偿,輸出中的\xe5\xb9\xbf\xe5\xb7\x9e 就是所謂的字節(jié)串,它對應了廣州搓幌。實際上 str_str 中的英文字母也是保存為字節(jié)串的杆故,不過 repr 并沒有以 \x 的形式返回。為了驗證上面輸出內容確實是字節(jié)串溉愁,我們用python提供的 bytearray 函數(shù)將相同內容的 unicode字符串用 UTF-8 編碼為字節(jié)數(shù)組处铛,如下所示:

>>> unicode_str = u'Welcome to 廣州'
>>> bytearray(unicode_str, 'UTF-8')
bytearray(b'Welcome to \xe5\xb9\xbf\xe5\xb7\x9e')
>>> list(bytearray(unicode_str, 'UTF-8')) 
# 字節(jié)數(shù)組,每一項為一個字節(jié);
[87, 101, 108, 99, 111, 109, 101, 32, 116, 111, 32, 229, 185, 191, 229, 183, 158]
>>> print r"\x" + r"\x".join(["%02x" % c for c in list(bytearray(unicode_str, 'UTF-8'))])
# 轉換為 \xhh 的形式
\x57\x65\x6c\x63\x6f\x6d\x65\x20\x74\x6f\x20\xe5\xb9\xbf\xe5\xb7\x9e

可見撤蟆,上面的 str_str 是 unicode_str 經(jīng)過 UTF-8 編碼 后的字節(jié)串奕塑。這里透漏了一個十分重要的信息,str類型隱含有某種編碼方式枫疆,正是這種隱式編碼(implicit encoding)的存在導致了許多問題的出現(xiàn)(后面詳細說明)爵川。值得注意的是,str類型字節(jié)串的隱式編碼不一定都是'UTF-8'息楔,前面示例程序都是在 OS X 平臺下的終端寝贡,所以隱式編碼是 UTF-8。對于 Windows 而言值依,如果語言設置為簡體中文圃泡,那么交互界面輸出如下:

# Win 平臺下,系統(tǒng)語言為簡體中文
>>> str_str = 'Welcome to 廣州'   
>>> str_str, type(str_str)
('Welcome to \xb9\xe3\xd6\xdd', <type 'str'>)

這里str_str的隱式編碼是cp936愿险,可以用 bytearray(unicode_str, 'cp936') 來驗證這點颇蜡。終端下,str類型的隱式編碼由系統(tǒng) locale 決定辆亏,可以采用下面方式查看:

# Unix or Linux
>>> import locale
>>> locale.getdefaultlocale()       
('zh_CN', 'UTF-8')
...
# 簡體中文 Windows
>>> locale.getdefaultlocale()       
('zh_CN', 'cp936')

總結一下风秤,我們可以將 <type 'str'> 看作是unicode字符串經(jīng)過某種編碼后的字節(jié)組成的數(shù)組。數(shù)組的每一項是一個字節(jié)扮叨,用 \xhh 來表示缤弦。所以對于 str 字符串來說,其長度等于編碼后字節(jié)的長度彻磁。

>>> len(str_str)
17
>>> str_str[0], str_str[-1]
('W', '\x9e')       # 實際上是('\x57', '\x9e') 

類型轉換

Python 2.x 中為上面兩種類型的字符串都提供了 encode 和 decode 方法碍沐,原型如下:

str.decode([encoding[, errors]])
str.encode([encoding[, errors]])

利用上面的兩個函數(shù),可以實現(xiàn) str 和 unicode 類型之間的相互轉換衷蜓,如下圖所示:

圖3. 類型間相互轉換
圖3. 類型間相互轉換

上圖中綠色線段標示的即為我們常用的轉換方法累提,紅色標示的轉換在 python 2.x 中是合法的,不過沒有什么意義磁浇,通常會拋出錯誤(可以參見 What is the difference between encode/decode?)斋陪。下面是兩種類型之間的轉換示例:

# decode: <type 'str'> 到 <type 'unicode'>的轉換
>>> enc = str_str.decode('utf-8')
>>> enc, type(enc)
(u'Welcome to \u5e7f\u5dde', <type 'unicode'>)

# encode: <type 'unicode'> 到 <type 'str'> 的轉換
>>> dec = unicode_str.encode('utf-8')
>>> dec, type(dec)
('Welcome to \xe5\xb9\xbf\xe5\xb7\x9e', <type 'str'>)

上面代碼中通過encode將unicode類型編碼為str類型,通過 decode 將str類型解碼為unicode類型扯夭。當然鳍贾,編碼、解碼的過程并不總是一帆風順的交洗,通常會出現(xiàn)各種錯誤骑科。

編、解碼錯誤

Python 中經(jīng)常會遇到 UnicodeEncodeError 和 UnicodeDecodeError构拳,怎么產(chǎn)生的呢咆爽? 如下代碼所示:

>>> u'Hello 廣州'.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-7: ordinal not in range(128)

>>> 'Hello 廣州'.decode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 6: ordinal not in range(128)

當我們用 ascii 去編碼帶有中文的unicode字符串時梁棠,發(fā)生了UnicodeEncodeError,當我們用 ascii 去解碼有中文的str字節(jié)串時斗埂,發(fā)生了UnicodeDecodeError符糊。我們知道,ascii 只包含 127 個字符呛凶,根本無法表示中文男娄。所以,讓 ascii 來編碼漾稀、解碼中文模闲,就超出了其能力范圍。這就像你對一個不懂中文的老外說中文崭捍,他根本沒法聽懂尸折。簡單來說,所有的編碼殷蛇、解碼錯誤都是由于所選的編碼实夹、解碼方式無法表示某些字符造成的

有時候我們就是想用 ascii 去編碼一段夾雜中文的str字節(jié)串粒梦,并不希望拋出異常亮航。那么可以通過 errors 參數(shù)來指定當無法編碼某個字符時的處理方式,常用的處理方式有 "strict"匀们,"ignore"和"replace"塞赂。改動后的程序如下:

>>> u'Hello 廣州'.encode('ascii', 'replace')
'Hello ??'
>>> u'Hello 廣州'.encode('ascii', 'ignore')
'Hello '

隱藏的解碼

str和unicode類型都可以用來表示字符串,為了方便它們之間進行操作昼蛀,python并不要求在操作之前統(tǒng)一類型,所以下面的代碼是合法的圆存,并且能得到正確的輸出:

>>> new_str = u'Welcome to ' + 'GuangZhou'
>>> new_str, type(new_str)
(u'Welcome to GuangZhou', <type 'unicode'>)

因為str類型是隱含有某種編碼方式的字節(jié)碼叼旋,所以python內部將其解碼為unicode后,再和unicode類型進行 + 操作沦辙,最后返回的結果也是unicode類型夫植。

第2步的解碼過程是在幕后悄悄發(fā)生的,默認采用ascii來進行解碼油讯,可以通過 sys.getdefaultencoding() 來獲取默認編碼方式详民。Python 之所以采用 ascii,是因為 ascii 是最早的編碼方式陌兑,是許多編碼方式的子集沈跨。

不過正是這個不可見的解碼過程,有時候會導致出乎意料的解碼錯誤兔综,考慮下面的代碼:

>>> u'Welcome to' + '廣州'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)

上面在字符串的+操作時饿凛,python 偷偷對'廣州'用 ascii 做解碼操作狞玛,所以拋出了UnicodeDecodeError異常。其實上面操作等同于 u'Welcome to' + '廣州'.decode('ascii') 涧窒,你會發(fā)現(xiàn)這句代碼拋出的異常和上面的一模一樣心肪。

隱藏的編碼

Python 不只偷偷地用 ascii 來解碼str類型的字節(jié)串,有時還會偷偷用ascii來編碼unicode類型纠吴。如果函數(shù)或類等對象接收的是 str 類型的字符串硬鞍,但傳進去的是unicode,python2 就會使用 ascii 將其編碼成str類型再做運算戴已。

以raw_input為例固该,我們可以給 raw_input 函數(shù)提供 prompt 參數(shù),作為輸入提示內容恭陡。這里如果 prompt 是 unicode 類型蹬音,python會先用ascii對其進行編碼,所以下面代碼會拋出UnicodeEncodeError異常:

>>> a = raw_input(u'請輸入內容: ')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-4: ordinal not in range(128)

上面操作完全等同于 a = raw_input(u'請輸入內容: '.encode('ascii'))休玩,你會發(fā)現(xiàn)它們拋出的異常完全一樣著淆。此外,如果嘗試將unicode字符串重定向輸出到文本中拴疤,也可能會拋出UnicodeEncodeError異常永部。

$ cat a.py
demo = u'Test 試試'
print demo
$ python a.py > output
Traceback (most recent call last):
  File "a.py", line 5, in <module>
    print demo
UnicodeEncodeError: 'ascii' codec can't encode characters in position 5-6: ordinal not in range(128)

當然,如果直接在終端進行輸出呐矾,則不會拋出異常苔埋。因為python會使用控制臺的默認編碼,而不是 ascii蜒犯。

總結

總結下本文的內容:

  • str可以看作是unicode字符串經(jīng)過某種編碼后的字節(jié)組成的數(shù)組
  • unicode是真正意義上的字符串
  • 通過 encode 可以將unicode類型編碼為str類型
  • 通過 decode 可以將str類型解碼為unicode類型
  • python 會隱式地進行編碼组橄、解碼,默認采用 ascii
  • 所有的編碼罚随、解碼錯誤都是由于所選的編碼玉工、解碼方式無法表示某些字符造成的

如果你明白了上面每句話的含義,那么應該能解決大部分編淘菩、解碼引起的問題了遵班。當然,本篇文章其實并不能幫你完全避免python編碼中的坑(坑太多)潮改。還有許多問題在這里并沒有說明:

  • 讀取狭郑、寫入文件時的編碼問題:
  • 數(shù)據(jù)庫的讀寫
  • 網(wǎng)絡數(shù)據(jù)操作
  • 源文件編碼格式的指定

有空再詳細談談上面列出的坑。

更多閱讀

Pragmatic Unicode
Unicode In Python, Completely Demystified
Solving Unicode Problems in Python 2.7
Unicode HOWTO
Wiki:PrintFails
Unicode and Character Sets
What is the purpose of __str__ and __repr__ in Python?
What does a leading \x mean in a Python string \xaa

Python: 熟悉又陌生的字符編碼
PYTHON-進階-編碼處理小結
五分鐘戰(zhàn)勝 Python 字符編碼
python 字符編碼與解碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末汇在,一起剝皮案震驚了整個濱河市翰萨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌糕殉,老刑警劉巖缨历,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件以蕴,死亡現(xiàn)場離奇詭異,居然都是意外死亡辛孵,警方通過查閱死者的電腦和手機丛肮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來魄缚,“玉大人宝与,你說我怎么就攤上這事∫逼ィ” “怎么了习劫?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嚼隘。 經(jīng)常有香客問我诽里,道長,這世上最難降的妖魔是什么飞蛹? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任谤狡,我火速辦了婚禮,結果婚禮上卧檐,老公的妹妹穿的比我還像新娘墓懂。我一直安慰自己,他們只是感情好霉囚,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布捕仔。 她就那樣靜靜地躺著,像睡著了一般盈罐。 火紅的嫁衣襯著肌膚如雪榜跌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天盅粪,我揣著相機與錄音斜做,去河邊找鬼。 笑死湾揽,一個胖子當著我的面吹牛,可吹牛的內容都是我干的笼吟。 我是一名探鬼主播库物,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贷帮!你這毒婦竟也來了戚揭?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤撵枢,失蹤者是張志新(化名)和其女友劉穎民晒,沒想到半個月后精居,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡潜必,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年靴姿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片磁滚。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡佛吓,死狀恐怖,靈堂內的尸體忽然破棺而出垂攘,到底是詐尸還是另有隱情维雇,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布晒他,位于F島的核電站吱型,受9級特大地震影響,放射性物質發(fā)生泄漏陨仅。R本人自食惡果不足惜津滞,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掂名。 院中可真熱鬧据沈,春花似錦、人聲如沸饺蔑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽猾警。三九已至孔祸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間发皿,已是汗流浹背崔慧。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留穴墅,地道東北人惶室。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像玄货,于是被迫代替她去往敵國和親皇钞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容