关于emoji:编码乱码unicode-和-Emoji

92次阅读

共计 4408 个字符,预计需要花费 12 分钟才能阅读完成。

概述

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

正文完
 0