字符编码那些事儿

你有没有被编码整过?

1 引子

如何将信息交给计算机处理呢?

首先要解决“交”的问题,即让计算机“拿到”这些信息,这个过程便是编码。

有两种很重要的信息:数字、字符。

对于数字来说,编码似乎要简单很多,因为数字天然会有一个二进制的表示。举个栗子,用1个字节存储“5”:5(10) = 101(2) => 0000 0101

但是,现实也并非那么美好。比如,我们有一个数字“-5”,或者数字“0.5”,或者数字“555555555”,会面临以下问题。

  • 如何编码这个数字?(编码方法)
  • 使用几个字节编码?(编码长度)
  • 会不会有一些数字编码不了?(编码范围)

字符编码,同样有这些问题。

2 字符编码

2.1 字符编码与数字编码

没有数字到二进制转换的天然性,字符编码的思路是先把字符映射到一个数字,然后再将数字映射到存储的二进制。

映射字符到数字的表格,被称作字符集(Character Set),微软称其为代码页(Code Page),又叫内码。字符集里的每一个字符都有一个编号,即映射到的数字,被称作码点(Code Point)。

数字是具有全球性的,大家都认,1就是1,2就是2。但是,字符中的大部分文字却没有这个特性。比如,拿汉字“诗”给老外看,他根本不知道也不关心这是啥意思。所以,就有了针对各种语言的字符集。

  • GB2312、GBK、GB18030 - 中文

  • BIG5 - 中文繁体

  • KS X 1001、EUC-KR、ISO-2022-KR - 韩文

  • JIS X 0208 - 日文

这里就有一个问题了,这些字符集会不会在表示各自文字的时候用到相同的码点?答案是肯定的,这也是乱码的一个原因。

  >>> c = '诗'
  >>> print c.decode('gbk')
  诗
  >>> print c.decode('big5')
  坅

2.2 ASCII编码

ASCII(American Standard Code for Information Interchange)编码是美国制定的一套字符编码方案,字符集中有0~127共计128个码点,每一个码点都可以使用7位二进制表示。编码方式非常直接,就是码点对应的二进制。

查看一个英文字符“a”对应的ASCII字符集的码点和ASCII编码:

>>> ord(u'a'.encode('ascii'))
97
>>> bin(ord(u'a'.encode('ascii')))
'0b1100001'

因为计算机是按字节分配内存的,尽管ASCII字符集的码点只需要7位,实际上要占用一个字节。所以,有2^8-2^7=128个字符空间是没有用到的。

后来,为了解决部分西欧语言的问题,有人想到利用这剩下的128来表示更多的字符,这些方法统称为EASCII(Extended ASCII),其中著名的有ISO 8859-1(又称作Latin 1)。

2.3 GB XXX编码

对于汉字来说,ASCII剩下的128个字符显然是不够用的。

这难不倒我们,GB2312率先出场。GB2312收录了共7445个图形字符,其中汉字占6763个。显然码点的个数已经超出了1个字节的表示范围,所以使用两个字节来编码一个汉字。

考虑到与ASCII的兼容,规定码点在0~127范围内的字符和ASCII相同,还是1个字节表示1个字符。

>>> ord(u'a'.encode('gb2312'))
97

规定两个码点大于127的字符连在一起时,表示一个汉字。首字节需要在0xA1到 0xF7之间,尾字节在0xA1到0xFE之间,这样可以组合出8000多种码点。

>>> print u'诗'.encode('gb2312')
'\xca\xab'

由于GB2312只收录6763个汉字,还有不少汉字并未有收录。于是微软基于GB2312扩展出GBK。GBK共收录21886个汉字和图形符号,其中汉字21003 个。GBK的首字节在0x81到0xFE 之间,尾字节在0x40到0xFE 之间,一共有2万多个码点。

机智的你有没有猜到,GB是Guo Biao(国标)的意思,K是Kuo Zhan(扩展)的意思...

再后来,GB18030推出,兼容GB2312,基本兼容GBK,共收录汉字70244个。GB18030采用了多字节编码方案,支持更大的编码空间。

3 UNICODE与UTF-XXX

3.1 UNICODE

ASCII作为一个开始,后面出现了各种字符集,各种编码方案。貌似,我们在自己的世界里玩得很好。

事实上,如果把字符集比作是一部字典,那现在的情况是,我的字典里未必有你,你的字典里未必有我。

如果能有一部大而全的字典,我们都用同一个,世界大同,再也不用担心乱码,多好!

于是,UNICODE来了。

UNICODE是一个世界级的字符集,全世界的每一个字符都有唯一的码点。码点具有这样的形式U+[XX]XXXX,其中,X是一个十六进制数字。UNICODE的范围目前是U+0000~U+10FFFF,超过100万。感觉好任性,还有哪个字符没收录进来~

因为码点太多了,为了方便管理,把每65536个码点归为一组,称为一个平面(Plane),共有17个平面。

第一个平面,即Plane 0,又叫做BMP(Basic Multilingual Plane,基本多语言平面),码点范围是U+0000~U+FFFF,日常用到的绝大多数字符都在这个平面。

>>> u'诗'
u'\u8bd7'

参考ASCII的编码方法,UNICODE的编码方法也可以如此直接,即直接存储码点对应的二进制。

额,因为UNICODE的码点最少也要4个字节,所以,意味着曾经在ASCII只需要1个字节的字符们要占用4个字节了,并且,这还和ASCII不兼容。

我们千辛万苦找到UNICODE,却又遇不兼容和占空间的问题,好忧伤啊。

但这也不能怪UNICODE,人家是字符集,只是查个码点,具体如何编码到计算机是不管的。

3.2 UTF-XXX

UTF(Unicode Transformation Format),即UNICODE的码点转换为最终的编码的格式。出于不同的目的,有多种UTF编码方法:UTF-8、UTF-16、UTF-32。

其中,UTF-32便是直接将码点转换为4字节二进制的方法。

考虑到大多数人都在BMP(U+0000~U+FFFF)里玩,只需要使用两个字节编码即可。万一用到了BMP以外的字符,再使用4字节。这便是UTF-16。

由于UTF-16和UTF-32使用多个字节来存储一个数字(码点),所以都需要考虑字节序的问题。

UTF-8不同于UTF-16、UTF-32,UTF-8是一种变长字节的编码方式,无需考虑字节序。对于汉字“诗”,其UTF-8编码计算方式如下。

对于字符“a”,对应的UNICODE码点、UTF-XXX编码为:

CHAR   : a
UNICODE: u+0061
UTF-8  : 0x61
UTF-16 : 0x0061
UTF-32 : 0x00000061

对于字符“诗”,对应的UNICODE码点、UTF-XXX编码为:

CHAR   : 诗
UNICODE: u+8bd7
UTF-8  : 0xe8af97
UTF-16 : 0x8bd7
UTF-32 : 0x00008bd7

世界还是很美好的,尽管还有一波GBXXX的接口在跑着,不支持4字节UTF-8的MySQL在跑着,老的NodeJS在跑着...

这种感觉就像住在老破小里,看着宏伟的城市蓝图。

不对,是租住(逃

参考