首页 > 代码库 > 精述字符编码(读这篇绝对不够)

精述字符编码(读这篇绝对不够)

带你了解ASCII,Latin1,ANSI,Unicode,UCS-2,UCS-4,UTF-8,UTF-16,UTF-32,GB2312,GB13000,GBK,GB18030,BIG5,BOM,BMP,Littile Endian,Big Endian,内码,外码,CodePage。

1.导语

字符编码(Character Encoding)是计算机显示文本的基础,是每一位IT从业者必知的计算机基础知识点,如同数值在计算中如何存储表示,那么基础,那么重要。然因字符编码历史久远,变更频繁,地域差别,参考文献内容不全,质量参差不齐等原因,让不少读者望而却步,坚持刨根究底的读者,最终也难免云里雾里,不知所以然。鉴于此,本文将尝试带领大家弄清楚字符编码相关术语的概念,各自间的联系和区别,不足之处,请读者批评指正,不甚感激。

关于字符编码的介绍,网上已经有很多前人留下了值得参考的文章,这里推荐几篇,建议在阅读本篇博文前,请大家研读以下几篇文章,阅读顺序不作要求。
(1)字符编码笔记:ASCII,Unicode和UTF-8
(2)各种编码UNICODE、UTF-8、ANSI、ASCII、GB2312、GBK详解
(3)闲谈字符和字符集以及编码(上)
(4)闲谈字符和字符集以及编码(下)
(5)字节那些事儿

2.ASCII

我们知道计算机存储数据都是以二进制形式存储的,以字节(Byte)为最小存储单位,以比特(Bit)为最小状态。每一个Bit取值为0或1两种状态,每一个字节有8个Bit位,也就是一个字节可以表示256种状态。那计算机是如何存储和识别0和1这两种状态的呢?计算机中0和1分别由低电平(低电压)和高电平(高电压)表示,实现的硬件基础就是晶体二极管,原理就是利用了晶体二极管的单向导电性。

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。上个世纪60年代,由美国制定的一套字符编码,将英语字符与二进制位之间做了统一规定。主要用于显示现代英语和其他西欧语言。它是现今最通用的单字节编码系统。它已被国际标准化组织(ISO)定为国际标准,称为ISO/IEC 646。

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

3. Latin1

Latin1相对ASCII略显尴尬,因为说到字符编码,ASCII总会被频繁提及,而Latin1却恰恰相反,很少有人说到它。其实Latin1的使用还是比较广泛的,比如MySQL的数据表存储默认编码就是Latin1。

Latin1是国际标准编码ISO-8859-1的别名。Latin1也是单字节编码,在ASCII编码的基础上,利用了ASCII未利用的最高位,扩充了128个字符,因此Latin1可以表示256个字符,并向下兼容ASCII。Latin1收录的字符除ASCII收录的字符外,还包括西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。欧元符号出现的比较晚,没有被收录在ISO-8859-1当中,在后来的修订版ISO-8859-15加入了欧元符号。Latin1的编码范围是0x00-0xFF,ASCII的编码范围是0x00-0x7F。

4.ANSI

说到ANSI,大家也许会认为是美国国家标准委员会(American National Standards Institute),的确没错,但是本文讨论的是字符编码,此处的ANSI是指字符编码。ANSI编码与ANSI委员会的关系,我暂时尚未知晓。

事实上,ANSI编码是Windows操作系统为了显示不同国家语言采用的一种编码方式。因为每个国家和地区为了表示自己的文字字符,各自制定了不同的编码标准,由此产生了GB2312、GBK、GB18030、Big5、Shift_JIS 等各自的编码标准。ANSI编码不是单一明确的字符编码,而是根据当前系统的语言环境采用相应的编码方式,比如自Windows98以来的简体中文系统,采用的编码就是GBK,相应的,在繁体中文Windows操作系统中,ANSI编码代表Big5;在日文Windows操作系统中,ANSI 编码代表 Shift_JIS 编码。

