徹底解決Python編碼問題

oaksharks發表於2020-04-25

1. 基本概念

  • 字符集(Character set)

    • 解釋:文字和符合的總稱
    • 常見字符集:
      • Unicode字符集
      • ASCII字符集(Unicode子集)
      • GB2312字符集
  • 編碼方法(Encoding)

    • 解釋:將字元對應到位元組的方法,部分字符集和編碼方法名稱一樣。
    • 常見編碼方法:
      • UTF-8:可對Unicode字元進行編碼
      • GB2312
      • ASCII
  • 編碼(Encode)

    • 解釋:將字符集中字元按照一定規則轉換成位元組
  • 解碼(Decode)

    • 解釋:與編碼相反,將位元組轉換為字符集中的字元
  • 字符集與編碼方法的關係

    • 每個字符集都有對應的編碼方法
    • 一種字符集可能有多種編碼方法
      • 不同的編碼方法得到的位元組不同,佔用儲存空間也不一樣
      • 例如Unicode字元可以使用UTF-8/ASCII/GBK等方法編碼
    • Unicode字符集包含世界上大部分字元,很多其他字符集有的字元它都有,是他們的超集
    • 大部分字符集可以理解為Unicode的子集
    • 實際上,除了Unicode之外所謂的字符集主要是對Unicode部分字元編碼而已(定義編碼方式)
    • 一種編碼不必支援Unicode的所有字元(通常把它能支援的那部分稱為它的字符集)

2. 關於編碼的錯誤和解決方法

在開發過程中,我們所接觸的字符集大多都是Unicode,大部分報錯都是關於編碼和解碼的。

2.1. 編碼錯誤UnicodeEncodeError

2.1.1. 錯誤分析

導致該錯誤的原因通常是編碼方法支援的Unicode字元不全;在工作中,你寫了一個txt中文文件,想用ascii編碼去儲存這個檔案,就會報這種錯誤。

錯誤復現:

我們知道ascii不支援字元,那我們用ascii編碼方法對Unicode進行編碼:

# -*- encoding: utf-8 -*- 
u"中".encode(encoding='ascii')

報錯如下:

UnicodeEncodeError: 'ascii' codec can't encode character '\u4e2d' in position 0: ordinal not in range(128)

這是一個UnicodeEncodeError 型別的錯誤,提示無法使用指定的編碼方法對字元進行編碼,報錯提示中可以得到3個資訊:

  • 當前使用的是acsii編碼方法
  • 被編碼的字元是'\u4e2d'
  • ascii編碼方法能支援的字元有128個

有時候我們還可以利用這個提示檢視編碼方法支援的字元個數:

# -*- encoding: utf-8 -*- 
u"中".encode(encoding='iso-8859-1')

報錯:

UnicodeEncodeError: 'latin-1' codec can't encode character '\u4e2d' in position 0: ordinal not in range(256)

通過報錯提示可以看出iso-8859-1能編256個字元。

接著,我們來看下用支援中文的utf-8 編碼方法進行編碼能得到什麼結果:

# -*- encoding: utf-8 -*-
s = u"中".encode(encoding='utf-8')
print("s: ", s)
print("s == 中?" , s == '中')
print("type of s: ", type(s))
print("str==bytes? ", bytes == type(s))

輸出:

('s: ', '\xe4\xb8\xad')
('s == \xe4\xb8\xad?', True)
('type of s: ', <type 'str'>)
('str==bytes? ', True)

從輸出的結果可以得到:

- 編碼得到的物件跟我們直接定義的字串是一樣的,都是str
- `str`就是`bytes`(python中)

2.1.2. 解決方法

UnicodeEncodeError 是說編碼方法支援的字元不全,而UTF-8編碼就能很好地對Unicode編碼,所以只要把編碼方法指定為utf-8就可以了。

在python2中:

如果你呼叫encode方法但沒有指定encoding引數,那很可能使用了系統預設的引數,就像:

# -*- encoding: utf-8 -*-
import sys
print "default encoding is %s ." % sys.getdefaultencoding()
u'中'.encode()

輸出:

default encoding is ascii .
UnicodeEncodeError: 'ascii' codec can't encode character u'\u4e2d' in position 0: ordinal not in range(128)

可以手動指定encoding引數,也可以修改python預設編碼方法:

# -*- encoding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')  # 必須在reload之後呼叫

print u'中'.encode()

在python3中:

在python3中你很難看到UnicodeEncodeError了,因為python3的預設編碼就是utf-8,而Unicode字元都可以用utf-8編碼方法編碼。

2.2. 編碼錯誤UnicodeDecodeError

2.2.1. 錯誤分析

導致該錯誤的原因是使用了錯誤的解碼方式把位元組資料還原成字元。例如在工作中,有一個utf-8生成中文文件,我們選擇用ascii編碼解碼,就會報這個錯。

錯誤復現:

