字符集和字符编码

字符集和字符编码

一月 04, 2021

转载自:字符集和字符编码 - notbecoder - 博客园

1. 概述

现在的编程语言对字符串的处理一般封装比较好,所以平时编写代码,很少要自己考虑字符编码问题。以前学习xml时,由于xml的存储涉及到编码格式,查过一些资料,知道一些概念,GB2312、Unicode、Utf-8、Utf-16、UCS-2等,但这些概念之间什么关系,仍然一知半解。最近要做国际化,需要把不支持Unicode的程序升级为Unicode,借着这个机会,把其中的知识梳理了一遍,对字符编码的理解算更系统化了,在此总结一下。

2. 字符集和字符编码

  首先明确两个概念,“字符集”(charset)和“字符编码”(encoding)”。有些地方经常把这两个概念等同,比如说到ASCII码,有时候指“字符集”,有时候指“字符编码”,其实这是两个不同的概念,混淆了这两个概念,很难真正理解一些概念,比如Unicode和Utf8、Utf16的关系(其实Unicode是字符集和第一层字符编码,Utf8、Utf16是第二层字符编码,它们都表示了同一个字符集)。

字符集顾名思义是“许多字符的集合”,这些字符组成一套符号系统,可以组合起来形象的表达各种含义。比如26个英文字母加上标点符号可以组成“英文字符集”,这个字符集的组合可以形成英美人可以理解的一套文字系统,看到了“I love you”,就能明白什么意思。再比如新华字典中的所有汉字加上标点符号可以组成“中文字符集”,这个字符集的组合可以形成中文文字系统,看到“我爱你”就能理解其含义。对于老外,如果不懂中文,看到“我爱你”三个字符,可能以为就是“鬼画符”。

  “字符编码”字面意思就是“对字符进行编码”。呵呵,听起来有点废话。那先说下为什么要对字符进行编码。对字符编码的最大目的还是为了传输、储存信息(其实储存也是为了传输,是为了能传给以后的人看)。人和人之间交流,无非两种方式:口头交流,书面交流。书面交流就是把“字符”“画”到纸上或者其他介质上,然后传输给别人看。但是当传输过程遇到了特殊情况,就出现问题了。比如下面三种情况:

  1. 盲人和他人书面交流。
  2. 军队要传递情报,防止被敌军截获泄密。  
  3. 采用了新的传输、存储技术。比如电报、计算机。

  盲人和他人之间的书面交流采用盲文。国际上的盲文又叫六点盲文,是法国人路易·布莱尔(Louis Braille)于1824年发明的【1】。此前盲文就是普通的文字,只是印刷制作比较特殊,采用凸起的文字,便于触摸。但是学习、使用起来非常复杂,书写、阅读都很困难。路易·布莱尔上学时候,有个海军军官查尔斯·巴比埃给他们讲了一种“夜间书写”法符号,它是一种用两行各6个凸点的符号来表示各种音标的方法,是专为夜间作战时传递命令和加强联络而创造的。布莱尔由此受到启发,后来潜心研究,终于发明了六点盲文。

  六点盲文由63个编码字符组成,每个字符由1~6个突起的点儿安排在一个有6个点位的长方形里。为了确认63个不同的点式或盲文字符,数点位时是左起自上而下1—2—3,然后右起自上而下4—5—6。这些凸起在厚纸上的行行盲文,可以用手指轻轻摸读。六点盲文可以看作盲人书面交流的一种字符编码。

盲文

六点盲文字符集及编码【1】

  情报传递的方法比较多了,主要采用字符加密的方法以防止泄密。加密过程本身就是一种字符编码的过程,把能够看懂的文字通过某种算法,变成杂乱无章毫无意义的文字;或者把文字拆散,分布在许多毫无关系的文字中。接收方收到以后,再按照相反的方法还原,进行解码得到对方的真实信息。

  电报的传输通常使用“莫尔斯电码”。抗日剧、抗日电影中经常听到这种发报机的声音,“嘀,嘀嘀,嘀嘀嘀嘀……”。它的诞生,就是为了能用电信号传输文字。莫尔斯电码主要由点、划组成,比如“.-”表示“A”,“-…”表示“B”,可以通过发报机以脉冲电流的方式发送。发报方把文字编码,转成莫尔斯电码,接收方再解码,还原成文字。

