字符编码简介

乱码,应该没有人没遇到过,可能是在打开网页的时候,可能是在打开文本文件的时候,也可能是在程序中处理字符串的时候。有的时候问题很容易解决,还有的时候则让人大伤脑筋。
在写程序的时候碰到过几次字符编码相关的问题后,才发现字符编码相关的概念应该是程序员必须牢固掌握的基础知识。字符编码相关的概念其实不复杂,但是往往上学的时候没有重视,加上在学校里面用到的时候也不多,所以大多数人可能都没弄清基本概念,只知道了几种码的名字,比如ascii,gbk等等。
下面简单介绍字符编码中的几个基本概念,具体例子主要是以unicode(也称为UCS,即Unicode Character Set)为例,一些概念的英文名称也来自Unicode标准。

为什么要需要编码

这也是很显然的。在最底层的硬件层面,计算机只能处理二进制的bit 或者 byte,要处理符号,需要将符号和这种二进制数建立一种对应关系,这就是编码。

字符集(字符库)

要设计一种编码,首先要确定这种编码是针对哪些字符的,毕竟,这个世界上,人们用于交流的符号种类实在太多了,一种编码不可能什么都编进来。这样一个字符的集合,就是该编码的字符集(repertoire of abstract characters)。

码空间和码点(codespace & code points)

编码就是用数字来表示一个符号,每个符号对应的这个数字(一般都是整数,好像没听说过不是整数的编码方式)就是这个符号的码点(code point), 所有码点组成的集合就是这种编码的码空间(codespace)。
例如unicode的码空间是从 0 to 10FFFF16, 一共有 1,114,112 码点。在unicode里面,表示一个字符和它对应的码点,是这种格式:

U+0061 latin small letter a

前缀U+,后面接16进制表示的码点,后面接对这个字符的描述。

编码形式(Encoding Forms)

如果一种编码仅仅是给字符集中的每个字符分配一个code point,那这种编码是完全无法实用的。因为计算机不能抽象的处理数字(这里说的是硬件层面,不是通过软件模拟出来的抽象能力)。一个数学上的整数,是一个抽象的概念,要让计算机处理(包括计算、存储和通过网络发送)一个整数,必须明确这个整数在计算机中如何表示,就是要精确到用几个字节来表示这个整数。这就产生了具体的编码形式。

编码单元(code unit)

所谓编码单元,就是一种编码形式中的最小编码单位,一个字符的编码的长度一定最小编码单元的整数倍。
比如UTF-16的编码单元是16bit,那么UTF-16中任何字符的编码肯定是16bit,32bit,48bit..., 而绝不会是8bit或者24bit。当然,实际由于32bit已经足够,所以UTF-16不会超过32bit。
计算机中最常见的三种基本数据单元是 8-bit, 16-bit 和 32-bit,而unicode也有三种编码形式分别以这三种数据单元作为 code unit,这三种编码分别叫做utf-8, utf-16, utf-32

UTF-32

对于utf-32来说,最简单了,因为32-bit能表示的整数范围已经完全容纳了unicode的codespace,所以直接将每个code point用32-bit的二进制整数表示就可以了。所有字符的编码都只需要一个code unit。 这种编码方法的缺点是显而易见的:太浪费存储空间和传输带宽了,其实我们常用的字符数量并不多,用两个字节都够了,英语国家的人用的字符就就更少了。

UTF-16

utf-16就有点麻烦了,一个code unit只有16位,如果想只用一个 code unit表示所有字符是不可能了。解决问题也很简单:对于那些在 U+0000..U+FFFF之间的码点,就用一个编码单元表示,对于 U+10000..U+10FFFF, 就用两个编码单元表示。
等等,好像有哪里不对,仔细一看,就能发现这么做显然是有问题的,那些用两个16-bit表示的code points, 它的每个16-bit unit又分别表示了一个code point。当计算机从磁盘中读取一个utf-16编码的文本文件,它读入一个16-bit的编码单元后是应该将这16-bit当作一个字符呢,还是将这16bit和其后16bit合在一起解释为一个字符呢?
所以,其实在 U+0000..U+FFFF之间,有2048个code points (U+D800–U+DFFF) 是不用于表示任何字符的,而是专门保留出来给UTF-16编码用的,这2048个code points 叫做 Surrogates Area,具体还可以分为 high-surrogate code points (U+D800..U+DBFF)以及 Low-Surrogate code points(U+DC00..U+DFFF)。而那些需要用两个16-bit来表示的code points, 其两个code unit 都是surrogate区域的数字。

BMP(Basic Multi-lingual Plane)

对于那些code points在 U+0000..U+FFFF 之间的字符,统称为Basic Multi-lingual Plane (BMP),实际上,这部分字符几乎包含了所有的常用字符,所以,用utf-16编码时,绝大部分时候用到的都是 1个16-bit的编码,比起utf-32,能省下一半空间。

UTF-8

像UTF-16一样,utf-8也是一种变长的码(就是不同字符的编码包含的编码单元的个数不是一致的,有的多,有的少),最短1个字节,最长4个字节,具体的规则此处就不详述了。
utf-8有几个好处:

  • 兼容ascii码,显然,utf-16和utf-32都做不到这一点
  • 节省空间,对于汉字来说可能不一定,但是对于使用英文的人来说,utf-8使用的空间基本就和ascii编码一样了。
  • 没有字节序问题,这一点在下一节说明。

BOM和字节序

在文本编辑器的格式菜单中,常常能看到这样一些字眼:BOM,Big Endian, Little Endian。这些都是用于表示编码的字节序的。
确定了编码形式后,在存储和传输一个编码时,发现还有一个问题,就是字节序问题。
如果一种编码的基本编码单元就是字节,那显然就啥问题没有了,因为计算机处理和传输的最小单位就是字节。例如ascii码,UTF-8就没有这种问题(一个utf8编码虽然可能有多个字节,但这几个字节的顺序是固定的)。
而utf-16和utf-32的基本编码单元都不是字节,所以当计算机存储一个基本编码单元时,属于该单元的两个或者4个字节应该以什么顺序存储就成了问题。(传输也有同样的问题)
Unicode规范中推荐的标记字节顺序的方法是BOM(Byte Order Mark)。在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前(或者文件的最开头),先传输字符"ZERO WIDTH NO-BREAK SPACE"。这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。 UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。

posted on 2017-08-19 10:47  等待未知  阅读(217)  评论(0编辑  收藏  举报

导航