ANSI编码最常见的应用就是在Windows当中的记事本程序中,当新建一个记事本,默认的保存编码格式就是ANSI。不同 ANSI编码之间互不兼容,当信息在国际间交流时,就时常会出现令人头痛的乱码问题。

5.Unicode与BMP

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

但是对于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。单字节编码方案最多只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256x256=65536个符号。这里不详细展开,后面会具体讨论GB2312。每个国家或地区都有自己的一套编码方案,于是当信息在国际间间流是就会出现乱码问题,好比世界上每个国家都有自己的语言,相互交流时就会出现障碍。于是,就需要一个国际语言,让每个国家和地区的人之间可以正常的交流,对于计算机也是同样的道理,需要一个统一的字符编码方案,让每一台电脑都能正确的识别字符。铺垫了那么多,就是想说明一个叫Unicode的字符编码横空出世的必要性和意义。

Unicode俗称万国码,是由统一码联盟在1991年首次发布,请注意,并非由ISO发布。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以跨语言环境来呈现和处理文字。需要注意的是,Unicode虽然称为万国码,但是目前也不能涵盖世界上所有的文字字符,因为Unicode自发布以来,至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为2016年6月21日公布的Unicode9.0.0,已经收入超过十万个字符(我们华夏民族的字符也不止十万个啊,Uinicode仍需努力啊)。

Unicode的编码方式。
Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符. Unicode的编码空间可以划分为17个平面(plane),每个平面包含216<script type="math/tex" id="MathJax-Element-1">2^{16}</script>(65,536)个码位。17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从0016<script type="math/tex" id="MathJax-Element-2">00_{16}</script>到1016<script type="math/tex" id="MathJax-Element-3">10_{16}</script>,共计17个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从U+D800到U+DFFF之间的码位区块是永久保留不映射到Unicode字符。实际使用中,目前只用了少数平面内编码的字符。

平面 始末字符值 中文名称 英文名称
0号平面 U+0000 - U+FFFF 基本多文种平面 Basic Multilingual Plane,简称BMP
1号平面 U+10000 - U+1FFFF 多文种补充平面 Supplementary Multilingual Plane,简称SMP
2号平面 U+20000 - U+2FFFF 表意文字补充平面 Supplementary Ideographic Plane,简称SIP
3号平面 U+30000 - U+3FFFF 表意文字第三平面(未正式使用[1]) Tertiary Ideographic Plane,简称TIP
4号平面~13号平面 U+40000 - U+DFFFF (尚未使用)
14号平面 U+E0000 - U+EFFFF 特别用途补充平面 Supplementary Special-purpose Plane,简称SSP
15号平面 U+F0000 - U+FFFFF 保留作为私人使用区(A区) Private Use Area-A,简称PUA-A
16号平面 U+100000 - U+10FFFF 保留作为私人使用区(B区) Private Use Area-B,简称PUA-B


其中,中国由GB2312编码表示的常用的6763个汉字就被收录在Unicode的0号平面内U+4E00-U+9FFF码值之间,该区间的码值也包含也很多非常用的中文汉字,共收录了2W多个汉字。Unicode对各国语言文字的编码情况具体可参见维基百科Unicode字符平面映射。

6.UCS与UCS-2和UCS-4

说到字符编码,大家肯定听过UCS-2和UCS-4,在说完Unicode,好学的大家肯定心存疑惑,UCS-2和UCS-4和Unicode之间的关系和区别到底是什么?我曾经也为此痛苦不已,但是下面我将努力尝试捋清楚UCS-2与Unicode之间千丝万缕的关系,为大家答疑解惑。

首先说一下什么是UCS。
UCS(Universal Character Set,通用字符集)是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。UCS又被称为Universal Multiple-Octet Coded Character Set,中国大陆译为通用多八位编码字符集,台湾译为广用多八比特编码字元集。