电报现在虽然不再使用,莫尔斯电码在一些关键场合可能还会用到。“SOS”是国际救援信号,但它并非单词的缩写。二十世纪初,海难事件频繁发生,往往由于不能及时发出求救信号和最快组织施救,结果造成很大的人员伤亡和财产损失,后来国际无线电报公约组织于1908年正式将它确定为国际通用海难求救信号【2】。这三个字母组合没有任何实际意义,只是因为它的电码“ …—…”(三个圆点,三个破折号,然后再加三个圆点)在电报中是发报方最容易发出,接报方最容易辨识的电码。1912年泰坦尼克号成为海事史上第一艘发出SOS求救信号的船【3】。

  计算机诞生以后,面临跟电报同样的问题——计算机只有电子信号0和1,字符的形状对于它来说没有任何意义,如何表示各种文字符号呢?这样就产生了各种计算机字符集和字符编码——ASCII、Unicode等。

3. 字符编码的四个步骤

  从上一节来看,“字符集”是一种形象表意的工具,“字符编码”是表示字符的一种方式。在计算机出现之前就已经有了这两种技术。计算机中,是使用二进制的方式对字符集重新编码。

在计算机中,要建立一种“字符编码模型”,需要四步【4】

  1. 要有一个字符库,确定这些字符足够表意。 比如ASCII字符集,已经足够表示英语,但不能表示中文,于是产生GB2312字符集。

  2. 第一层编码,给每个字符编个数字,英文叫Code Point 或 Code Position。比如ASCII字符集中,65表示“A”,97表示“a”。

  3. 第二层编码,确定表示字符的二进制位数(8位,16位,32位)。ASCII使用7位,DBCS(双字节字符集)使用16位。

  4. 第三层编码,确定字符二进制值的存储格式(大端法,小端法)。比如X86机器使用小端法。

    一种字符集一般只有一种编码方式,当字符集不够用时,会增加一些新的符号,形成新的字符集。对于新的符号会有新的数字,新的编码格式。所以有时“字符集”和“字符编码”的概念并不严格区分。比如ASCII码,可以指128个字符的字符集,也可以指对这128个字符的编码方式。不过有的字符集有多种编码格式,比如Unicode字符集,Utf8、Utf16都是其编码格式(第二层编码)。

4.常用计算机编码

了解了“字符集”和“字符编码”的概念,下面说说计算机中的各种编码,及其来龙去脉。

4.1.ASCII码

全称“American Standard Code for Information Interchange”,美国标准信息交换码,由美国标准委员会(American Standards Association,简称ASA)制定,后来该协会改组为“美国国家标准学会”(American National Standard Institute , 简称ANSI ),所以很多资料上说ASCII码是ANSI制定的。ASCII码是从电报码发展过来的,最早使用是用在7-bit的电传打字机上的【5】。1960年10月6日,ASA开始ASCII的标准化工作,于1963年发布第一版,1967年再发布一次大的版本,这个标准版本,也是一个7-bit码,包含33个非打印字符(现在许多已经废弃了),95个打印字符(包含空格符)【5】

800px-ASCII_Code_Chart-Quick_ref_card

1972年打印机手册上的ASCII表【5】

详细介绍,可以参见维基百科-ASCII,百度百科中讲得比较笼统,有时候误用其表示其他含义,很容易引起混淆。

注意,这个最初是美国的标准,包含的95个打印字符中,无法囊括世界上所有国家的字符,比如英镑符号“£”,各种拉丁符号“δ、β、θ”,中文、日文、韩文这么多象形文字等等。为了解决这个问题,各国都制定了自己的字符集和字符编码,不过,基本上都兼容7-bti ASCII码。比如ISO-8859系列,日文的JIS,中国大陆的GB2312、GBK,台湾的Big5等。

4.2. Extended ASCII码

