小结字符集及字符编码问题

字符编码问题一直深深困扰着我~无论是网页还是数据库抑或是单纯的文件字符流,总有各种奇怪的编码问题。之所以称之为奇怪其实主要还是因为我对于编码的知识了解太浅。近来深刻觉醒编码问题非解决不行,故将所阅读的资料内容概括整理如下。

字符编码与字符集

一直以来我常常把字符编码和字符集混着说,而周围的人大多也都不区分它们的含义。不过真要较真的话,字符编码和字符集其实还是很有区别的。

当然,从简单字符集的角度来说,按照惯例,人们认为字符集和字符编码是同义词,因为使用同样的标准来定义提供什么字符并且这些字符如何编码到一系列的代码单元(通常一个字符一个单元)。

但是对于由统一码和通用字符集所构成的现代字符编码模型来说,这些概念之间有了细微的区别。它们将字符编码的概念分为:有哪些字符、它们的编号、这些编号如何编码成一系列的“码元”(有限大小的数字)以及最后这些单元如何组成八位字节流。区分这些概念的核心思想是建立一个能够用不同方法来编码的一个通用字符集。为了正确地表示这个模型需要更多比“字符集”和“字符编码”更为精确的术语表示。现代模型中所用的术语有字符集(Character Set)、编码字符集(CCS:Coded Character Set)、字符编码表(CEF:Character Encoding Form)、字符编码方案(CES:Character Encoding Scheme)等。这里的定义比较学术,大家感兴趣的可以自己查找维基百科,链接在本文后面的参考资料里可以找到。为了好记,我又找到一些比较通俗的关于字符集与字符编码区别的说法:

字符集:字符的集合,规定了在这些集合里面有哪些字符。比如Unicode字符集,目标就是收纳了这个世界上所有语言的文字、符号等。

字符编码:就是规定用一个字节还是用多个字节来存储一个字符,用固定的二进制码值表示某个字符。注意,字符集只是规定了有哪些字符,而最终决定采用哪些字符,每一个字符用多个字节表示等问题,则是由编码来决定的。像Unicode字符集的编码方式有很多,诸如UTF-8、UFT-16、UTF-32等。

字符编码举例

要解决编码问题,首先要明确究竟都有哪些编码,它们有什么样的特点,相互之间有何种关系。这样使用起来才能够有的放矢。

在《Java编程思想》一书中,作者Bruce Eckel就是通过讲述文件输入输出流发展历史的方式清晰地展示了Java IO包中的各个stream、reader和writer该如何使用。我个人深感这是一种很好的学习方法,所以这里借鉴一下,也尽量按照字符编码的发展历史来介绍各个编码,这样我们就很容易明白这种编码为什么会诞生,以及它的特性了。

首先先解释一下“字符”与“字节”的区别:

字节(octet):是一个8位的物理存贮单元,取值范围一定是0~255。

字符(character):是一个文化相关的符号,或说是一个语言上的符号,如“中”字就是一个字符。字符所占的大小由其编码方式解决,比如“中”在UTF-8中占3个字节(0xE4A8AD),而在GBK中,则占两个字节(0xD6D0)。

1. 内码

内码是操作系统内部所采用的字符编码,并不特指某种编码。比如早期的DOS采用的是ASCII编码,而现在的操作系统大都采用Unicode编码。

2. ASCII码

ASCII编码全称为American Standard Code for Information Interchange(美国信息交换标准代码)。对应的ISO 标准为ISO646标准。

ASCII字符集:主要包括控制字符(回车键、退格、换行键等),可显示字符(英文大小写字符、阿拉伯数字和西文符号)。基本的ASCII字符集共有128个字符,其中有96个可打印字符,包括常用的字母、数字、标点符号等,另外还有32个控制字符。

