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

原文转自我的个人博客——温馨咖啡小屋

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

字符编码与字符集

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

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

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

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

字符编码举例

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

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

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

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 字符集的区位分布表:

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将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)》

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2014年9月22日
下一篇 2014年9月22日

相关推荐