概述
在各種日誌、tty 輸出中,我們總是能夠發現各種編碼不正確的字符。
�😸� `\xef\xbf\xbd\xf0\x9f\x98\xb8\xef\xbf\xbd`
'\xe7\xb2\xbe\xe5\xa6\x99'
`<<"ä½ å¥½">>`
遇到這種情況,我們下意識地會產生三個想法:
- 這是什麼(原本的內容應該是什麼)?
- 從哪裏來的?
- 為什麼會這樣?
- 我該怎麼處理好?
對於我個人的理解,亂碼只不過是「一種對於文本類數據的錯誤==解讀==或者==展示==」。
結論(造成的原因):
- 編碼不當 encoding issue。比如,使用 utf8 編碼的文本數據使用 gbk 解碼。
- 字體缺失 character missing in font。
- 文本數據被錯誤的截斷 data was not properly splited。在網絡傳輸或者儲存的時候被程序不恰當的處理了。
接下來,分享一下本人對於這些相關的問題整理的信息。
準備工作
我們以 Python3 為例,先學習一些簡單且有必要的相關處理手段。
Python3 中用來處理字符的數據類型有以下:
| represent | type | element type | length |
|---|---|---|---|
'精妙' |
<class 'str'> |
<class 'str'> |
2 |
b'\xe7\xb2\xbe\xe5\xa6\x99' |
<class 'bytes'> |
<class 'int'> |
6 |
這個地方需要注意,'str' 中的每一個元素(element),py3 可不僅僅是range 256。請看:
Python2:
Python 2.7.18
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: chr() arg not in range(256)
Python3:
Python 3.9.1
>>> chr(0x70ce)
'烎'
可以很明顯的看到,b'\xe7\xb2\xbe\xe5\xa6\x99' 這個長度為6的 bytes 就是精妙這兩個漢字的 utf8 編碼後二進制數據。它等價於bytes([0xe7, 0xb2, 0xbe, 0xe5, 0xa6, 0x99]])。
轉換 bytes <-> str
>>> bs = '精妙'.encode('utf8') # str to bytes/binary
>>> type(bs), len(bs), type(bs[0])
(<class 'bytes'>, 6, <class 'int'>)
>>> bs2 = bytes('精妙', 'utf8') # alternative way to convert
>>> bs2
b'\xe7\xb2\xbe\xe5\xa6\x99'
>>> origin_s = bs.decode('utf8') # bytes to str
>>> origin_s, type(origin_s), len(origin_s), type(origin_s[0])
('精妙', <class 'str'>, 2, <class 'str'>)
有多種構造二進制的方法
cons_byte = bytes([231, 178, 190, 229, 166, 153])
cons_byte2 = b'\xe7\xb2\xbe\xe5\xa6\x99'
>>> cons_byte, cons_byte2
(b'\xe7\xb2\xbe\xe5\xa6\x99', b'\xe7\xb2\xbe\xe5\xa6\x99')
請留意,當我們拿到一塊二進制數據的時候。即便知道他是字符串編碼成的數據,在不清楚編碼方式的情況下,我們是沒有辦法直接還原原始的字符數據的。
這種時候,如果大家都約定內存中的 string 用 unicode,二進制都用 utf8 編碼,那就會非常方便。拿到一個 binary 直接進行 decode utf8 即可。
如果我們不知道未知的 bytes 數據編碼類型,那麼可以嘗試用 chardet 來分析:
>>> chardet.detect(b'\xe7\xb2\xbe\xe5\xa6\xfe')
{'encoding': 'ISO-8859-1', 'confidence': 0.73, 'language': ''}
>>> chardet.detect(b'\xe7\xb2\xbe\xe5\xa6\x99')
{'encoding': 'utf-8', 'confidence': 0.7525, 'language': ''}
有時候,bytes 數據出現了一些問題(IO error 或者程序 bug),雖然我們知道它是怎麼編碼的,但是 decode 的時候仍然會出錯。此時可以嘗試設置一下 decode 函數的 errors 參數來碰碰運氣:
這裏我們把 xe5\xa6\x99 改成 xe5\xa6\==xfe== 把原始的二進制數據改成一個不合法的 utf8 編碼的 bytes。
>>> b'\xe7\xb2\xbe\xe5\xa6\xfe'.decode('utf8', errors='strict')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 3-4: invalid continuation byte
>>> b'\xe7\xb2\xbe\xe5\xa6\xfe'.decode('utf8', errors='ignore')
'精'
可以看到decode 函數很努力地把前三個 bytes 對應的漢字正確的解析出來了。實際是非常不推薦大家在程序中這麼寫的,畢竟找到問題才是正道(而不是掩蓋過去)。
亂碼的幾種形態
➊ 編碼不當。
我們嘗試對於 utf 編碼的「你好棒棒噠」,分別使用 gbk 和 ASC II 方式來解析:
>>> r = '你好棒棒噠'.encode('utf8')
>>> r
b'\xe4\xbd\xa0\xe5\xa5\xbd\xe6\xa3\x92\xe6\xa3\x92\xe5\x93\x92'
>>> r.decode('gbk', errors='ignore')
'浣犲ソ媯掓掑搾'
>>> ''.join([chr(c) for c in r])
'ä½\xa0好æ£\x92æ£\x92å\x93\x92'
看看這個 浣犲ソ媯掓掑搾 和 ä½\xa0好æ£\x92æ£\x92å\x93\x92,是不是有那股味了?
➊附➀ 上古時代的==錕斤拷==和==燙燙燙==
大概15年前,有過寫win32程序的朋友大概都有一些印象。我們也可以嘗試復現一下:
-
錕斤拷似乎是由 unicode 的
0xFFFD引發的:>>> [chr(0xFFFD)]*10 ['�', '�', '�', '�', '�', '�', '�', '�', '�', '�'] >>> ''.join([chr(0xFFFD)]*10).encode('utf8') b'\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd' >>> ''.join([chr(0xFFFD)]*10).encode('utf8').decode('gbk') '錕斤拷錕斤拷錕斤拷錕斤拷錕斤拷' -
同樣的,把一塊二進制每個 byte 寫為某個默認值(0xCC),再亂解碼就有了==燙燙燙==:
>>> bytes([0xCD]*10).decode('gbk', errors='ignore') '屯屯屯屯屯' >>> bytes([0xCC]*10).decode('gbk', errors='ignore') '燙燙燙燙燙'
➋ 字體缺失
這個就更好理解了,你用的字體裏面沒有那個字符。大部分情況下顯示的是。
圖片來源
字符、unicode、Emoji 和編碼。
為了澄清亂碼的概念,我們有必要先搞清楚是計算機系統中的「字符」。
首先要定義字符的意義,我們先不定義它,舉一些例子出來:
- 漢語中的一個漢字(例如
字)是==一個==字符,這種觀念肯定是深入人心的。 - ASCII 中的可見字符(例如
A)是一個字符。 - 😂 請注意,這不是一個圖片。
- 有一些不可見的控制字符也是字符。
我們先來研究一下那個流行的「笑哭臉」符號:
>>> s = '😂'
>>> s
'😂'
>>> s.encode('utf8')
b'\xf0\x9f\x98\x82'
>>> [hex(b) for b in s.encode('utf8')]
['0xf0', '0x9f', '0x98', '0x82']
>>> bytes([0xf0, 0x9f, 0x98, 0x82]).decode('utf8')
'😂'
>>> chr(0x1f602)
'😂'
>>> ord('😂')
128514
>>> hex(ord('😂'))
'0x1f602'
實際上,這哭臉符號是一個 unicode「字符(character)」。
- 它是一個字符,再次強調。
- 它的解釋是:"face with tears of joy"。
- 它的 unicode 編號是
U+1F602。 - 對應的 utf-8 編碼是
0xf0 0x9f 0x98 0x82,一共4個字節。 - 它處於 unicode 的 Emoticons 塊(範圍 U+1F600 - U+1F64F),也就是我們通常所説的
Emoji。
如何構造一個 emoji 的 unicode 字符呢?我們可以有多種方式構造這個字符,比如
- 通過 unicode 編號
chr(0x1F602) - 通過二進制數據解碼
bytes([0xf0, 0x9f, 0x98, 0x82]).decode('utf8')。
同時,我們還了解到了在 Python3 中 bytes / str / unicode 的關係:
另外,這個😂,它還有2個變種,具體可以參考 unicode 相關的 wikipedia 頁面。
有趣的記錄
-
大小寫的困惑
>>> 'BAfflE' 'BAfflE' >>> 'BAfflE'.upper() 'BAFFLE' >>> 'BAfflE'.upper() == 'BAFFLE' True >>> 'BAfflE' == ''BAfflE'.upper() lower() False >>> len('BAfflE') 4 >>> len('BAfflE'.upper()) 6
參考:
- Wikipedia Mojibake
- 一篇有意思的博文
- Python3裏面的説明
第二篇參考中,作者的觀點主要是:
Indeed, an array of unicode characters performs better on these tests than many of the specialized string classes.
作者發現,在當時的 Python 庫中,對於 unicode 的,字符串的「取長」、「逆轉」、「截取」、「轉換大小寫」、「遍歷」等操作,在當時的 string 類型中都不能夠很好的處理。
因此他希望能夠有一個直接的,對 unicode 進行類似 list 操作的支持。這一點對我的啓發也比較大。