ASCII编码:将ASCII字符集转换为计算机可以接受的数字系统,使用7位(bits)表示一个字符,共128字符。每个ASCII码以1个字节(Byte)储存,从0(二进制:0000 0000)到127(二进制:0111 1111)代表不同的常用符号。例如大写A的ASCII码是65(二进制:0100 0001),小写a则是97(二进制:0110 0001)。虽然标准ASCII码是7位编码,但由于计算机基本处理单位为字节(1byte = 8bit),所以一般仍以一个字节来存放一个ASCII字符。每个字节中多余出来的一位(最高位)在计算机内部通常保持为0(在数据传输时可用作奇偶校验位)。

ASCII码的缺点:只能显示26个基本拉丁字母、阿拉伯数字和英式标点符号,因此只能用于显示现代美国英语(而且在处理英语当中的外来词如naïve、café、élite等等时,所有重音符号都不得不去掉,即使这样做会违反拼写规则)。

3. 扩展ASCII码(Extended ASCII)

由于ASCII码只用了字节的七位,最高位并不使用,所以后来又将最高的一位也编入这套编码中,成为八位的扩展ASCII码。对应的标准为ISO2022标准,它规定了在保持与 ISO646兼容的前提下将ASCII字符集扩充为8位代码的统一方法。

扩展ASCII字符集:ISO陆续制定了一批适用于不同地区的扩充ASCII字符集,每种扩充ASCII 字符集分别可以扩充128个字符,这些扩充字符的编码均为高位为1的8位代码(即十进制数128~255)。

扩展ASCII码:为了表示更多的欧洲常用字符对ASCII进行了扩展,ASCII扩展字符集使用8位(bits)表示一个字符,共256字符。

EASCII码的缺点:虽然解决了部份西欧语言的显示问题,但对更多其他语言依然无能为力。

4. 区位码

每个汉字或图形符号分别用两位的十进制区码(行码)和两位的十进制位码(列码)表示,不足的地方补0,组合起来就是区位码。比如“啊”的区位码是1601,写成16进制是0x10,0x01(二进制:00010000 00000001)。

不过这和计算机广泛使用的ASCII编码冲突。为了兼容00-7f的 ASCII编码,我们在区位码的高、低字节上分别加上0xA0(二进制:10100000)。这样“啊”的编码就成为0xB0A1。我们将加过两个0xA0的编码也称为GB2312编码,虽然 GB2312的原文根本没提到这一点。

5. 国标码

把区位码按一定的规则转换成的二进制代码叫做信息交换码(简称国标码)

国标码共有汉字6763个(一级汉字,是最常用的汉字,按汉语拼音字母顺序排列,共3755个;二级汉字,属于次常用汉字,按偏旁部首的笔划顺序排列,共3008个),数字、字母、符号等682个,共7445个。

汉字信息在计算机内部也是以二进制方式存放。由于汉字数量多,用一个字节的128种状态不能全部表示出来,因此在1980年我国颁布的《信息交换用汉字编码字符集——基本集》,即国家标准GB2312-80方案中规定用两个字节的十六位二进制表示一个汉字,每个字节都只使用低7位(与ASCII码相同), 即有128×128=16384种状态。由于ASCII码的34个控制代码在汉字系统中也要使用,为不致发生冲突,不能作为汉字编码,128除去34只剩 94种,所以汉字编码表的大小是94×94=8836,用以表示国标码规定的7445个汉字和图形符号。

具体详见后面的GB2312(80)中。

6. 机内码(汉字内码,内码)

由于国标码不能直接存储在计算机内,为方便计算机内部处理和存储汉字,又区别于ASCII码,将国标码中的每个字节在最高位改设为1,这样就形成了在计算机内部用来进行汉字的存储、运算的编码叫机内码。内码既与国标码有简单的对应关系,易于转换,又与ASCII码有明显的区别,且有统一的标准(内码是惟一的)。

7. 汉字外码(汉字输入码)

无论是区位码或国标码都不利于输入汉字,为方便汉字的输入而制定的汉字编码,称为汉字输入码。汉字输入码属于外码。不同的输入方法,形成了不同的汉字外码。常见的输入法有以下几类:

