关于emoji:编码乱码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')
    '烫烫烫烫烫'

➋ 字体缺失

这个就更好了解了,你用的字体外面没有那个字符。大部分状况下显示的是。

图片起源

字符、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 操作的反对。这一点对我的启发也比拟大。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理