博客 / 詳情

返回

編碼、亂碼、unicode 和 Emoji

概述

在各種日誌、tty 輸出中,我們總是能夠發現各種編碼不正確的字符。

�😸�  `\xef\xbf\xbd\xf0\x9f\x98\xb8\xef\xbf\xbd`
'\xe7\xb2\xbe\xe5\xa6\x99'
`<<"你好">>`

遇到這種情況,我們下意識地會產生三個想法:

  • 這是什麼(原本的內容應該是什麼)?
  • 從哪裏來的?
  • 為什麼會這樣?
  • 我該怎麼處理好?
    對於我個人的理解,亂碼只不過是「一種對於文本類數據的錯誤==解讀==或者==展示==」。

結論(造成的原因):

  1. 編碼不當 encoding issue。比如,使用 utf8 編碼的文本數據使用 gbk 解碼。
  2. 字體缺失 character missing in font。
  3. 文本數據被錯誤的截斷 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')
    '燙燙燙燙燙'

➋ 字體缺失

這個就更好理解了,你用的字體裏面沒有那個字符。大部分情況下顯示的是。
image.png

圖片來源

字符、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 的關係:
WeChatWorkScreenshot_d0827eb3-7348-45b1-ac66-39603aa98f53.png

另外,這個😂,它還有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 操作的支持。這一點對我的啓發也比較大。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.