(1)按汉字的排列顺序形成的编码(流水码):如区位码;

(2)按汉字的读音形成的编码(音码):如全拼、简拼、双拼等;

(3)按汉字的字形形成的编码(形码):如五笔字型、郑码等;

(4)按汉字的音、形结合形成的编码(音形码):如自然码、智能ABC。

输入码在计算机中必须转换成机内码,才能进行存储和处理。

8. 汉字字形码

为了将汉字在显示器或打印机上输出,把汉字按图形符号设计成点阵图,就得到了相应的点阵代码(字形码)。

汉字字库:全部汉字字形码的集合叫汉字字库。汉字字库可分为软字库和硬字库。软字库以文件的形式存放在硬盘上,现多用这种方式,硬字库则将字库固化在一个单独的存储芯片中,再和其它必要的器件组成接口卡,插接在计算机上,通常称为汉卡。

显示字库:用于显示的字库叫显示字库。显示一个汉字一般采用16×16点阵或24×24点阵或48×48点阵。已知汉字点阵的大小,可以计算出存储一个汉字所需占用的字节空间。例:用16×16点阵表示一个汉字,就是将每个汉字用16行,每行16个点表示,一个点需要1位二进制代码,16个点需用16位二进制代码(即2个字节),共16行,所以需要16行×2字节/行=32字节,即16×16点阵表示一个汉字,字形码需用32字节。即:字节数=点阵行数×点阵列数/8。

打印字库:用于打印的字库叫打印字库,其中的汉字比显示字库多,而且工作时也不像显示字库需调入内存。

9. 代码页

所谓代码页(code page)就是针对一种语言文字的字符编码。例如GBK的code page是CP936,BIG5的code page是CP950,GB2312的code page是CP20936。

Windows中有缺省代码页的概念,即缺省用什么编码来解释字符。例如Windows的记事本打开了一个文本文件,里面的内容是字节流:BA、BA、 D7、D6。Windows应该去怎么解释它呢?是按照Unicode编码解释、还是按照GBK解释、还是按照BIG5解释,还是按照ISO-8859-1去解释?如果按GBK去解释,就会得到“汉字”两个字。按照其它编码解释,可能找不到对应的字符,也可能找到错误的字符。所谓“错误”是指与文本作者的本意不符,这时就产生了乱码。

答案是Windows按照当前的缺省代码页去解释文本文件里的字节流。缺省代码页可以通过控制面板的区域选项设置。记事本的另存为中有一项ANSI,其实就是按照缺省代码页的编码方法保存。

Windows的内码是Unicode,它在技术上可以同时支持多个代码页。只要文件能说明自己使用什么编码,用户又安装了对应的代码页,Windows就能正确显示,例如在HTML文件中就可以指定charset。

有的HTML文件作者,特别是英文作者,认为世界上所有人都使用英文,在文件中不指定charset。如果他使用了0x80-0xff之间的字符,中文Windows又按照缺省的GBK去解释,就会出现乱码。这时只要在这个html文件中加上指定charset的语句,例如:

<meta http-equiv=”Content-Type” content=”text/html; charset=ISO8859-1″>

如果原作者使用的代码页和ISO8859-1兼容,就不会出现乱码了。

10. GB2312(80)

GB2312是汉字字符集和编码的代号,中国国家标准简体中文字符集,中文全称为“信息交换用汉字编码字符集-基本集”,又称GB0,由中华人民共和国国家标准总局发布,一九八一年五月一日实施。GB 是“国标” 二字的汉语拼音缩写。

GB2312字符集:只收录简化字汉字,以及一般常用字母和符号,主要通行于中国大陆地区和新加坡等地。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。GB2312采用2个字节,共收录有7445个字符,其中简化汉字6763个,字母和符号682个。GB2312 将所收录的字符分为94个区,编号为01区至94区;每个区收录94个字符,编号为01位至94位。GB2312的每一个字符都由与其唯一对应的区号和位号所确定。例如:汉字“啊”,编号为16区01位。GB2312 字符集的区位分布表:

区号 字数 字符类别
01 94 一般符号
02 72 顺序号码
03 94 拉丁字母
04 83 日文假名
05 86 Katakana
06 48 希腊字母
07 66 俄文字母
08 63 汉语拼音符号
09 76 图形符号
10-15 备用区
16-55 3755 一级汉字,以拼音为序
56-87 3008 二级汉字,以笔划为序
88-94 备用区

GB2312字符编码:GB2312是对 ASCII码的中文扩展,规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(称之为高字节)从0xA1用到 0xF7(由字符的区号值加上32而形成),后面一个字节(低字节)从0xA1到0xFE(由字符的位号值加上32而形成),这样我们就可以组合出大约7000多个简体汉字了。GB2312原始编码是对所收录的每个字符都用两个字节(byte)表示。例如:汉字“啊”,编号为16区01位。它的高字节为16 + 32 = 48(0x30),低字节为01 + 32 = 33(0x21),合并而成的编码为0x3021。在区位号值上加32的原因大慨是为了避开低值字节区间。由于 GB2312 原始编码与 ASCII 编码的字节有重叠,现在通行的GB2312编码是在原始编码的两个字节上各加128修改而形成。例如:汉字“啊”,编号为16区01位。它的原始编码为0x3021,通行编码为0xB0A1(总体看其实是在区位码的高字节和低字节上各加上0xA0)。如果不另加说明,GB2312 常指这种修改过的编码。

GB2312 字符集是Unicode字符集的一个子集。这也就是说,GB2312 所收录的每一个字符都收录在Unicode之中。但是GB2312编码和Unicode编码确没有什么相同之处。同一个汉字,它的GB2312编码和Unicode编码确毫不相同。例如:汉字“啊”,它的GB2312编码为0xB0A1,但是它的Unicode编码为0x554A。

11. GBK

微软利用GB 2312-80未使用的编码空间,收录GB 13000.1-93全部字符制定了GBK编码。根据微软资料,GBK是对GB2312-80的扩展,也就是CP936字码表(Code Page 936)的扩展(之前CP936和GB 2312-80一模一样),最早实现于Windows 95简体中文版。虽然GBK收录GB 13000.1-93的全部字符,但编码方式并不相同。GBK自身并非国家标准,只是曾由国家技术监督局标准化司、电子工业部科技与质量监督司公布为”技术规范指导性文件”。原始GB13000一直未被业界采用,后续国家标准GB18030技术上兼容GBK而非GB13000。

GBK字符集:GBK也采用2个字节,包括了GB2312的所有内容,同时又增加了近20000个新的汉字(包括几乎所有的Big5中的繁体汉字)和符号。总共21003个字符。

GBK字符编码:因为汉字太多,GB2312不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为GBK 标准

12. GB18030

全称为国家标准GB 18030-2005《信息技术 中文编码字符集》,是中华人民共和国现时最新的内码字集,是GB 18030-2000《信息技术 信息交换用汉字编码字符集 基本集的扩充》的修订版。与GB 2312-1980完全兼容,与GBK基本兼容,支持GB 13000及Unicode的全部统一汉字。

GB18030字符集:为了加入几千个新的少数民族的字,GBK扩成了GB18030。

GB18030字符编码:它采用了变长的编码方式,有1、2、4个字节的编码长度。1个字节编码与ASCII兼容,2个字节编码与GBK兼容,4个字节主要是收录了少数民族的文字等。