我們知道python中字串和位元組是一樣,我們可以定義一箇中文字串,通過ascii來解碼生成Unicode,復現這個錯誤:

# -*- encoding: utf-8 -*-
print '中'.decode(encoding='ascii')

輸出:

UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

注意,這是一個UnicodeDecodeError錯誤,區別於編碼錯誤UnicodeEncodeError,它提示無法把位元組0xe4(實際上對應三位元組,0xe4是第一個) 解碼成Unicode。0xe4轉換為10進位制是228,已經超過127了,所以ascii無能力了。

2.2.2. 解決方法

我們需要知道生成位元組的編碼方式才能進行還原,就好像對一個檔案進行了加密,必須得知道加密的密碼才能把檔案還原,而解碼方式(或者稱為編碼)就是那個密碼。 所以不管是python2還是python3直接去修改預設編碼為UTF-8不一定能解決問題,具體的方法有兩種:

  1. 通過原始碼找到解碼失敗的位元組是使用那種編碼生成的
  2. 對報錯的位元組資料使用各種常見的編碼進行解碼,觀察哪一種是正確的

以一個例子來說明為什麼直接設定預設編碼為UTF-8不能有效解決UnicodeDecodeError問題

python檔案頭指定了編碼為,在宣告字串時候將會使用指定的編碼轉換為位元組:

# -*- encoding: GBK -*-
s = "張"
print ("s is ", s)

s_unicode = u'張'
print ("encoding with GBK is ", s_unicode.encode("GBK"))
print ("encoding with UTF-8 is ", s_unicode.encode("UTF-8"))

輸出:

('s is ', '\xd5\xc5')
('encoding with GBK is ', '\xd5\xc5')
('encoding with UTF-8 is ', '\xe5\xbc\xa0')

在檔案頭中指定了GBK編碼後預設情況下字元就會被編碼為\xd5\xc5,這與我們手動用GBK編碼得到結果一致,而使用utf-8編碼得到的是3個位元組的資料\xe5\xbc\xa0(使用了更多的儲存空間)。

例子開始了,在python2中將一個dict轉換成json:

# -*- encoding: GBK -*-
import json
d = {'name': "張", 'sex': u'man'}
print (json.dumps(d))

輸出:

UnicodeDecodeError: 'utf8' codec can't decode byte 0xd5 in position 0: invalid continuation byte

錯誤說無法使用utf-8解碼0xd5,0xd5也就是GBK中的張,我們知道這個位元組是用GBK生成的,這個時候可以設定json.dumps的encoding引數解決:

# -*- encoding: GBK -*-
import json
d = {'name': "張", 'sex': u'man'}
print (json.dumps(d, encoding='gbk'))

修改一下程式碼,繼續使用我們熟悉的utf-8編碼來執行:

# -*- encoding: utf-8 -*-
import json
d = {'name': "張", 'sex': u'man'}
print (json.dumps(d, encoding='utf-8'))

輸出:

{"name": "\u5f20", "sex": "man"}

發現name是unicode,使用ensure_ascii=False,不強制轉換成ascii:

# -*- encoding: utf-8 -*-
import json
d = {'name': "張", 'sex': u'man'}
print (json.dumps(d, encoding='utf-8', ensure_ascii=False))

輸出:

  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.py", line 251, in dumps
    sort_keys=sort_keys, **kw).encode(obj)
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.py", line 210, in encode
    return ''.join(chunks)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 1: ordinal not in range(128)

納尼,檔案頭也是utf-8,也指定了utf-8怎麼還是報錯?錯誤提示用ascii去解碼0xe5位元組,在上面的程式碼輸出中可以知道0xe5是utf-8對字元編碼的第一個位元組 ,報錯的原因是用ascii去解析utf-8生成的位元組了。我們並沒有設定哪個地方使用ascii解碼,應該是系統預設的編碼,嘗試設定系統預設編碼再執行:

# -*- encoding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

import json
d = {'name': "張", 'sex': u'man'}
print (json.dumps(d, encoding='utf-8', ensure_ascii=False))

輸出:

{"name": "張", "sex": "man"}

3. 總結

3.1. python2和python3對字串的處理區別

  • Python2
    • 預設編碼是ascii
  • Python3
    • 預設編碼為utf-8
    • 不能使用import sys;sys.setdefaultencoding('utf-8')設定預設編碼
    • Unicode和str都用字串表示了

3.2. 為什麼不全用UTF-8編碼?

UTF-8包含的字元更多,佔用的記憶體和磁碟空間也更大,比如對漢字,utf-8是3個位元組,gbk是2個位元組。

3.3. 如何快速解決UnicodeEncodeError錯誤?

python3中基本不會出現,python2中嘗試設定預設編碼為utf-8。

3.4. 快速解決UnicodeDecodeError?

需要知道出錯的位元組是使用哪種編碼方式生成的,然後嘗試把預設編碼設定成這種。

相關文章