最初的标准ASCII码中包含的字符太少,很多应用或者国家中的符号都无法表示。比如数学符号“× ÷ ≠ ≥ ≈ π”。尤其20世纪70年代,随着PC的兴起,各电脑厂商开始增加自己的图形符号,形成了各种非标准的扩展ASCII码字符集(比如,ATASCIIPETSCII等)。TRS-80(美国早期的一种家庭电脑),增加了32个图形字符【6】

当时的IBM也为自己的PC定制了8-bit扩展ASCII码字符集,叫做代码页(code page),给不同语言制定不同的 code page,并把这些 code page 编号。这些 code page 都是兼容标准ASCII的。比如,北美市场的DOS电脑,使用 code page 437,包含了法语、德语和一些其他欧洲国家的带音调的字符【6】。code page 技术一直保留了下来,发展到现在,微软的windows内部就用了这种技术。

苹果电脑也在 Mac OS 系统中引入了自己的8-bit扩展ASCII编码,如 Mac OS Roman。国家的带音调的字符【6】

DEC公司也开发了 MCS(Multinational Character Set,多国字符集)【6】

4.3 ISO-8859

由于各公司、国家之间都有自己的字符集,同一个数值,在不同的字符集之间表示的符号可能不一样,这样在一台电脑上正常可以阅读的文件,到另外一台电脑可能就成了乱码。为了解决这个问题,ISO组织统一了一套标准字符集,就是ISO-8859.不过 ISO-8859 不是一个字符集,而是一系列扩充的ASCII码字符集。如下【7】

ISO/IEC 8859-1 (Latin-1) - 西欧语言
ISO/IEC 8859-2 (Latin-2) - 中欧语言
ISO/IEC 8859-3 (Latin-3) - 南欧语言。世界语也可用此字符集显示。
ISO/IEC 8859-4 (Latin-4) - 北欧语言
ISO/IEC 8859-5 (Cyrillic) - 斯拉夫语言
ISO/IEC 8859-6 (Arabic) - 阿拉伯语
ISO/IEC 8859-7 (Greek) - 希腊语
ISO/IEC 8859-8 (Hebrew) - 希伯来语(视觉顺序)
ISO 8859-8-I - 希伯来语(逻辑顺序)
ISO/IEC 8859-9(Latin-5 或 Turkish)- 它把Latin-1的冰岛语字母换走,加入土耳其语字母。
ISO/IEC 8859-10(Latin-6 或 Nordic)- 北日耳曼语支,用来代替Latin-4。
ISO/IEC 8859-11 (Thai) - 泰语,从泰国的 TIS620 标准字集演化而来。
ISO/IEC 8859-13(Latin-7 或 Baltic Rim)- 波罗的语族
ISO/IEC 8859-14(Latin-8 或 Celtic)- 凯尔特语族
ISO/IEC 8859-15 (Latin-9) - 西欧语言,加入Latin-1欠缺的芬兰语字母和大写法语重音字母,以及欧元(€)符号。
ISO/IEC 8859-16 (Latin-10) - 东南欧语言。主要供罗马尼亚语使用,并加入欧元符号。

由于英语没有任何重音字母(不计外来词),故可使用以上十五个字集中的任何一个来表示。至于德语方面,因它除了 A-Z, a-z 外,只用 Ä, Ö, ü, ä, ö, ß, ü 七个字母,而所有拉丁字集(1-4, 9-10, 13-16)均有此七个字母,故德语可使用以上十个字集中的任何一个来表示。
此系列中没有-12号的原因是,此计划原本要设计成一个包含塞尔特语族字符集的“Latin-7”,但后来塞尔特语族变成了ISO 8859-14 / Latin-8。亦有一说谓-12号本来是预留给印度天城体梵文的,但后来却搁置了。

4.4. MBCS、DBCS

上面所说的ASCII码、扩展ASCII码、ISO-8859中每个字符都是一个字节(8-bit)可以表示的,所以称为“单字节字符集”(Single-Byte Character Set,简称SBCS)。单字节字符集最多只能同时表示256个字符,ISO-8859中总字符数可能超过256个,但同时能使用的字符要么是8859-1字符集中的,要么是8859-2字符集中的,每个字符集最多仍然是256个,如果一篇文章中要出现所有语系中的字符,也是做不到的。