GB18030现在是国家非手持/非嵌入式设备的强制性标准。所有的Unicode编码都可以转换为GB18030,而且GB18030除了兼容GBK以及Unicode的BMP部分外,其余的Unicode扩展平面和它的4字节扩展平面都是简单直接的映射。其具体映射关系的计算参见《GB18030编码研究以及GBK、GB18030与Unicode的映射》:[http://blog.csdn.net/fmddlmyy/archive/2008/04/13/2288312.aspx]

GB 18030主要有以下特点:

(1)与UTF-8相同,采用多字节编码,每个字可以由1个、2个或4个字节组成。

(2)编码空间庞大,最多可定义161万个字符。

(3) 支持中国国内少数民族的文字,不需要动用造字区。

(4)汉字收录范围包含繁体汉字以及日韩汉字

13. DBCS

GB2312、GBK、GB18030等一系列汉字编码的标准被通称叫做“DBCS”(Double Byte Charecter Set 双字节字符集)。在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。

14. BIG5

BIG5字符集又称为大五码或五大码,是使用繁体中文(正体中文)社区中最常用的电脑汉字字符集标准,共收录13,060个汉字。

BIG5字符集:它是台湾繁体字集,共包括国标繁体汉字13053个。中文码分为中文内码及交换码两类,Big5属中文内码,知名的中文交换码有CCCII、CNS11643。Big5虽普及于台湾、香港与澳门等繁体中文通行区,但长期以来并非当地的国家标准,而只是业界标准。倚天中文系统、Windows等主要系统的字符集都是以Big5为基准,但厂商又各自增加不同的造字与造字区,派生成多种不同版本。2003年,Big5被收录到CNS11643中文标准交换码的附录当中,取得了较正式的地位。这个最新版本被称为Big5-2003。

BIG5字符编码:Big5码是一套双字节字符集,使用了双八码存储方法,以两个字节来安放一个字。第一个字节称为”高位字节”,第二个字节称为”低位字节”。”高位字节”使用了0x81-0xFE,”低位字节”使用了0x40-0x7E,及0xA1-0xFE。在Big5的分区中:

0x8140 0xA0FE  保留给用户自定义字符(造字区)
0xA140 0xA3BF  标点符号、希腊字母及特殊符号,包括在0xA259-0xA261,安放了九个计量用汉字:兙兛兞兝兡兣嗧瓩糎。
0xA3C0 0xA3FE  保留。此区没有开放作造字区用。
0xA440 0xC67E   常用汉字,先按笔划再按部首排序。
0xC6A1 0xC8FE  保留给用户自定义字符(造字区)
0xC940 0xF9D5   次常用汉字,亦是先按笔划再按部首排序。
0xF9D6 0xFEFE  保留给用户自定义字符(造字区)
15. ANSI

ANSI是美国国家标准局的缩写,这里用来指代一类编码。使用2个字节来代表一个字符的各种汉字延伸编码方式,称为ANSI编码。比如,在简体中文系统下,ANSI编码代表GB2312编码;在日文操作系统下,ANSI编码代表JIS编码。

非英文系的国家为了显示自家的文字,不得不一开始就得面对字符编码的问题,不同国家不同地区都创建了自己的编码标准。像是中国大陆是GB2312及后来的GBK,台湾是BIG5,日本是JIS。ASCII字符集,以及这些由此派生并兼容的字符集称为ANSI字符集。

16. ISO-8859-1

ISO 8859,全称ISO/IEC 8859,是国际标准化组织(ISO)及国际电工委员会(IEC)联合制定的一系列8位字符集的标准,现时定义了15个字符集。

ISO 8859字符集:ASCII收录了空格及94个“可印刷字符”,足以给英语使用。但是,其他使用拉丁字母的语言(主要是欧洲国家的语言),都有一定数量的变音字母,故可以使用ASCII及控制字符以外的区域来储存及表示。除了使用拉丁字母的语言外,使用西里尔字母的东欧语言、希腊语、泰语、现代阿拉伯语、希伯来语等,都可以使用这个形式来储存及表示。

* ISO 8859-1 (Latin-1) – 西欧语言;

* ISO 8859-2 (Latin-2) – 中欧语言;

* ISO 8859-3 (Latin-3) – 南欧语言。世界语也可用此字符集显示;

* ISO 8859-4 (Latin-4) – 北欧语言;

* ISO 8859-5 (Cyrillic) – 斯拉夫语言;

* ISO 8859-6 (Arabic) – 阿拉伯语;

* ISO 8859-7 (Greek) – 希腊语;

* ISO 8859-8 (Hebrew) – 希伯来语(视觉顺序);

* ISO 8859-8-I – 希伯来语(逻辑顺序);

* ISO 8859-9 (Latin-5 或 Turkish) – 它把Latin-1的冰岛语字母换走,加入土耳其语字母;

* ISO 8859-10 (Latin-6 或 Nordic) – 北日耳曼语支,用来代替Latin-4;

* ISO 8859-11 (Thai) – 泰语,从泰国的 TIS620 标准字集演化而来;

* ISO 8859-13 (Latin-7 或 Baltic Rim) – 波罗的语族;

* ISO 8859-14 (Latin-8 或 Celtic) – 凯尔特语族;

* ISO 8859-15 (Latin-9) – 西欧语言,加入Latin-1欠缺的法语及芬兰语重音字母,以及欧元符号;

* ISO 8859-16 (Latin-10) – 东南欧语言。主要供罗马尼亚语使用,并加入欧元符号;

很明显,iso8859-1编码表示的字符范围很窄,无法表示中文字符。但是,由于是单字节编码,和计算机最基础的表示单位一致,所以很多时候,仍旧使用iso8859-1编码来表示。而且在很多协议上,默认使用该编码。

ISO 8859字符编码:我们知道ASCII码是从0x00(二进制:0000 0000)到0x7F(二进制:0111 1111),也就是还有1位没有用到,ISO 8859-1就是在空置的0xA0-0xFF(二进制:1010 0000-1111 1111)的范围内,加入192个字母及符号,藉以供使用变音符号的拉丁字母语言使用。所以ISO 8859-1又称Latin-1。

17. Unicode(统一码、万国码、单一码)

全称为Universal Multiple-Octet Coded Character Set,简称UCS(通用字符集-Universal Character Set)

历史上存在两个独立的尝试创立单一字符集的组织,即国际标准化组织(ISO)和多语言软件制造商组成的统一码联盟(http://www.unicode.org)。前者开发的ISO/IEC 10646项目,后者开发的统一码项目。1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码;ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。两个项目仍都存在,并独立地公布各自的标准。但统一码联盟和ISO/IEC JTC1/SC2都同意保持两者标准的码表兼容,并紧密地共同调整任何未来的扩展。在发布的时候,Unicode一般都会采用有关字码最常见的字型,但ISO 10646一般都尽可能采用Century字型。

Unicode的编码方式与ISO 10646的通用字符集(UCS)概念相对应,目前的用于实用的Unicode版本对应于UCS-2,使用16位的编码空间。也就是每个字符占用2个字节,总共可以组合出65535个不同的字符,这大概已经可以覆盖世界上所有文化的符号。实际上目前版本的Unicode尚未填满这16位编码,保留了大量空间作为特殊使用或将来扩展。如果还不够也没有关系,ISO已经准备了UCS-4方案,说简单了就是四个字节来表示一个字符,这样我们就可以组合出21亿个不同的字符出来(最高位有其他用途)。

由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的字符编码方式,采用4字节编码。UCS包含了已知语言的所有字符。除了拉丁语、希腊语、斯拉夫语、希伯来语、阿拉伯语、亚美尼亚语、格鲁吉亚语,还包括中文、日文、韩文这样的象形文字,UCS还包括大量的图形、印刷、数学、科学符号。

* UCS-2: 与Unicode的2byte编码基本一样;

* UCS-4: 4byte编码, 目前是在UCS-2前加上2个全0的byte。

ISO就直接规定必须用两个字节,也就是16位来统一表示所有的字符,对于ASCII里的那些“半角”字符,Unicode保持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于“半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。

Unicode 字符集收录了这世界上所有的文字符号和特殊符号。对于每一个符号都定义了一个值,称为代码点(code point)。代码点可以用2个字节表示(UCS-2),也可以用4个字节(UCS-4编码)。

Unicode在制订时没有考虑与任何一种现有的编码方案保持兼容,这使得GBK与Unicode在汉字的内码编排上完全是不一样的,没有一种简单的算术方法可以把文本内容从Unicode编码和另一种编码进行转换,这种转换必须通过查表来进行。

18. UTF编码

UCS编码虽然定义了每个代码点的编码方式,但是没规定如何传输和存储。比如,在UCS-2码中,英文符号是在ACSII码的前面加上一个0 byte,像“A”的ASCII码 0x41,在UCS码中就是0x0041,这样,对于英文系统来讲会出现大量的0 byte,造成不必要的浪费。而且容易存在对现在的ASCII码不兼容的问题。所以这个重担就落在了UTF编码身上。

于是面向传输的众多UTF(UCS Transfer Format)标准出现了。顾名思义,UTF8就是每次8个位传输数据,而UTF16就是每次16个位,只不过为了传输时的可靠性,从Unicode到UTF时并不是直接的对应,而是要通过一些算法和规则来转换。

(1)UTF-8(Unicode Transformation Format – 8-bit)

UTF-8是一种针对Unicode的可变长度字符编码(定长码),也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无需或只需做少部份修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。

UTF-8使用一至四个字节为每个字符编码:

* 128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。

*带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码(Unicode范围由U+0080至U+07FF)。

* 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字)使用三个字节编码。

*其他极少使用的Unicode辅助平面的字符使用四字节编码。

在处理经常会用到的ASCII字符方面非常有效。在处理扩展的拉丁字符集方面也不比UTF-16差。对于中文字符来说,比UTF-32要好。同时,UTF-8以字节为编码单元,由位操作的天性使然,使用UTF-8不再存在字节顺序的问题了。一份以utf-8编码的文档在不同的计算机之间是一样的比特流。

总体来说,在Unicode字符串中不可能由码点数量决定显示它所需要的长度,或者显示字符串之后在文本缓冲区中光标应该放置的位置;组合字符、变宽字体、不可打印字符和从右至左的文字都是其归因。所以尽管在UTF-8字符串中字符数量与码点数量的关系比UTF-32更为复杂,在实际中很少会遇到有不同的情形。

UTF-8的优点:

*UTF-8是ASCII的一个超集。因为一个纯ASCII字符串也是一个合法的UTF-8字符串,所以现存的ASCII文本不需要转换。为传统的扩展ASCII字符集设计的软件通常可以不经修改或很少修改就能与UTF-8一起使用。

*使用标准的面向字节的排序例程对UTF-8排序将产生与基于Unicode代码点排序相同的结果。(尽管这只有有限的有用性,因为在任何特定语言或文化下都不太可能有仍可接受的文字排列顺序。)

*UTF-8和UTF-16都是可扩展标记语言文档的标准编码。所有其它编码都必须通过显式或文本声明来指定。

*任何面向字节的字符串搜索算法都可以用于UTF-8的数据(只要输入仅由完整的UTF-8字符组成)。但是,对于包含字符记数的正则表达式或其它结构必须小心。

* UTF-8字符串可以由一个简单的算法可靠地识别出来。就是,一个字符串在任何其它编码中表现为合法的UTF-8的可能性很低,并随字符串长度增长而减小。举例说,字符值C0,C1,F5至FF从来没有出现。为了更好的可靠性,可以使用正则表达式来统计非法过长和替代值(可以查看W3 FAQ: Multilingual Forms上的验证UTF-8字符串的正则表达式)。

UTF-8的缺点:

* 因为每个字符使用不同数量的字节编码,所以寻找串中第N个字符是一个O(N)复杂度的操作,即,串越长,则需要更多的时间来定位特定的字符。同时,还需要位变换来把字符编码成字节,把字节解码成字符。

(2)UTF-16(Unicode Transformation Format – 16-bit)

UTF-16以两个字节为编码单元。在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。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。Windows就是使用BOM来标记文本文件的编码方式的。

UTF-16将0–65535范围内的字符编码成2个字节,如果真的需要表达那些很少使用的”星芒层(astral plane)”内超过这65535范围的Unicode字符,则需要使用一些诡异的技巧来实现。UTF-16编码最明显的优点是它在空间效率上比UTF-32高两倍,因为每个字符只需要2个字节来存储(除去65535范围以外的),而不是UTF-32中的4个字节。并且,如果我们假设某个字符串不包含任何星芒层中的字符,那么我们依然可以在常数时间内找到其中的第N个字符,直到它不成立为止这总是一个不错的推断。其编码方法是:

*如果字符编码U小于0x10000,也就是十进制的0到65535之内,则直接使用两字节表示;

* 如果字符编码U大于0x10000,由于UNICODE编码范围最大为0x10FFFF,从0x10000到0x10FFFF之间共有0xFFFFF个编码,也就是需要20个bit就可以标示这些编码。用U’表示从0-0xFFFFF之间的值,将其前 10 bit作为高位和16 bit的数值0xD800进行 逻辑or 操作,将后10 bit作为低位和0xDC00做逻辑or 操作,这样组成的 4个byte就构成了U的编码。

(3)UTF-32(Unicode Transformation Format – 32-bit)

使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph),每个数字代表唯一的至少在某种语言中使用的符号的编码方案,称为UTF-32。UTF-32又称UCS-4,是一种将Unicode字符编码的协定,对每个字符都使用4字节。就空间而言,是非常没有效率的。

