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

时间:2021-08-12 19:44:35

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

Python中文社区 ID:python-china

Unicode——万国码

 

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

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

  1. ls = 'abAB巩★☆' 
  2. 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码位对应的字符。

  1. >>> print([chr(i) for i in [123,957,24041]]) 
  2. ['{''ν''巩'

那么我们可以使用强大的Unicode进行编码了么?

  1. >>> ls = 'abAB巩★☆' 
  2. >>> ls.encode('Unicode'
  3. Traceback (most recent call last): 
  4.   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位,也就是四个字节来存储一个字符的编码方案。

  1. >>> 'aA巩'.encode('utf-32LE'
  2. b'a\x00\x00\x00A\x00\x00\x00\xe9\x5d\x00\x00' 

可见,所有的字符,都使用了四个字节来存储:每个字节除了Unicode码位之外,不足用\x00来填充。此法简单明了,Unicode码位不用转换,直接填充。但大量的\x00造成了极大的浪费。有没有办法解决这种浪费了?压缩下用两位行不行?

UTF-16 两字节为单位

 

当用UTF-16来编码时。

  1. >>> 'aA巩'.encode('utf-16LE'
  2. b'a\x00A\x00\xe9\x5d' 

两个字节对绝大多数Unicode码位来说是够用的,不够用的话系统自动用四位表示。这是系统实现,我们无需关心。UTF-16编码后的字节序列和字符,依然能够一一对应起来。UTF-16其实有两种编码方法,分别为上例的UTF-16LE和如下的UTF-16BE,测试:

  1. >>> 'aA巩'.encode('utf-16BE'
  2. b'\x00a\x00A\x5d\xe9' 

两者基本一样,只是高低字节位置发生了颠倒。LE和BE后缀,表示小字节序(little endian)和大字节序(big endian)。这是计算机内部关于字节的MSB(大权重字节)放在字节的开头还是结尾的具体实现细节。

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

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

UTF-8 变长字节编码

 

能不能用可变数目的字节来存储文本呢?如果存储的是英文文本的话,每个字符只用一个字节就可以;汉字的话,再进行扩展。如此来进一步节省存储空间。答案是可以的,这就是可变长度编码UTF-8。

  1. >>> 'aA巩'.encode('utf-8'
  2. 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三种编码后的长度

 

以上三种编码方式,由于压缩率不用,导致文件长度也不同,以下程序比较当文本为汉字和英语内容时,三种不同编码的长度:

  1. es = 'abcdefghij' 
  2. cs = '莫愁前路无知己,天下谁人不识君。' 
  3.  
  4. codes = ['utf-32le','utf-16le','utf-8'
  5.  
  6. print([len(es.encode(code)) for code in codes]) 
  7. 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使用一个字节直接存储,是效率、空间的平衡。 

原文地址:https://mp.weixin.qq.com/s/sN6EAhB1wPtVNWtVwHzjPg