但是到了亚洲,中、日、韩等国家文字,每个文字都是一个符号,远远超过256个字符。于是,亚洲国家制定了自己的字符集,使用1个或2个或以上的字节数表示自己的字符集,这就是“多字节字符集”(Multi-Bytes Character Sets,简称MBCS)。

windows 系统中,本地字符集就是MBCS,不过由于大部分字符是2字节的,所以又称为“双字节字符集”(Double-Bytes Character Sets,简称DBCS),所以有的时候看到MBCS、DBCS,都是一回事。MBCS是完全兼容标准ASCII码的。

4.5. GB2312、GB13000、GBK、GB18030

这几种字符都是中国国家标准委员会制定的,简称“GB”(国标)XXX。

1980年,中国制定了自己的字符集标准,全称为《信息交换用汉字编码字符集–基本集》,简称GB2312-80,一共收录了 7445 个字符,包括 6763 个汉字和 682 个其它符号。GB2312-80,简称为GB2312

1993年,国际标准Unicode 1.1版本推出,收录中国大陆、台湾、日本及韩国通用字符集的汉字,总共有20,902个。中国大陆制定了等同于Unicode 1.1版本的“GB 13000.1-93”,简称为GB13000。

GB13000显然包含GB2312已有的文字和其他很多为包含的文字,如GB2312-80推出以后才简化的汉字(如“啰”),部分人名用字(如中国前总理朱镕基的“镕”字),台湾及香港使用的繁体字,日语及朝鲜语汉字等。

GBK全称《汉字内码扩展规范》(GBK即“国标”、“扩展”汉语拼音的第一个字母,英文名称:Chinese Internal Code Specification) ,中华人民共和国全国信息技术标准化技术委员会1995年12月1日制订,国家技术监督局标准化司、电子工业部科技与质量监督司1995年12月15日联合以技监标函1995 229号文件的形式,将它确定为技术规范指导性文件。这一版的GBK规范为1.0版【8】

国家标准GB18030-2005《信息技术中文编码字符集》是我国继GB2312-1980和GB13000.1-1993之后最重要的汉字编码标准,是我国计算机系统必须遵循的基础性标准之一。 GB18030有两个版本:GB18030-2000和GB18030-2005。GB18030-2000是GBK的取代版本,它的主要特点是在GBK基础上增加了CJK统一汉字扩充A的汉字。GB18030-2005的主要特点是在GB18030-2000基础上增加了CJK统一汉字扩充B的汉字【9】

4.6. ANSI 编码

ANSI愿意是指美国国家标准协会,但是在windows系统中,ANSI编码意思却代表“本地编码”。也就是说,在中国代表GBK,在台湾代表Big5,在日本代表JIS,所以windows编程中常说的ANSI字符串,就是指本地编码的字符串,在中国,就是一种DBCS,用1个和2个字节表示一个字符的编码。

为什么ANSI编码等同于本地编码?在微软的MSDN中找到一些说明:

早期,windows 中的英语和西欧字符集 code page 1252,是在ANSI草案基础上制定的,这个草案最后发展成ISO 8859-1,由此看来code page 1252 是早于ISO 8859-1标准的。因为这个,windows code page,通常被称为“ANSI code page”。

windows code page 有时候也指“系统活动代码页”,一个windows操作系统,总会有一个当前的活动代码页,也就是“本地代码页”【11】。

可能因为这个原因,ANSI编码等同于本地编码。

不过也可以这么理解:ANSI编码本来只包含了英文编码,后来到了各国,在ANSI编码基础上扩展成了本地编码,所以ANSI编码也可以指本地编码,ANSI字符串也表示本地字符串。

4.7. Unicode、UCS

以上的编码都是本地化编码,一国之内还没有问题,但是要跨国,就不行了。比如汉字,在只有ISO-8859系列字符集的电脑上显示就只能是乱码了,要显示汉字,电脑上必须装GB2312或GBK的字符集。有没有一个字符集,能够包含全球所有的字符呢?这就是Unicode和UCS。