说到UCS,不得不说一下UCS和Unicode的关系。历史上存在两个独立的尝试创立单一字符集的组织,即国际标准化组织(ISO)于1984年创建的ISO/IEC JTC1/SC2/WG2(英文全称:International Organization for Standardization / International Electrotechnical Commission, Joint Technical Committee#1/Subcommittee#2/Working Group#2)和由Xerox、Apple等软件制造商于1988年组成的统一码联盟。前者开发的ISO/IEC 10646(UCS)项目,后者开发的统一码(Unicode)项目。因此最初制定了不同的标准。1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。1991年,不包含CJK统一汉字集的Unicode 1.0发布。随后,CJK统一汉字集的制定于1993年完成,发布了ISO 10646-1:1993,即Unicode 1.1。从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码。ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。两个项目仍都独立存在,并独立地公布各自的标准。但统一码联盟和ISO/IEC JTC1/SC2都同意保持两者标准的码表兼容,并紧密地共同调整任何未来的扩展。也就是说,我们可以简单的理解Unicode和UCS是两个不同机构发布的对全球文字字符进行统一编码的相同方案,更为简单粗暴的理解就是“Unicode=UCS”。

Unicode与UCS的区别。
Unicode和UCS毕竟两个不同机构研发的编码方案,它们之间还是存在着一些区别。Unicode和UCS虽然对全球字符编码的码值相同,ISO/IEC 10646标准,就像ISO/IEC 8859标准一样,只不过是一个简单的字符集表,但Unicode标准,额外定义了许多与字符有关的语义符号学。Unicode详细说明了绘制某些语言(如阿拉伯语)表达形式的算法,处理双向文字(比如拉丁文和希伯来文的混合文字)的算法,排序与字符串比较所需的算法,等等。此外两者部分样例字形有显著的区别。ISO/IEC 10646-1标准同样使用四种不同的风格变体来显示表意文字如中文、日文、韩文(即CJK),但Unicode 2.0的表里只有中文的变体。甚至存在“Unicode对日本用户来说不可接受”的不实传说。

UCS是ISO研发的全球通用字符集,那么UCS-2又是什么呢?
UCS-2(2-byte Universal Character Set,两字节通用字符集)是一个实际未使用的字符编码方案,是UTF-16的前身。还记得前面说到的Unicode的BMP吗,就是Unicode使用两字节来编码全球大部分文字字符的一个编码区间,号称0号平面,UCS-2是一个固定两字节长度的编码,每一个字符都采用一个单一的16位值来表示,因此只能表示Unicode的BMP范围的码值从U+0000到U+FFFF的字符。那么UCS-2和Unicode的0好平面又是啥关系呢?其实UCS-2编码的字符和Unicode的BMP编码的字符是相同的,因此UCS-2就是Unicode的BMP。那么UCS-2是那个机构颁发的呢,很显然是ISO。那么UCS-2和UCS有时什么关系呢?UCS-2是UCS的子集,UCS-2是UCS的编码方式之一。其中,中文范围 4E00-9FBF,即CJK 统一表意符号 (CJK Unified Ideographs)。

UCS-4又是什么呢?
UCS-2采用两个字节编码字符,只能标识65536个字符,对于Unicode编码的字符已经超过了十万个,很显然UCS-2只能标识了Unicode的0号平面字符,对于其它辅助平面字符,UCS-2就无能为力,心有余而力不足了。不过不用担心,IEEE(电气和电子工程师协会)就此问题推出了UCS-4编码方案,来标识的Unicode其它辅助平面编码的字符。UCS-4对所有的字符均采用四字节31位编码形式,码值范围是0x00000000-0x7FFFFFFF。

简短总结。
UCS-2的UCS-4都是ISO颁发的UCS(ISO 10646)标准中定义的两种编码方式,它们的关系是:
技术分享

7.UTF-8,UTF-16,UTF-32

7.1UTF-8

大概来说,Unicode编码系统可分为编码方式和实现方式两个层次。上面关于Unicode编码系统的解释,主要叙述了其的编码方式,即Uinicode每一个字符赋予了确切的不同的码值,但是实际使用当中,其实现方式是不同于编码方式的。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称UTF),这就是UTF-8,UTF-16,UTF-32为何已经存在了UCS-2和UCS-4还被提出的原因。

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码,由肯·汤普逊(Ken Thompson)于1992年创建,现在已经标准化为RFC 3629。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码。

