字符编码基础

 

字符编码

 

 

1. 常见字符编码方法1

1.1 ASCII 码

在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有 0 和 1 两种状态,因此八个二进制位就可以组合出 256 种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示 256 种不同的状态,即 256 个符号,从 00000000 到 11111111。

上个世纪 60 年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。

ASCII 码一共规定了 128 个字符 (00000000 - 01111111,即 0 - 127) 的编码,比如空格”SPACE”是 32(二进制00100000),大写的字母 A 是 65(二进制01000001)。这 128 个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面 7 位,最前面的 1 位统一规定为 0。

ASCII码大致可以分作:

  • ASCII非打印控制字符,ASCII表上的数字0–31分配给了控制字符,用于控制像打印机等一些外围设备。
  • ASCII打印字符,数字 32–127 分配给了能在键盘上找到的字符,当您查看或打印文档时就会出现。

Tips:详细 ASCII 码表信息

ASCII 0-127

1.2 非 ASCII 编码

英语用 128 个符号编码就够了,但是用来表示其他语言,128 个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的 é 的编码为 130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多 256 个符号。

但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用 256 个符号的编码方式,代表的字母却不一样。比如,130 在法语编码中代表了 é,在希伯来语编码中却代表了字母 Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127 表示的符号是一样的,不一样的只是 128—255 的这一段。

一种扩展 ASCII 码

ASCII 128-255

至于亚洲国家的文字,使用的符号就更多了,汉字就多达 10 万左右。一个字节只能表示 256 种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256x256=65536 个符号。

1.3 Unicode

世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。

可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode(Universal Multiple-Octet Coded Character Set),简称为 UCS。

历史上存在两个试图独立设计 Unicode 的组织,即国际标准化组织(ISO)和一个软件制造商的协会(unicode.org)。ISO开发了 ISO 10646 项目,Unicode 协会开发了 Unicode 项目。

在 1991 年前后,双方都认识到世界不需要两个不兼容的字符集。于是它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从 Unicode2.0 开始,Unicode 项目采用了与 ISO 10646-1 相同的字库和字码。

目前两个项目仍都存在,并独立地公布各自的标准。Unicode 协会现在的最新版本是 2005 年的 Unicode 4.1.0。ISO 的最新标准是 10646-3:2003。

Unicode 当然是一个很大的集合,现在的规模可以容纳 100 多万个符号。每个符号的编码都不一样,比如,U+0639 表示阿拉伯字母 Ain,U+0041 表示英语的大写字母 A,U+4E25 表示汉字 “严”。

Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是 0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

1.4 UTF-8

UCS 规定了怎么用多个字节表示各种文字。怎样传输这些编码,是由 UTF (UCS Transformation Format) 规范规定的,常见的 UTF 规范包括 UTF-8、UTF-16、UTF-32。

UTF-8是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一

UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用 1~4 个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8的编码规则很简单,只有二条:

  1. 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 unicode 码。因此对于英语字母,UTF-8 编码和ASCII 码是相同的。
  2. 对于 n 字节的符号(n>1),第一个字节的前 n 位都设为 1,第 n+1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 unicode 码。
Unicode符号范围 (十六进制)UTF-8编码方式 (二进制)
0000 0000 - 0000 007F 0xxxxxxx
0000 0080 - 0000 07FF 110xxxxx 10xxxxxx
0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

例如“汉”字的 Unicode 编码是 6C49。6C49 在 0800-FFFF 之间,所以肯定要用 3 字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将 6C49 写成二进制是:0110 110001 001001, 用这个比特流依次代替模板中的 x,得到:11100110 10110001 10001001,即 E6 B1 89。


2. 大端机和小端机2

不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序。

最常见的有两种:

  • 小端机 LE(little-endian):将低序字节存储在起始地址(低位编址)

    最符合人的思维的字节序,地址低位存储值的低位,地址高位存储值的高位

    为什么说是最符合人的思维的字节序,是因为从人的第一观感来说,低位值小,就应该放在内存地址小的地方,也即内存地址低位;反之,高位值就应该放在内存地址大的地方,也即内存地址高位。

  • 大端机 BE(big-endian):将高序字节存储在起始地址(高位编址)

    最直观的字节序,地址低位存储值的高位,地址高位存储值的低位

    为什么说直观,不要考虑对应关系,只需要把内存地址从左到右按照由低到高的顺序写出,把值按照通常的高位到低位的顺序写出,两者对照,一个字节一个字节的填充进去。

    简单地说,低位字节存放数据的高位就是大端,低位就是小端。

    “endian” 这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。