Unicode起源于1987年,施乐的 Joe Becker、苹果的 Lee Collins 和 Mark Davis 就开始研究能否创造一种全球通用的字符集。1988年,Joe Becker 发布了一个草案,提出了“Unicode”的概念,他解释说“‘Unicode’是一种唯一的、统一的、全球的编码”。【12】。后来,RLG、Sun、Microsoft、NeXT(乔布斯被赶出苹果后创建的公司)的人也都逐渐加入到Unicode工作组里。1991年1月3日,Unicode联盟组织成立,同年发布了Unicode1.0.

同时,ISO组织也在做同样的事情,创造一个全球统一的字符集(Universal Coded Character Set,简称UCS),1993年发布了标准ISO 10646-1。

后来,两个组织认识到,世界不需要两个不兼容的字符集,于是,开始合作。从Unicode2.0开始,开始采用和UCS相同的字库和字码。这样,两个项目仍都存在,并独立地公布各自的标准。但双方都同意保持两者标准的码表兼容,并紧密地共同调整任何未来的扩展。所以,现在说到UCS字符集,跟Unicode可以看成一回事。

Unicode编码包含两个层次(见第三节,计算机编码四步),第一层定义字符的数值和第二层定义数值的实现方式。Unicode用数字 0x0~0x10FFFF 表示所有字符,所以最多可以容纳 1114112 个字符。数值的编码方式,也就是实现方式包括 UTF-8,UTF-16,UTF-32 三种。

有人会说,Unicode不是两个字节表示字符的码?为什么数值可以到0x10FFFF,这不21位,两个半字节还多了吗?其实,这是混淆了Unicode的数值定义和实现,这根本就是两个概念,Unicode到底用几个字节表示,取决于其实现方式是UTF-8,UTF-16,还是UTF-32.

比如,“汉字”对应的Unicode值是0x6c49和0x5b57,而编码实现是:

char data_utf8[]={0xE6,0xB1,0x89,0xE5,0xAD,0x97}; //UTF-8编码
char16_t data_utf16[]={0x6C49,0x5B57}; //UTF-16编码
char32_t data_utf32[]={0x00006C49,0x00005B57}; //UTF-32编码

4.8. UTF-8

UTF,全称“Unicode Transformation Formats”。是Unicode的编码格式。

UTF-8是使用8-bit为单位,对Unicode进行编码的。特点是,对不同范围的字符使用不同长度的编码。

Unicode编码(十六进制) UTF-8 字节流(二进制)
00000000 - 0000007F 0xxxxxxx
00000080 - 000007FF 110xxxxx 10xxxxxx
00000800 - 0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
00010000 - 001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
00200000 - 03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
04000000 - 7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8编码的最大长度是6个字节。

对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同,用1个字节表示,首位为0。

对于0x80-0x7FF之间的字符,用2个字节表示,第一个字节前三位“110”为标志位,第二个字节前两位“10”为标志位。剩下的11位用来表示Unicode值(7FF最多11位)。

同样,UTF-8的3个字节,可以表示0x800-0xFFFF的Unicode(最多16位)。

UTF-8的4个字节,可以表示0x10000-0x001FFFFF的Unicode(最多21位)。

4个字节以内,已经包含了Unicode所有字符。

5、6个字节表示的已经是非Unicode编码范围,属于UCS-4 编码。早期UTF-8规范也可以达到6字节序列,不过2003年11月UTF-8 被 RFC 3629 重新规范,只能使用原来Unicode定义的区域, U+0000到U+10FFFF。根据规范,这些字节值将无法出现在合法 UTF-8序列中。

例1:“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
例2:Unicode编码0x20C30在0x010000-0x10FFFF之间,使用用4字节模板了:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。将0x20C30写成21位二进制数字(不足21位就在前面补0):0 0010 0000 1100 0011 0000,用这个比特流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。

