编码知识

摘录自http://newhtml.net/from-unicode-to-emoji/

ACII

ASCII基本上是所有程序员都能熟知的编码,这个编码本身的规则也比较简单:

  • ASCII使用一个字节来表示一个字符,一个字节一共有256种可能性,
  • 其中第0~31种可能性,用来表示各种控制符,例如换行符
  • 32~126用于表达可见字符,包括字母、数字、常用标点等
  • 第127种可能性是删除符,也属于一个控制符

在ASCII中,256种可能性只占用了128种,这128种可能性实际都位于1个字节的低7位中,最高位并没有内容。那么我们就可以在最高位做手脚,来识别一个字节究竟是ASCII,还是新编码2个字节中的第一个。
至于2个字节组合起来到底是什么字符,不同国家、地区以及公司,都有不同的安排,这个安排就是所谓的代码页了。因此,代码页不是一种编码,而是一系列编码所采用的方法。GBK这样的编码,就是在这个时代背景下诞生的。

下图是GBK编码对2个字节的使用情况,这张图来自于维基百科。


1.png

不过,代码页并没有完全解决编码问题。

首先,代码页逐渐成为了操作系统、文字软件中,语言设置的一部分,并不在文件或字符当中。这就是说,在日文操作系统下产生的含有日文文字的文件,传输到中文操作系统下打开,就会因为彼此系统的代码页不同,而无法正常显示,俗称乱码。不过这还不是最惨的。
其次,单单Windows就有一百余种代码页,IBM、HP等厂商也有自己规定的代码页。IBM大型机上甚至有一千多种代码页。这就使得代码页的自动推导,很难施行。机器无法仅仅根据内容来准确判断文件所使用的代码页。当然,这个问题在今天看来未必完全不可行,毕竟现在人人都说自己在搞机器学习。我不是这方面的专家,我和当时的程序员一样一筹莫展。

最致命的一点是,各个组织、标准、国家,无法在哪个字符如何表达上达成一致,同一个二进制序列所代表的意思,产生了很大的歧义。即使2个字节能够涵盖现今文化中的所有字符,但大家无法妥善规范,又有什么意义呢。

Unicode

Unicode是将一切字符编码到同一个编码体系的结果。
Unicode给每个字形一个独立的表示,叫做“码点”(code point)。为了方便表示,码点一般写作“U+xxxxx”,其中x为16进制表示的数字。例如U+54C8,就是“哈”。
用途或意思相近的码点被划分到不同的组当中,叫做“平面”(plane)。目前一共规定了16个平面,目前只使用到了其中少数几个。


2.png

截至Unicode 10.0,Unicode一共规定了136,690个不重复字符。

可以看出,目前我们使用到的绝大部分字符,都放在BMP当中。
这里面有一个非常眼熟的缩写:CJKV。程序员老鸟可能见过CJK,CJK是什么呢?CJK特指中国(China)、日本(Japan)、韩国(Korea)这三国的表意文字。那么CJKV是什么意思呢?我特地查了一下,原来越南曾经也使用过汉字,被称为喃字。Unicode后来也包含了喃字,因此有了CJKV这个缩写。


3.png

组合与拆分

Unicode中存在“组合字”的概念。使用者可以通过组合多个码点,“拼”出一个完整的字。

例如,Á 实际可以由U+0041(“A”)与U+0301 (“◌́” )组合而成。在不同的环境里,U+0301可能会展示成不同的样子,不过如果你的软件支持,它应该会很像是一个拼音二声的样子。
最厉害的是,这种组合拆分不一定发生在两个“半字”之间,还有可能更多个!

例如“ệ”这个越南字母,可以有多达5种组合方式:

U+1EC7 “ệ”
U+1EB9 “ẹ” + U+0302 “◌̂”
U+00EA “ê” + U+0323 “◌̣”
U+0065 “e” + U+0323 “◌̣” + U+0302 “◌̂”
U+0065 “e” + U+0302 “◌̂” + U+0323 “◌̣”

