Python 2.x 字符編碼終極指南

2021-02-20 Python開發者

(點擊上方公眾號,可快速關注)

來源:伯樂在線專欄作者 - selfboot 

如有好文章投稿,請點擊 → 這裡了解詳情

如需轉載,發送「轉載」二字查看說明

在人機互動之字符編碼 一文中對字符編碼進行了詳細的討論,並通過一些簡單的小程序驗證了我們對於字符編碼的認識。但僅了解這篇文章的內容,並不能幫我們在日常編程中躲過一些字符編碼相關的坑,Stackoverflow 上就有大量編碼相關的問題,比如 1,2,3。

本文首先嘗試對編碼、解碼進行一個宏觀、直觀的解讀,然後詳細來解釋 python2 中的str和unicode,並對常見的UnicodeEncodeError 和 UnicodeDecodeError 異常進行剖析。

如何理解編、解碼?

如何去理解編碼、解碼?舉個例子,Alice同學剛加入了機器學習這門課,想給同班的Bob同學打個招呼。但是作為人,Alice不能通過意念和Bob交流,必須通過某種方式,比如手語、聲音、文字等來表達自己的想法。如果Alice選擇用文字,那麼他可能會寫下這麼一段文字:My name is: boot …… 來學機器學習嘍,寫文字這個過程其實就是編碼,經過編碼後的文字才能給Bob看。Bob收到Alice的文字後,就會用自己對文字的認知來解讀Alice傳達的含義,這個過程其實就是解碼。當然,如果Bob不懂中文,那麼就無法理解Alice的最後一句了,如果Bob不識字,就完全不知道Alice想表達什麼了。

上面的例子只是為了方便我們理解編碼、解碼這個抽象的概念,現在來看看對於電腦程式來說,如何去理解字符的編碼、解碼過程。我們知道絕大多數程序都是讀取數據,做一些操作,然後輸出數據。比如當我們打開一個文本文件時,就會從硬碟讀取文件中的數據,接著我們輸入了新的數據,點擊保存後,文本程序會將更新後的內容輸出到硬碟。程序讀取數據就相當於Bob讀文字,必須進行一個解碼的過程,解碼後的數據才能讓我們進行各種操作。同理,保存到硬碟時,也需要對數據進行編碼。

下圖方框 A 代表一個輸出數據的程序,方框 B 代表一個讀取數據的程序。當然這裡的程序只是一個概念,表示一個處理數據的邏輯單元,可以是一個進程、一個函數甚至一個語句等。A 和 B 也可以是同一個程序,先解碼外部獲取的數據,內部操作後,再進行某種編碼。

值得注意的是,有的編碼方案不一定能表示某些信息,這時編碼就會失敗,比如 ASCII 就不能用來表示中文。當然,如果以錯誤的方式去解讀某段內容,解碼也會失敗,比如用 ASCII 來解讀包含 UTF-8的信息。至於什麼是 ASCII,UTF-8等,在人機互動之字符編碼 中有詳細的說明,這裡不再贅述。下面結合具體的例子,來看看編碼、解碼的細節問題。

python2.x 中的字符串

