概述
在各种日志、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 操作的反对。这一点对我的启发也比拟大。