UTF一家子

上面的Unicode编码完成了给每个字符,甚至每个字形、字元一个编号的过程。但正如之前所说,编码的第二步,是还需要将这些编号能够序列化,否则这些编码只能停留于理论,无法进行传输和传播了。

不过在讲如何序列化Unicode之前,需要先稍微卖个关子,说一下字节序。

字节序是要解决一个问题:如果一次读取多个字节,组合成一个更大的数字时,哪部分在低位,哪部分在高位?

计算机世界里有两种主流字节序:大端序、小端序。大端序的意思是,先序列化的是大端,因此大端在前面传输;而小端序的意思是,先序列化的是小端,因此小端在前面传输。

例如,0x0A0B0C0D,这么长一个数字,在大端序和小端序的样子就是:

大端序:0A 0B 0C 0D
小端序:0D 0C 0B 0A

因为Unicode要使用不止一个字节来表示一个字符,字节序就成为了序列化时一个重要的考量点。

UTF-32

尽管UTF-8是大家最熟悉的Unicode序列化格式,但我想先说说UTF-32,这个编码是最为简单的。

UTF-32每个字符固定使用4个字节,因此理论上最多表达2564种字符。然而实际上UFT-32要求每个字符最高位必须为0,所以比2564稍微要少一些,不过仍然足以覆盖Unicode。

那么字节序怎么去考虑呢?

如果一个文件使用UTF-32存储,通常需要以U+FEFF来开头。如果程序读出的是0x00 0x00 0xFE 0xFF,则表示文件内容是小端序,反之则说明是大端序(0xFF 0xFE 0x00 0x00)。

这个特殊的四字节,被称为BOM。

UTF-16

UTF-16就比UTF-32要复杂一些了,不过大体上来说,除了构成“代理编码对”(surrogate pair)的情况以外,每个字符使用2个字节存储。而且和你想的一样,UTF-16也需要BOM,只不过BOM缩减到了2个字节。
那么什么是“代理编码对”呢?

最早UTF-16可以像UTF-32一样简单,只计划包含6万多个码点。令人意外的是,Unicode后来超过了这个数量,于是就有了代理编码对这样的技巧。

原理上来说,就是将一部分字符的表达,拆成了4个字节来表示。而为了不造成混淆,之前一些有效的2字节组合,也不再对应真实字符。

因此UTF-16的解码和编码,并不是简单地查表翻译,而存在一个“if”的情况。具体的算法并不复杂,大家可以自己搜索一下如何实现。维基百科上有一个非常不错的例子。

UTF-8

UTF-8可能是最常用的Unicode传输格式了,别看名字上是“8”,但实际只有ASCII涵盖的那些字符,才是真正1个字节。而Unicode里的其他字符,可能占用2个字节,也可能占用3个甚至4个字节。

UTF-8的算法比较精巧,在算法执行过程中,需要根据特定的情况来决定一个字符是否已经读完。而且算法当中也已经囊括了字节序相关的信息,因此UTF-8并不需要BOM。

由于兼容ASCII,UTF-8对于以英文为主的文本,最节省存储空间。这里特别注意下,UTF-16和UTF-32是不兼容ASCII的哦。

UTF-7

下面来说说这个异类。早期一些软件及协议对字符的限制很严格,导致使用了最高位的UTF-8在这些软件或协议中就会出现问题。UTF-7就是只使用低7位来进行编码。

具体的编码规则在这就不赘述了,我自己也没有深入去看,大体上来说,就是将一部分合法的ASCII字符用作转义,类似于base64。

不过值得一提的是,由于UTF-7中的转义规则,一些ASCII字符也可以被转义。例如“<”和“>”可以被转义为“+ADw-”“+AD4-”。这曾经导致过一些网站遭受UTF-7 XSS攻击,攻击者利用UTF-7编码逃过了敏感字符过滤,进而大摇大摆地向页面注入了自己的JS代码。关于这一点,感兴趣的各位可以自己搜索一下。

你可能感兴趣的:(编码知识)