但这种方法有其优点,最重要的一点就是可以在常数时间内定位字符串里的第N个字符,因为第N个字符从第4×Nth个字节开始。虽然每一个码位使用固定长定的字节看似方便,它并不如其它Unicode编码使用得广泛。

常用软件的默认字符集及其查看方法

(1)window下面保存记事本的文本字符集编码为:系统编码GBK;

(2)linux下面的默认字符集编码查看方法:/etc/sysconfig/i18n;

(3)利用cpdetector第三方包可以判断文件或者流的编码;

(4)查询oracle的默认字符集编码方法:select userenv(‘language’) from dual;

(5)早期操作系统的内码是与语言相关的。现在的Windows在内部统一使用Unicode,然后用代码页适应各种语言;

(6)C、C++、Python2内部字符串都是使用当前系统默认编码;

(7)Python3、Java内部字符串用Unicode保存;

(8)Ruby有一个内部变量$KCODE用来表示可识别的多字节字符串的编码,变量值为”EUC” “SJIS” “UTF8” “NONE”之一。$KCODE的值为”EUC”时,将假定字符串或正则表达式的编码为EUC-JP。同样地,若为”SJIS”时则认定为Shift JIS。若为”UTF8″时则认定为UTF-8。若为”NONE”时,将不会识别多字节字符串。在向该变量赋值时,只有第1个字节起作用,且不区分大小写字母。”e” “E” 代表 “EUC”,”s” “S” 代表 “SJIS”,”u” “U” 代表 “UTF8″,而”n” “N” 则代表 “NONE”。默认值为”NONE”。即默认情况下Ruby把字符串当成单字节序列来处理;

(9)ultraedit在默认情况下是把utf-8转换为unicode编码,你看到的是unicode编码,

如果你想看到真正的utf-8编码,那么在ultraedit中,做如下操作

File—>conversions—>unicode/asc2/utf-8 to utf-8(asc2 editing)

关于字符集及字符编码的问题这次先总结到这里,下次将对Java等字符编码进行进一步分析。

参考资料

(1)维基百科-字符编码

(2)《计算机编码知识——区位码、国标码、机内码、输入码、字形码》

(3)《计算机内码与外码的区别》

(4)《和荣笔记- GB2312 字符集与编码对照表》

(5)《说说字符集和编码》

(6)《编码简介》

(7)《深入了解字符集和编码》

(8)吴秦 《字符集和字符编码(Charset & Encoding)》

Leave a Reply

Your email address will not be published. Required fields are marked *