在程序設計中,字符串一般是指一連串的字符,比如hello world!、你好或者もしもし(日語)等等。各種語言對於字符串的支持各不相同,Python 2 中字符串的設計頗不合理,導致新手經常會出現各種問題,類似下面的提示信息相信很多人都遇到過(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」.

上面並沒有給出什麼有用的信息,不過好在這篇文章(https://www.azavea.com/blog/2014/03/24/solving-unicode-problems-in-python-2-7/)講的特別好,簡單來說:

str:是字節串(container for bytes),由 Unicode 經過編碼(encode)後的字節組成的。

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() 函數 獲得。文檔中對於 repr 函數解釋如下:

> 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 的返回值,我們可以真切體會到前面提到的兩點:

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後面的數字分別對應了廣、州在unicode中的code point:

>>> '{:04x}'.format(ord(u'廣'))

'5e7f'

>>> '{:04x}'.format(ord(u'W'))

'0057'

總結一下,我們可以將 <type 'unicode'> 看作是一系列字符組成的數組,數組的每一項是一個code point,用來表示相應位置的字符。所以對於 unicode 來說,其長度等於它包含的字符(a 和 廣 都是一個字符)的數目。

>>> len(unicode_str)

13

>>> unicode_str[0], unicode_str[12], unicode_str[-1]

(u'W', u'\u5dde', u'\u5dde')

str 類型

str 是字節串(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進位數字)表示一個字節,輸出中的\xe5\xb9\xbf\xe5\xb7\x9e 就是所謂的字節串,它對應了廣州。實際上 str_str 中的英文字母也是保存為字節串的,不過 repr 並沒有以 \x 的形式返回。為了驗證上面輸出內容確實是字節串,我們用python提供的 bytearray 函數將相同內容的 unicode字符串用 UTF-8 編碼為字節數組,如下所示:

>>> 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'))

# 字節數組,每一項為一個字節;

[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

可見,上面的 strstr 是 unicode_str 經過 UTF-8 編碼 後的字節串。這裡透漏了一個十分重要的信息,str類型隱含有某種編碼方式,正是這種隱式編碼(_implicit encoding)的存在導致了許多問題的出現(後面詳細說明)。值得注意的是,str類型字節串的隱式編碼不一定都是』UTF-8』,前面示例程序都是在 OS X 平臺下的終端,所以隱式編碼是 UTF-8。對於 Windows 而言,如果語言設置為簡體中文,那麼交互界面輸出如下:

# Win 平臺下,系統語言為簡體中文

>>> 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類型的隱式編碼由系統 locale 決定,可以採用下面方式查看:

# Unix or Linux

>>> import locale

>>> locale.getdefaultlocale()

('zh_CN', 'UTF-8')

...

# 簡體中文 Windows

>>> locale.getdefaultlocale()

('zh_CN', 'cp936')

總結一下,我們可以將 <type 'str'> 看作是unicode字符串經過某種編碼後的字節組成的數組。數組的每一項是一個字節,用 \xhh 來表示。所以對於 str 字符串來說,其長度等於編碼後字節的長度。

>>> 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]])

利用上面的兩個函數,可以實現 str 和 unicode 類型之間的相互轉換,如下圖所示:

上圖中綠色線段標示的即為我們常用的轉換方法,紅色標示的轉換在 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類型。當然,編碼、解碼的過程並不總是一帆風順的,通常會出現各種錯誤。

編、解碼錯誤

Python 中經常會遇到 UnicodeEncodeError 和 UnicodeDecodeError,怎麼產生的呢? 如下代碼所示:

>>> 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字符串時,發生了UnicodeEncodeError,當我們用 ascii 去解碼有中文的str字節串時,發生了UnicodeDecodeError。我們知道,ascii 只包含 127 個字符,根本無法表示中文。所以,讓 ascii 來編碼、解碼中文,就超出了其能力範圍。這就像你對一個不懂中文的老外說中文,他根本沒法聽懂。簡單來說,所有的編碼、解碼錯誤都是由於所選的編碼、解碼方式無法表示某些字符造成的

有時候我們就是想用 ascii 去編碼一段夾雜中文的str字節串,並不希望拋出異常。那麼可以通過 errors 參數來指定當無法編碼某個字符時的處理方式,常用的處理方式有 「strict」,」ignore」和」replace」。改動後的程序如下:

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

'Hello ??'

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

'Hello '

隱藏的解碼

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

>>> new_str = u'Welcome to ' + 'GuangZhou'

>>> new_str, type(new_str)

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

因為str類型是隱含有某種編碼方式的字節碼,所以python內部將其解碼為unicode後,再和unicode類型進行 + 操作,最後返回的結果也是unicode類型。

第2步的解碼過程是在幕後悄悄發生的,默認採用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') ,你會發現這句代碼拋出的異常和上面的一模一樣。

隱藏的編碼

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

以raw_input為例,我們可以給 raw_input 函數提供 prompt 參數,作為輸入提示內容。這裡如果 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')),你會發現它們拋出的異常完全一樣。此外,如果嘗試將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字符串經過某種編碼後的字節組成的數組

unicode是真正意義上的字符串

通過 encode 可以將unicode類型編碼為str類型

通過 decode 可以將str類型解碼為unicode類型

python 會隱式地進行編碼、解碼,默認採用 ascii

所有的編碼、解碼錯誤都是由於所選的編碼、解碼方式無法表示某些字符造成的

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

讀取、寫入文件時的編碼問題:

資料庫的讀寫