UTF-8的编码方式。
UTF-8就是以8位为单元对UCS进行编码,而UTF-8不使用大尾序(大端字节序)和小尾序(小端字节序)的形式,每个使用UTF-8存储的字符,除了第一个字节外,其余字节的头两个比特都是以”10”开始,使文字处理器能够较快地找出每个字符的开始位置。但为了与以前的ASCII码兼容(ASCII为一个字节),因此UTF-8选择了使用可变长度字节来存储Unicode:

Unicode 和 UTF-8 之间的转换关系表 ( x 字符表示码点占据的位 )。

码点的位数 码点起值 码点终值 字节序列 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6
7 U+0000 U+007F 1 0xxxxxxx
11 U+0080 U+07FF 2 110xxxxx 10xxxxxx
16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
26 U+200000 U+3FFFFFF 5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
31 U+4000000 U+7FFFFFFF 6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx


必须要注意的是,2003年十一月,UTF-8被RFC 3629限制了长度用来匹配的UTF-16字符编码的约束,码值由原来的1~6字节缩减为1-4字节,新的UTF-8编码方式与Unicode编码的对应关系如下:

字节数 码点位数 码点起值 码点终值 Byte 1 Byte 2 Byte 3 Byte 4
1 7 U+0000 U+007F 0xxxxxxx
2 11 U+0080 U+07FF 110xxxxx 10xxxxxx
3 16 U+0800 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 21 U+10000 U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx


