用 Python 理清编码问题:Unicode万国码

Python中文社区

共 3718字,需浏览 8分钟

 ·

2021-04-14 21:55

Unicode——万国码

为解决语言各自为政的编码,人们提出了Unicode编码方案,这个方案简单粗暴:把世界上所有语言字符统一编码。Unicode的两种方案UCS-2和UCS-4,可使用空间分别达到2^16和2^32个:外星人到访地球之前,应该是够用的。

我们看几个字符的Unicode编码码位(code point)是怎样的:

ls = 'abAB巩★☆'
print([ord(l) for l in ls])

结果:[97, 98, 65, 66, 24041, 9733, 9734]。可见,字母abAB的Unicode码位和其ASCII码位一致,所以字符为字母时两者兼容,而汉字巩的码位为24041(0x5de9),与之前的GB系列编码47534(0xb9ae)不同,所以Unicode和GB系列编码之间是不完全兼容的:只有ASCII部分兼容。

所有国家的人都使用Unicode编码之后,扩展、乱码问题都不复存在:所有人类语言字符都有了一个统一的编码码位,沟通中我们写出的每个数字编码,都有唯一的字符与他对应。Python中chr函数返回Unicode码位对应的字符。

>>> print([chr(i) for i in [123,957,24041]])
['{''ν''巩']
那么我们可以使用强大的Unicode进行编码了么?
>>> ls = 'abAB巩★☆'
>>> ls.encode('Unicode')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module> LookupError: unknown encoding: Unicode
未知编码Unicode!这是因为,并不存在Unicode码这种编码形式,Unicode只是一个码位表,它只是建立了字符和整数之间的映射。至于整数码位(code point)如何存储成字节,先存高位低位,有没有特殊标志,Unicode并不直接决定,而是交给具体编码来考虑这些细节:UTF-32,UTF-16和UTF-8。

UTF-32 四字节为单位

UTF-32,顾名思义,是用32位,也就是四个字节来存储一个字符的编码方案。
>>> 'aA巩'.encode('utf-32LE')
b'a\x00\x00\x00A\x00\x00\x00\xe9\x5d\x00\x00'
可见,所有的字符,都使用了四个字节来存储:每个字节除了Unicode码位之外,不足用\x00来填充。此法简单明了,Unicode码位不用转换,直接填充。但大量的\x00造成了极大的浪费。
有没有办法解决这种浪费了?压缩下用两位行不行?

UTF-16 两字节为单位

当用UTF-16来编码时。
>>> 'aA巩'.encode('utf-16LE')
b'a\x00A\x00\xe9\x5d'
两个字节对绝大多数Unicode码位来说是够用的,不够用的话系统自动用四位表示。这是系统实现,我们无需关心。UTF-16编码后的字节序列和字符,依然能够一一对应起来。UTF-16其实有两种编码方法,分别为上例的UTF-16LE和如下的UTF-16BE,测试:
>>> 'aA巩'.encode('utf-16BE')
b'\x00a\x00A\x5d\xe9'
两者基本一样,只是高低字节位置发生了颠倒。LE和BE后缀,表示小字节序(little endian)和大字节序(big endian)。这是计算机内部关于字节的MSB(大权重字节)放在字节的开头还是结尾的具体实现细节。

《格列佛游记》中,小人国国民为吃鸡蛋先吃大头或小头,针锋相对,组成了两个军事对立集团big endians和little endians,相互间多次发动战争。

那么两个字节就是Unicode编码的极限了么?

UTF-8 变长字节编码

能不能用可变数目的字节来存储文本呢?如果存储的是英文文本的话,每个字符只用一个字节就可以;汉字的话,再进行扩展。如此来进一步节省存储空间。答案是可以的,这就是可变长度编码UTF-8。
>>> 'aA巩'.encode('utf-8')
b'aA\xe5\xb7\xa9'
这是目前最短的字节序列,因为aA分别存储成了一个字节。
需要注意的是,UTF-32和UTF-16中,巩的字节序列是0x5de9,但在UTF-8中,字节序列变成了0xe5b7a9。这说明UTF-8编码不是简单地把Unicode码位直接存储进字节序列中,而是进行了某些转换。这些转换,保证了英文用一位存储,汉语等较大字符多字节存储。
那么是如何转换的呢?

UTF-8 编码转换规则

本部分过于细节,可略过。
UTF-8实现了可变长度的编码,为解码时区分可变长度究竟多长,需要在字节序列里使用特殊模板。UTF-8编码遵循以下规则:
  • 0x00-0x7F之间的码位,兼容ASCII码,单字节直接存储在以下模板 0*** ****
  • 0x80-0x7ff之间,使用两个字节存储,字节模板是110* **** 10** ****
  • 0x800-0xffff之间,使用三个字节存储,字节模板是1110 **** 10** **** 10** ****
  • 0x10000-0x1fffff之间,使用四个字节存储,字节模板是1111 0*** 10** **** 10** **** 10** ****
以汉字巩为例,其Unicode码位为0x6c49,二进制位110 1100 0100 1001。位于第三行范围,所以需要三个字节来存储,写出模板,1110 **** 10** **** 10** ****,使用二进制,从右向左填充,不足部分补零,可得结果1110 0110 1011 0001 1000 1001,十六进制为0xe6 0xb7 0x89,所以巩编码为UTF-8的字节序列形式为0Xe6b789。
让我们从UTF-8编码转换细节中,回到UTF三种编码的长度问题上来。

UTF三种编码后的长度

以上三种编码方式,由于压缩率不用,导致文件长度也不同,以下程序比较当文本为汉字和英语内容时,三种不同编码的长度:
es = 'abcdefghij'
cs = '莫愁前路无知己,天下谁人不识君。'

codes = ['utf-32le','utf-16le','utf-8']

print([len(es.encode(code)) for code in codes])
print([len(cs.encode(code)) for code in codes])
输出为 [40, 20, 10] [64, 32, 48] 可见,对于英文来说,UTF-8比UTF-16和UTF-32编码都要有优势;对汉字来说,最有优势的反而是UTF16编码。这是因为UTF-16编码中,大部分汉字采用2Byte存储,而UTF-8中汉字需要三个字节存储。
在日常生活中,因为考虑到最大兼容性,UTF-8使用的最为广泛。
至此,我们从ASCII码到GB系列编码,再到Unicode和相应的UTF系列编码,一路进化,拥有了一个包罗万码,不会乱码和有较高压缩率的字符编码系统。
可以使用了么?没有!因为我们只是编码了文本自身,并没有记载具体用了那个编码:当我们发送一份文件后,除非告诉对方,否则对方不知道应该该用什么编码打开它。
解决这个问题,我们留待下篇文章分析。

总结

  • Unicode统一了世界各语言字符。Unicode几种编码形式中;
  • UTF-32简单,但浪费严重。
  • UTF-16使用两个字节为单位存储,节省了空间。
  • UTF-8使用一个字节直接存储,是效率、空间的平衡。


作者:巩庆奎,大奎,对计算机、电子信息工程感兴趣。gongqingkui at 126.com

赞 赏 作 者



更多阅读



2020 年最佳流行 Python 库 Top 10


2020 Python中文社区热门文章 Top 10


5分钟快速掌握 Python 定时任务框架

特别推荐




点击下方阅读原文加入社区会员

浏览 62
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报