網絡數據操作

源文件編碼格式的指定

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

更多閱讀

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-進階-編碼處理小結

五分鐘戰勝 Python 字符編碼

python 字符編碼與解碼

由於微信無法添加外部連結,可以點擊 閱讀原文 查看原文中連結

覺得本文對你有幫助?請分享給更多人

關注「Python開發者」

看更多技術乾貨

專欄作者簡介 ( 點擊 → 加入專欄作者 )

selfboot:熱愛計算機技術的學生...書中尋求心靈的平靜...selfboot,自啟動,只有自己能啟動自己所以不要寄希望於別人,自我蛻變展翅飛翔吧!

打賞支持作者寫出更多好文章,謝謝

相關焦點

  • python字符的編碼與解碼
    既然計算機是通過二進位的數字來識別不同字符的,那不同的字符該用多少個1和0,又該以什麼樣的順序來排列呢?為什麼要字符編碼這裡為了規範,就出現了ASCII編碼。十進位是逢十進一,十六進位是逢十六進一,二進位就是逢二進一,以此類推,x進位就是逢x進位。下面就來通過python中自帶的函數一起來驗證一下。理解:ord() 函數:以一個字符串作為參數,返回對應的 ASCII 數值。
  • 【Python基礎】(6.1)字符編碼
    3.2 字符編碼表的發展史 (了解)3.3 編碼與解碼4.1 文本編輯器nodpad++存取文本文件 ✦ ✦ ✦ ✦ ✦ ✦一 引入    字符串類型、文本文件的內容都是由字符組成的,但凡涉及到字符的存取,都需要考慮字符編碼的問題。
  • 給妹子講python-S01E08理清python中的字符編碼方法
    容器遍歷和列表解析式給妹子講python-S01E05字符串的基本用法給妹子講python-S01E06字符串用法進階給妹子講python-S01E07字符編碼歷史觀:從ASCII到Unicode【要點搶先看】1.python中編、解碼的本質是文本字符串和字節字符串的相互轉換
  • python入門教程06-01(python語法入門之字符編碼)
    >2.3 python解釋器執行文件的流程以python test.py為例,執行流程如下#階段1、啟動python解釋器,此時就相當於啟動了一個文本編輯器#階段2、python解釋器相當於文本編輯器,從硬碟上將test.py的內容讀入到內存中#階段3、python解釋器解釋執行剛剛讀入的內存的內容,開始識別python語法2.4 總結python
  • Python3 是如何解決棘手的字符編碼問題的?
    題圖:unsplash.comPython3 最重要的一項改進之一就是解決了 Python2 中字符串與字符編碼遺留下來的這個大坑。gt;bytesPython3 中,在字符引號前加『b』,明確表示這是一個 bytes 類型的對象,實際上它就是一組二進位字節序列組成的數據,bytes 類型可以是 ASCII範圍內的字符和其它十六進位形式的字符數據,但不能用中文等非ASCII字符表示。
  • 如何正確解決Python中的中文編碼問題?
    字符串在Python內部的表示是unicode編碼,因此,在做編碼轉換時,通常需要以unicode作為中間編碼,即先將其他編碼的字符串解碼(decode)成unicode,再從unicode編碼(encode)成另一種編碼。但是,Python 2.x的默認編碼格式是ASCII。
  • Python高效編程之88條軍規(1):編碼規範、字節序列與字符串
    不過這些編碼方式真的是最好的選擇嗎?本系列文章將為你揭秘88種在編寫Python代碼中的規則,這些規則將會讓你Python程序更加健壯,運行效率更高。Python的PEP 8是Python官方提供了關於如何格式化Python代碼的樣式指南。
  • Python安全編碼指南(一)
    在Python 2.6.x中報錯為OverflowError: long int too large to convert to int在Python 2.7.x, Python 3.1中報錯為OverflowError: Python int too large to convert to C long如果我們將其中的modified time設置為2^55
  • Python基礎:數據類型和變量&字符串和編碼
    比如下面的代碼:x = 10x = x + 2如果從數學上理解x = x + 2那無論如何是不成立的,在程序中,賦值語句先計算右側的表達式x + 2,得到結果12,再賦給變量x。由於x之前的值是10,重新賦值後,x的值變成12。最後,理解變量在計算機內存中的表示也非常重要。
  • python基礎學習—04字符串與編碼
    定義:將信息從一種形式轉換為另外一種形式的過程叫做編碼,即信息轉換過程舉例:信息加密解密、語言翻譯1.2  計算機編碼定義:將計算機可讀信息轉換為人類可讀形式的過程叫做計算機編碼。二、常見編碼2.1  ASCII碼中文名:美國信息交換標準碼英文名:American Standard Code for Information Interchange基本簡介:ASCII碼是最早的計算機編碼,主要用於表示英文字符、數字和一些標點符號下面是ASCII表:ASCII值控制字符
  • Python 編碼為什麼那麼蛋疼?
    字符 「p」 保存到硬碟就是一串二進位數據 01110000,佔用一個字節的長度編碼與解碼我們用編輯器打開的文本,看到的一個個字符,最終保存在磁碟的時候都是以二進位字節序列形式存起來的。那麼從字符到字節的轉換過程就叫做編碼(encode),反過來叫做解碼(decode),兩者是一個可逆的過程。編碼是為了存儲傳輸,解碼是為了方便顯示閱讀。
  • 給妹子講python-S01E07字符編碼歷史觀:從ASCII到Unicode
    作者:醬油哥/ 清華程序猿       微信公眾號: python數據科學家 知乎專欄: 《給妹子講python》
  • 令人頭疼的Python編碼問題
    或者是列印一串字符串,確是亂碼,搞人心態。別慌,本文將從編碼的前世今生講解,讓你對編碼有個深刻了解,以便後期對Python編碼問題進行分析和解決。字符編碼的前世今生大家都知道,電腦本身是不認識字符的,只認識0和1。
  • python自學 第二章 python語言基礎之語法特點(注釋、代碼縮進、編碼規範)
    文件、模塊、類或者函數等添加版權、功能等信息在python中,三引號(』』』...』』』)或者(」」」..」」」)是字符串定界符,如果三引號作為語句的一部分出現,就不是注釋,而是字符串,這一點要注意區分。
  • 你真的知道 Python的 字符串是什麼嗎?
    字符串是一種序列,這意味著它具備序列類型都支持的操作:# 以下的s、t皆表示序列,x表示元素x in s  # 若s包含x,返回True,否則返回Falsex not in s  # 若s包含x,返回False,否則返回Trues + t  # 連接兩個序列s * n  # s複製n次s[i]   # s的索引第i項s[
  • 科普:Python編碼的前世今生
    於是統一聯盟國際組織提出了Unicode編碼,Unicode的學名是「Universal Multiple-Octet Coded Character Set」,簡稱為UCS。Unicode有兩種格式:UCS-2和UCS-4。
  • 深入淺出 + 徹底理解 Python 編碼
    本文的目的是簡明扼要地說明python的編碼機制,並給出一些建議。問題1:問題在哪裡?問題是我們的靶子,心中沒有問題去學習就會抓不住重點。本文使用的編程環境是centos6.7,python2.7。問題2:Why?要搞清楚原因,我們不妨認真分析下這兩句話的執行流程:首先,我們通過鍵盤在python命令行解釋器中鍵入了 中國zg 並且給它加上了英文的雙引號,然後又賦值給了變量s,看起來很稀鬆平常是不是?其實裡面大有玄機。當我們通過鍵盤在程序中輸入字符時,我們是通過作業系統完成這個功能的。
  • Python 2 與 Python 3 的區別
    比如:a = range(10)out_file = open(「print_test_file.txt」, 『w』)for x in a:       print(x,sep=』 『, end = 「\n」, file=out_file) 編碼問題在py2中,編碼問題是個大問題
  • 我的 Python 編碼規範
    /usr/bin/env python# -*- coding: utf-8 -*-"""通常這裡是關於本文檔的說明(docstring),須以半角的句號、 問號或驚嘆號結尾!解釋器聲明編碼格式聲明模塊注釋或文檔字符串模塊導入常量和全局變量聲明頂級定義(函數或類定義)執行代碼編碼格式聲明通常,編碼格式聲明是必需的。
  • Python 2.x 與 Python 3.x 的區別
    由於 Python 3.x 源碼文件默認使用 uft-8 編碼,這就使得以下代碼是合法的:>>> 中國 = 'china'>>> print(中國)chinaPython 2.x>>> str = '我愛北京天安門'>>