已知“严”的unicode是4E25(01001110 00100101),根据上表,可以发现4E25处在第三行的范围内(U+0800-U+FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5。我们以Notepad++(需要安装插件HEX-Editor,安装方法)查看中文”严”字的UTF-8编码。

技术分享

7.2UTF-16

UTF-16类似于UTF-8,都是变长字符编码,都是Unicode的实现方式之一。但是与UTF-8的区别主要有UTF-16最短编码长度是2个字节,UTF-8最短是1个字节,还有就是UTF-8不存在字节序的问题,UTF-16存在字节序的问题。与此同时,UTF-16还利用了Unicode保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。这里我想问,有了应用广泛的UTF-8,为何还要搞个UTF-16呢?还记得前面因为UCS-2的双字节码位不够,IEEE提出的UCS-4编码吗,因为UCS-4规定了每一个字符需要4个字节,31bits来表示,这样太浪费存储空间了,为了解决这个问题,于是IETF(The Internet Engineering Task Force,国际互联网工程任务组)于2000年提出了UTF-16的编码方案并发表在RFC 2781。

UTF-16编码方式。
用于数据存储或传递。Unicode字符的码位,需要1个或者2个16位长的码元来表示,因此这是一个变长表示。

16进制编码范围 UTF-16表示方法(二进制) 10进制码范围 字节数量
U+0000—U+FFFF xxxxxxxx xxxxxxxx yyyyyyyy yyyyyyyy 0-65535 2
U+10000—U+10FFFF 110110yyyyyyyyyy 110111xxxxxxxxxx 65536-1114111 4


UTF-16比起UTF-8,好处在于大部分字符都以固定长度的字节(2字节)存储,但UTF-16却无法兼容于ASCII编码,因UTF-8兼容 ASCII,能适应许多 C 库中的 ‘\0’结尾惯例,没有字节序问题,以及英文和西文符号比较多的场景下(如 HTML/XML),编码较短的优点,UTF-8 编码比 UTF-16 编码应用更为广泛。

UTF-16和UCS-2的区别与联系。
第一个Unicode平面(码位从U+0000至U+FFFF)包含了最常用的字符。UTF-16与UCS-2编码这个范围内的码位需要16比特长的单个码元,数值等价于对应的码位。BMP中的这些码位是仅有的可以在UCS-2中表示的码位。但是对于BMP外的其它辅助平面字符(surrogate code points),UCS-2却无法表示,但是UTF-16用1个或者2个16位长的码元来表示,既可以容纳UCS-2,也可以表示辅助平面字符,因此UTF-16可看成是UCS-2的父集。在没有辅助平面字符(surrogate code points)前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符后,就称为UTF-16了。现在若有软件声称自己支持UCS-2编码,那其实是暗指它不能支持在UTF-16中超过2字节的字集。对于小于0x10000的UCS码,UTF-16编码就等于UCS码。

7.3UTF-32

UTF-32是采用定长4字节来表示Unicode字符,不同于其它的Unicode transformation formats,UTF-8和UTF-16则使用不定长度编码。UTF-32在实际应用中也很少被使用,因为UTF-32对每个字符都使用4字节,就空间而言,是非常没有效率的。特别地,非基本多文种平面的字符在大部分文件中通常很罕见,以致于它们通常被认为不存在占用空间大小的讨论,使得UTF-32通常会是其它编码的二到四倍。

UTF-32与UCS-4关系。
原本ISO 10646标准定义了一个4字节31位的编码形式,称作UCS-4,使通用字符集(UCS)的每一个字符,会在0到0x7FFFFFFF这样的字码空间中,被表示成一个的31位的码值。UCS-4足以用来表示所有的Unicode的字码空间,其最大的码位为0x7FFFFFFF,其空间约为20亿个码位。有些人认为保留如此大的字码空间却只为了对应很小的码集是很浪费的,所以一个新的编码UTF-32被提出来了。UTF-32是一个UCS-4 的子集,使用4字节对字符编码,但是码值只从0到0x10FFFF(百万个码位),与Unicode码值一一对应。UTF-32 原本是 UCS-4 的子集,但JTC1/SC2/WG2声明,未来所有对字符的指定都将会限制在BMP及其14个补充平面。

于是就现状而言,除了 UTF-32 标准包含额外的 Unicode 意涵,UCS-4 和 UTF-32 大体是相同的。

8.GB2312,BIG5,GB13000,GBK,GB18030

本节讨论的内容主要围绕本土中文编码的发展以及不同编码之间的关系。计算机史上,中国大陆即台湾香港等地区自行研发的中文编码方案主要有GB2312,GBK,GB18030,BIG5,下面将一一讲解其大致的发展和特点。

GB2312。
GB2312 或 GB2312–80 是中华人民共和国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。GB 2312编码通行于中国大陆,新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB 2312。

GB2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。GB 2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。但对于人名、古汉语等方面出现的罕用字和繁体字,GB2312不能处理,因此后来GBK及GB 18030汉字字符集相继出现以解决这些问题。

GB2312在一段文本中,如果一个字节是0~127,那么这个字节的含义同ASCII编码,否则,这个字节和下一个字节共同组成汉字(或是GB编码定义的其他字符)。所以GB2312对ASCII编码是兼容的。也就是说,如果一段用GB编码文本里的所有字符都在ASCII中有定义,那么这段编码和ASCII编码完全一样。

GB13000,GBK,GB18030的由来。
GB编码早期收录的汉字不足一万个,基本满足日常使用需求,但不包含一些生僻的字,后来在一个个新版本中加进去。最早的GB编码是GB2312,由于GB2312-80只收录6763个汉字,根本不够用。1993年,随着Unicode 1.1版本推出,收录中国大陆、台湾、日本及韩国通用字符集的汉字,总共有20,902个。我国按捺不住,订定了等同于Unicode 1.1版本的国家中文编码标准GB13000(全称:GB 13000.1-93),采用双字节编码,但因其与GB2312不兼容,没有照顾到市场上软件厂商的感情,因为大部分中文软件都是采用了GB2312,所以一时间,GB13000并没有得到广泛的应用,现如今已是废弃的标准,这也是我们很少听到这个编码标准的原因。

GB13000虽然没有得到应用,但是收录了很多GB2312没有收录的汉字,还是起到了一定的作用。如部分在GB 2312-80推出以后才简化的汉字(如“啰”),部分人名用字(如中国前总理朱镕基的“镕”字),台湾及香港使用的繁体字,日语及朝鲜语汉字等,并未收录在内。于是微软利用GB 2312-80未使用的编码空间,与中国合作制订了GBK,GBK全称《汉字内码扩展规范》(GBK即“国标”、“扩展”汉语拼音的第一个字母,英文名称:Chinese Internal Code Specification)。因为微软的插手,GBK只被中华人民共和国国家有关部门作为技术规范。注意GBK并非国家正式标准,只是国家技术监督局标准化司、电子工业部科技与质量监督司发布的“技术规范指导性文件”。虽然 GBK收录了所有Unicode 1.1及GB 13000.1-93之中的汉字,但是编码方式与Unicode 1.1及GB 13000.1-93不同。仅仅是GB2312到GB 13000.1-93之间的过渡方案。但因为其在Windows95简体中文版开始使用,得到了广泛的推广,成为了事实上不争的中文编码标准。GBK编码,是在GB2312-80标准基础上的内码扩展规范,使用了双字节编码方案,其编码范围从0x8140至0xFEFE(剔除xx7F),共23940个码位,共收录了21003个汉字,完全兼容GB2312-80标准,支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字,并包含了BIG5编码中的所有汉字。GBK编码方案于1995年10月制定, 1995年12月正式发布,目前中文版的WIN95、WIN98、WINDOWS NT以及WINDOWS 2000、WINDOWS XP、WIN 7、WIN 8、WIN10等都支持GBK编码方案。

最新的是GB18030,国家质量技术监督局于2000年3月17日推出了GB 18030-2000标准,以取代GBK,加入了一些国内少数民族的文字,一些生僻字被编到4个字节,每扩展一次都完全保留之前版本的编码,所以每个新版本都向下兼容。

Big5的由来。
20世纪80年代初期,中国大陆制订了GB2312,可能因为改革开放搞得热火朝天,无暇顾及,没有考虑到台湾,香港,澳门同胞的使用繁体中文的情况,没有帮他们制定繁体中文编码(主要原因是还没回归),但是台湾同胞怎么能够容忍这种情况,于是自己搞了个Big5繁体中文编码。Big5又称为大五码或五大码,是使用繁体中文(正体中文)社区中最常用的电脑汉字字符集标准,共收录13,060个汉字。Big5虽普及于台湾、香港与澳门等繁体中文通行区,但长期以来并非当地的国家/地区标准或官方标准,而只是业界标准。倚天中文系统、Windows繁体中文版等主要系统的字符集都是以Big5为基准,但厂商又各自增加不同的造字与造字区,派生成多种不同版本。2003年,Big5被收录到CNS11643中文标准交换码的附录当中,获取了较正式的地位。这个最新版本被称为Big5-2003。

GBK,GB18030,Big5与ANSI的关系。
前面对ANSI已经有较详细的介绍,讲到了GBK,GB18030,Big5,这里再啰嗦一遍。

除了中文本土的编码方案,同样,日文、韩文、世界各国文字都有它们各自的编码(如果ASCII不能满足使用要求的话)。这些编码都和GB编码相似,兼容ASCII并用两个字节表示一个字。所有这些各国文字编码,微软统称为ANSI 。所以即使知道是ANSI,我们还需要知道这是哪国文字才能解码,因为这些编码都互相冲突。另外,你无法用一段ANSI 编码表示既有汉字、又有韩字的文本。

等等,上面我误导大家了……其实是微软误导大家了,严格来说ANSI 不是字符编码,而是美国一个非营利组织,他们做了很多标准制定工作,包括C语言规范ANSI C,还有各国文字编码对应的“代码页”(code page)。ANSI 规定简体中文GB编码的代码页是936,所以GB编码又叫做ANSI code page 936(按ANSI标准的代码页936),各国编码被统称为ANSI 由来于此——这是个离谱的历史错误,这就像我自己给国内的大学做了个排名,排名的依据是饭堂吃出虫子的概率,然后国内的大学就被统称为黄油猫。不过对于这些乱七八糟互相冲突的多国字符编码,有个统称还是不错的,下面讨论姑且继续使用ANSI 。

小结。
说了那么多,简单的梳理一下中文相关编码之间的关系。

从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。其中,GBK包含了BIG5编码中的所有汉字,但是GBK不兼容BIG5。GB13000码值与Unicode中文字符码值相同,与其它中文GB编码均不兼容。它们的关系如下图所示:
技术分享

9.BOM与Little Endian和Big Endian

字节序与大小端是伴随着多字节字符编码而出现的问题。单字节编码如ASCII是不存在编码字节序问题的,每一个字节代表一个字符,但是对于Unicode多字节字符编码,如UTF-16和UTF-32,就会存在字节序的问题。例如“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?

编码存储差异。
这里就要引出两个名词:
LE(Little Endian):小端字节序,意思就是一个单元在计算机中的存放时按照低位在低地址,高位在高地址的模式存放。

BE(Big Endian):大端字节序,和LE相反,是高位在低地址,低位在高地址的模式存放。

例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。如果将49写在前面,就是little endian。“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,一个皇帝送了命,另一个丢了王位。

我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾序”和“小尾序”或者大端序和小端序。

编码存储差异解决办法:BOM。
为了解决上面存储时字节序的问题,Unicode规范中推荐的标记字节顺序的方法是BOM(Byte Order Mark)头,意思是字节序标志头。在UCS编码中有一个叫做零宽度非换行空格(ZERO WIDTH NO-BREAK SPACE)的字符,它的编码是U+FEFF。而U+FEFF在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符零宽度非换行空格字符。这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符”ZERO WIDTH NO-BREAK SPACE”又被称作BOM。

UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。UTF-8编码的BOM是EF BB BF。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。Windows就是使用BOM来标记文本文件的编码方式的。通过它基本能确定编码格式和字节序。UTF相关编码的BOM如下。

UTF编码 Byte Order Mark
UTF-8 EF BB BF
UTF-16LE FF FE
UTF-16BE FE FF
UTF-32LE FF FE 00 00
UTF-32BE 00 00 FE FF

如果没有BOM只能靠猜了。软件读入文件时可以所有编码都试一下,看哪个像。另外,BOM只针对Unicode系列编码,ANSI通通不使用BOM。很显然,没有BOM难免偶然猜错。网上就流传着一个神奇的段子:打开Windows记事本,打入“联通”两个字,保存,关闭,再打开,变成了个黑块。

技术分享

有人说这是因为联通得罪了微软,当然这是玩笑话。记事本用ANSI(GBK)保存联通这两个字,编码是c1 aa cd a8,刚好这两个字的编码匹配到了UTF-8编码模式,于是就当成UTF-8来解析,结果就变成了U+006A和U+0368,也就是小写的字母”j”,和一个未知的字符。按照这种解释应该会显示字母j的,为什么两个字符都是乱码?请知道的网友留言告知,万分感谢。

可以认为,当文档中的所有字符的二进制编码在C0DF<script type="math/tex" id="MathJax-Element-6">C0\le第一个字节\le DF</script> 80BF<script type="math/tex" id="MathJax-Element-7">80≤第二个字节≤BF</script>时,记事本都无法确认文本的编码格式,就按照UTF-8的格式来显示,比如“透支”二字也会出现乱码。

BOM听起来很不错,但实际是个讨厌的设计,因为它和很多协议、规范不兼容,这是题外话。

有了BOM,于是很多文本编辑软件就会有UTF-8 without BOM,UTF-16 without BOM编码格式的选项。如果不提BOM,究竟有BOM还是没有BOM?又是一个十分纠结的问题,Windows里的软件一般都默认有BOM,而其它系统都默认没有BOM——可能是因为Windows常要兼容ANSI的原因,特别依赖BOM来防止出错。

10.内码,外码和Code Page(代码页)

前面在描述相关字符编码时也涉及到内码和代码页,但没有详细展开,这里简要的说明一下。

内码与外码关系。
内码是指操作系统内部的字符编码,内码其实就是字符编码。之所以称之为内码,是因为有外码这种东西。汉字输入码(外码)是指用户从键盘上键入汉字时所使用的汉字编码,存储计算机内部的就是汉字的内码。
常用的输入码有:
数字编码-区位码;
拼音编码-全拼、双拼、微软拼音输入法、自然码、智能ABC、搜狗等等;
字形编码-五笔、表形码、郑码输入法等。

早期操作系统的内码是与语言相关的.现在的Windows在内部统一使用Unicode,然后用代码页适应各种语言,” 内码”的概念就比较模糊了。我们一般将缺省代码页指定的编码说成是内码。内码这个词汇,并没有什么官方的定义。代码页也只是微软的一种习惯叫法。作为程序 员,我们只要知道它们是什么东西,没有必要过多地考证这些名词。

代码页是什么?
目前Windows的内核已经支持Unicode字符集,这样在内核上可以支持全世界所有的语言文字。但是由于现有的大量程序和文档都采用了某种特定语言的编码,例如GBK,Windows不可能不支持现有的编码,而全部改用Unicode。于是Windows使用代码页(code page)来标识各个国家和地区字符编码,所以代码页就是字符编码的代号。例如Windows系统中,GB2312对应的code page是CP20936,BIG5的code page是CP950,GBK对应的code page是CP936。GB18030对应的code page:CP54936。

11.后记

这篇杂谈的所谈论的内容实在是太庞杂了,坚持了四天,终于初步完成了本篇博文。字符编码涉及的用语和概念繁多,仔细考究的话,没有几十本著作和文献应该是没法说清楚的,所以本篇短短1W多字的精炼描述,是不可能对字符编码的历史,编码方式和关系完美诠释的,只求让大家有个大致的了解,免受因对字符编码的不解而带来的痛苦和困惑。文章操之过急,参考文献不足,再加上本人水平有限,难免出现不足和错误之处,望大家批评指正,留言探讨。

这应该是我在腾讯实习的最后一篇 blog了,离开前,留下点东西,刻下我在鹅场三个多月实习的痕迹。


参考文献

[1]为什么计算机能读懂1和0?
[2]latin1.百度百科
[3]ISO-8859-1.维基百科
[4]ANSI编码.百度百科
[5]Unicode.维基百科
[6]Unicode字符平面映射.维基百科
[7]谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词
[8]通用字符集.维基百科
[9]UTF-16/UCS-2.维基百科
[10]UTF-32.wikipedia
[11]UTF-8.维基百科
[12]UTF-8.wikipedia
[13]UTF-16.维基百科
[14]UTF-16.wikipedia
[15]UTF-32.wikipedia
[16]遇到乱码不怕不怕啦——计算机字符详尽讲解
[17]GB2312.维基百科
[18]汉字内码扩展规范.维基百科
[19]彻底搞懂字符编码(unicode,mbcs,utf-8,utf-16,utf-32,big endian,little endian…)
[20]windows内码、外码、字符映射表

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    精述字符编码(读这篇绝对不够)