UTF-8有两个好处:

  1. 1字节字符、2字节字符、3字节字符……的首字节标志位不同,这样可以很清楚的区分一个字节属于1字节字符还是2字节字符,如果一个字节流传输中出现错误,也不会错位,只影响部分字符,根据标志位,很容易找到下个正确字符。

  2. 兼容ASCII码,英美字符用UTF-8可以一个字节表示,所以,www组织选用UTF-8作为推荐编码格式。2007年,在互联网上,UTF-8格式已经超过了ASCII码。

4.9 UTF-16

UTF-16以2字节为单位,等同于UCS-2.

Unicode编码(十六进制) UTF-16 字节流(二进制)
00000000 - 0000FFFF xxxxxxxx xxxxxxxx
00010000 - 0010FFFF 110110yyyyyyyyyy 110111xxxxxxxxxx

Unicode值小于等于0xFFFF的,直接用两个字节表示,超过0xFFFF的,无法用两个字节表示。使用下面公式编码:

  1. 计算 U’= U – 0x10000

  2. 将U’写成二进制形式:yyyy yyyy yyxx xxxx xxxx

  3. 加上标志位,1101 10yy yyyy yyyy 1101 11xx xxxx xxxx

可见,这是4个字节表示,2个6位标志位,20位有效位。因为U最大是0x10FFFF,所以U’最大是0xFFFFF,20位足够表示。

windows上默认的Unicode编码方式就是UTF-16,使用wchar_t表示。

4.10 UTF-32

UTF-32编码以4字节为单位。直接把Unicode值转为4字节二进制数就是其UTF-32编码。等同于UCS-4.

5.各种字符编码之间的关系

上面关于字符集和编码讲了许多概念,其实归类一下可以这么理解。

首先是单字节字符集:

1、最初美国ANSI发明了自己的编码ASCII,7-bit足够,这是标准ASCII。

2、标准ASCII码没有西欧国家拉丁文、英镑等字符,各公司、国家开始扩展,形成自己的扩展ASCII码字符集,各方混战,不过8-bit也就足够。

3、 天下分久必合,ISO统一了8-bit字符集,叫做ISO 8859.

但是亚洲国家字符更多,一个字节远远不够,于是用多个字节表示,扩展形成本国字符集,中国GB系列,台湾Big5,日本JIS……,这些叫做多字节字符集(MBCS),windows中用双字节表示,也叫做(DBCS)。

以上字符都是群雄割据,各自为政,windows为了迎合大家需求,在哪个国家,默认编码就用那个国家的,不过后来不知怎么被误传位ANSI编码,其实ANSI怎么可能定义世纪各国编码,不过可以理解成各编码都是在ANSI基础上扩展的,因为都兼容ANSI的标准ASCII码。

这时,ISO再次出手,和Unicode联盟携手打造了Unicode(UCS),意图一统江湖。Unicode确实包罗万象,涵盖了各国字符,于是流行世界。Unicode自身只定义了每个字符的数值,真正二进制编码格式却是UTF-8,UTF-16(UCS-2),UTF-32(UCS-4)。

至此,天下一统,但愿程序员的太平盛世到来了!

参考文档:

【1】百度百科,《盲文》,http://baike.baidu.com/view/23057.htm

【2】百度百科,《SOS》,http://baike.baidu.com/subview/1744/5037817.htm

【3】松田行正,《零ZERO:世界符号大全》,中央编译出版社

【4】Peter Constable,《Character set encoding basics》,SIL,2001-06-13:http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter03#0d7ccb25

【5】维基百科,《ASCII》,https://en.wikipedia.org/wiki/ASCII

【6】维基百科,《Extended ASCII》,https://en.wikipedia.org/wiki/Extended_ASCII

【7】百度百科,《ISO-8859》,http://baike.baidu.com/view/9753105.htm

【8】百度百科,《GBK字库》,http://baike.baidu.com/view/931619.htm

【9】百度百科,《gb18030》,http://baike.baidu.com/view/889058.htm

【10】Crifan Li,《字符编码详解》,http://www.crifan.com/files/doc/docbook/char_encoding/release/html/char_encoding.html#enc_unicode

【11】MSDN,《Code Pages》,https://msdn.microsoft.com/en-us/library/dd317752(VS.85).aspx

【12】维基百科,《Unicode》https://en.wikipedia.org/wiki/Unicode