例:如果我们将 0x1234abcd 写入到以 0x0000 开始的内存中,则结果为

地址big-endianlittle-endian
0x0000 0x12 0xcd
0x0001 0x23 0xab
0x0002 0xab 0x34
0x0003 0xcd 0x12

x86 系列的 CPU 都是 little-endian 的字节序。

那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode 规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(ZERO WIDTH NO-BREAK SPACE),用 FEFF 表示。这正好是两个字节,而且 FF 比 FE 大 1。

如果一个文本文件的头两个字节是 FE FF,就表示该文件采用大头方式;如果头两个字节是 FF FE,就表示该文件采用小头方式。


3. BMP 和 BOM3

3.1 UCS-2、UCS-4、BMP

UCS 有两种格式:UCS-2 和 UCS-4。顾名思义,UCS-2 就是用两个字节编码,UCS-4 就是用 4 个字节(实际上只用了 31 位,最高位必须为 0)编码。下面让我们做一些简单的数学游戏:

UCS-2 有 2^16 个码位,UCS-4 有 2^31 个码位。

UCS-4根据最高位为0的最高字节分成  128 个 group。每个 group 再根据次高字节分为 256 个 plane。每个 plane 根据第 3 个字节分为 256 行 (rows),每行包含 256 个 cells。当然同一行的cells 只是最后一个字节不同,其余都相同。

group 0 的 plane 0 被称作 Basic Multilingual Plane, 即 BMP。或者说 UCS-4 中,高两个字节为 0 的码位被称作 BMP

将 UCS-4 的 BMP 去掉前面的两个零字节就得到了 UCS-2。在 UCS-2 的两个字节前加上两个零字节,就得到了 UCS-4 的 BMP。而目前的 UCS-4 规范中还没有任何字符被分配在 BMP 之外。

3.2 UTF 的字节序和 BOM

UTF-8 以字节为编码单元,没有字节序的问题。UTF-16 以两个字节为编码单元,在解释一个 UTF-16 文本前,首先要弄清楚每个编码单元的字节序。例如收到一个 “奎” 的 Unicode 编码是 594E,“乙” 的 Unicode 编码是 4E59。如果我们收到 UTF-16 字节流 “594E”,那么这是 “奎” 还是 “乙”?

Unicode 规范中推荐的标记字节顺序的方法是 BOM。所谓 BOM,全称是 Byte Order Mark,它是一个Unicode 字符,通常出现在文本的开头,用来标识字节序(Big/Little Endian),除此以外还可以标识编码(UTF-8/16/32),如果出现在文本中间,则解释为 “zero width no-break space”,它的编码是 FEFF。而 FEFF 在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。

UTF-8 主要的优点是可以兼容 ASCII,但如果使用 BOM 的话,这个好处就荡然无存了,除此以外,BOM的存在还可能引发一些问题,比如下面错误便都有可能是 BOM 导致的:

  • Shell: No such file or directory
  • PHP: Warning: Cannot modify header information – headers already sent

4. 汉字编码简介(待补充…)3

为了处理汉字,程序员设计了用于简体中文的 GB2312 和用于繁体中文的 big5。

GB2312(1980年)一共收录了 7445 个字符,包括 6763 个汉字和 682 个其它符号。汉字区的内码范围高字节从 B0-F7,低字节从 A1-FE,占用的码位是 72*94=6768。其中有 5 个空位是 D7FA-D7FE。

GB2312 支持的汉字太少。1995 年的汉字扩展规范 GBK1.0 收录了 21886 个符号,它分为汉字区和图形符号区。汉字区包括 21003 个字符。2000 年的 GB18030 是取代 GBK1.0 的正式国家标准。该标准收录了 27484 个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的 PC 平台必须支持 GB18030,对嵌入式产品暂不作要求。所以手机、MP3 一般只支持 GB2312。

从 ASCII、GB2312、GBK 到 GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为 0。按照程序员的称呼,GB2312、GBK 到 GB18030 都属于双字节字符集 (DBCS)。


  1. 字符编码笔记:ASCII,Unicode和UTF-8

  2. 小端格式和大端格式(Little-Endian&Big-Endian)

  3. 程序员趣味读物:谈谈Unicode编码

posted @ 2015-09-30 09:59  m_CHaN  阅读(462)  评论(0编辑  收藏  举报