摘自http://www.cnblogs.com/xkfz007/articles/2566434.html javascript
编码问题的例子html
在windows自带的notepad(记事本)程序中输入“联通”两个字,保存后再次打开,会发现“联通”不见了,代之以“��ͨ”的乱码。这是windows平台上典型的中文编码问题。即文件保存的时候是按照ANSI编码(其实就是GB2312,后面会详细介绍)保存,打开的时候程序按照UTF-8方式对内容解释,因而就出现了乱码。避免乱码的方式很简单,在“文件”菜单中选择“打开”命令,选择保存的文件,而后选择“ANSI”编码, 此时就能看到久违的“联通”两个字了。java
在Linux平台上若是使用cat等命令查看文件中的中文内容时,可能出现乱码。这也是编码的问题。简单的说是文件时按照A编码保存,可是cat命令按照当前Locale设定的B编码去查看,在B和A不兼容的时候就出现了乱码。node
中文编码因为历史缘由牵扯到很多标准,在不了解的时候感受一头雾水;但其实理解编码问题并不须要你深刻了解各个编码标准,只要你明白了前因后果,了解了关键的知识点,就能分析和解决平常开发工做中碰到的大部分编码问题。有感于我看过的资料和文章要么不够全面,要么略显枯燥,因此经过这篇文章记录下笔者在平常工做中碰到的中文编码原理相关问题,目的主要是自我总结,若是能给读者提供一些帮助那就算是意外之喜了。因为严谨的编码标准对我来讲是无趣的,枯燥的,难以记忆的,本文尝试用浅显易懂的生活语言解释中文编码相关的(也可能不相关的)一些问题,这也是为何取名杂谈的缘由。本文确定存在不规范不全面的地方,我会在参考资料里给出官方文档的连接,也欢迎读者在评论中提出更好的表达方式&指出错误,不胜感激。python
对编码问题的理解我认为分为三个层次,第一个层次:概念,知道各个编码标准的应用场景,了解之间的差别,能分析和解决常见的一些编码问题。 第二个层次:标准,掌握编码的细节,如编码范围,编码转换规则,知道这些就能自行开发编码转换工具。第三个层次,使用,了解中文的编码2进制存储,在程序开发过程当中选择合理的编码并处理中文。为了不让读者陷入编码标准的黑洞没法脱身(不相信?看看unicode的规范就明白个人意思了),同时因为编码查询&转换工具等都有现成工具可使用,本文只涉及第一个层次,不涉及第二层次,在第三层次上会作一些尝试。在本文的最后提供了相关连接供对标准细 节感兴趣的同窗继续学习。最后,本文不涉及具体软件的乱码问题解决,如ssh,shell,vim,screen等,这些话题留给剑豪同窗专文阐述。linux
电脑很聪明,能够帮咱们作不少事情,最开始主要是科学计算,这也是为何电脑别名计算机。电脑又很笨,在她的脑子里只有数字,即全部的数据 在存储和运算时都要使用二进制数表示。这在最初电脑主要用来处理大量复杂的科学计算时不是什么大问题可是当电脑逐步走入普通人的生活时,状况开始变遭了。 办公自动化等领域最主要的需求就是文字处理,电脑如何来表示文字呢?这个问题固然难不倒聪明的计算机科学家们,用数字来表明字符呗。这就是“编码”。shell
每一个人均可以约定本身的一套编码,只要使用方之间了解就ok了。好比说咱俩约定0×10表示a,0×11表示b。在一开始也的确是这样的, 出现了各式各样的编码。这样有两个问题:1.各个编码的字符集不同,有的多,有的少。2.相同字符的编码也不同。你这里a是0×10.他那里a多是 0×30。因而你保存的文件他就不能直接用,必需要转换编码。随着沟通范围的扩大,采用不一样编码的人们互相通讯就乱套了,这就是咱们常说的:鸡同鸭讲。若是要避免这种混乱,那么你们就必须使用相同的编码规则,因而美国有关的标准化组织就出台了ASCII( American Standard Code for Information Interchange )编码,统一规定了英文经常使用符号用哪些二进制数来表示。ASCII是标准的单字节字符编码方案,用于基于文本的数据。vim
ASCII最初是美国国家标准,供不一样计算机在相互通讯时用做共同遵照的西文字符编码标准,已被国际标准化组织 (International Organization for Standardization, ISO)定为国际标准,称为ISO 646标准。适用于全部拉丁文字字母。ASCII 码使用指定的7 位或8 位二进制数组合来表示128 或256 种可能的字符。标准ASCII 码也叫基础ASCII码,使用7 位二进制数来表示全部的大写和小写字母,数字0到九、标点符号, 以及在美式英语中使用的特殊控制字符。而最高位为1的另128个字符(80H—FFH)被称为“扩展ASCII”,通常用来存放英文的制表符、部分音标字 符等等的一些其它符号。windows
其中: 0 ~ 31 及 127( 共 33 个 ) 是控制字符或通讯专用字符(其他为可显示字符),32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字,65~90为26个大写英文字母,97~122号为26个小写英文字母,其他为一些标点符号、运算符号等。数组
如今全部使用英文的电脑终于能够用同一种编码来交流了。理解了ASCII编码,其余字母型的语言编码方案就举一反三了。
ASCII这种字符编码规则显然用来处理英文没有什么问题,它的出现极大的促进了信息在西方尤为是美国的传播和交流。可是对于中文,经常使用汉字就有6000以上,ASCII 单字节编码显然是不够用。为了粉碎美帝国主义经过编码限制中国人民使用电脑的无耻阴谋,中国国家标准总局发布了GB2312码即中华人民共和国国家汉字信息交换用编码,全称《信息交换用汉字编码字符集——基本集》,1981年5月1日实施,通行于大陆。GB2312字符集中除经常使用简体汉字字符外还包括希腊字母、日文平假名及片假名字母、俄语西里尔字母等字符,未收录繁体中文汉字和一些生僻字。 EUC-CN能够理解为GB2312的别名,和GB2312彻底相同。
GB2312是基于区位码设计的,在区位码的区号和位号上分别加上A0H就获得了GB2312编码。这里第一次提到了“区位码”,我就连带把下面这几个让人摸不到头脑的XX码一锅端了吧:
区位码,国标码,交换码,内码,外码
区位码:就是把中文经常使用的符号,数字,汉字等分门别类进行编码。区位码把编码表分为94个区,每一个区对应94个位,每一个位置就放一个字符(汉字,符号,数字都属于字符)。这样每一个字符的区号和位号组合起来就成为该汉字的区位码。区位码通常用10进制数来表示,如4907就表示49区7位,对应的字符是“学”。区位码中01-09区是符号、数字区,16-87区是汉字区,10-15和88-94是未定义的空白区。它将收录的汉字分红两级:第一级是经常使用汉字计3755个,置于16-55区,按汉语拼音字母/笔形顺序排列;第二级汉字是次经常使用汉字计3008个, 置于56-87区,按部首/笔画顺序排列。在网上搜索“区位码查询系统”能够很方便的找到汉字和对应区位码转换的工具。为了不广告嫌疑和死链,这里就不举例了。
国标码: 区位码没法用于汉字通讯,由于它可能与通讯使用的控制码(00H~1FH)(即0~31,还记得ASCII码特殊字符的范围吗?)发生冲突。因而ISO2022规定每一个汉字的区号和位号必须分别加上32(即二进制数00100000,16进制 20H),获得对应的国标交换码,简称 国标码,交换码,所以,“学”字的国标交换码计算为:
00110001 00000111
+ 00100000 00100000
-------------------
01010001 00100111
用十六进制数表示即为5127H。
交换码:即国标交换码的简称,等同上面说的国标码。
内码:因为文本中一般混合使用汉字和西文字符,汉字信息若是不予以特别标识,就会与单字节的ASCII 码混淆。此问题的解决方法之一是将一个汉字当作是两个扩展ASCII码,使表示GB2312汉字的两个字节的最高位都为1。即国标码加上128(即二进制 数10000000,16进制80H)这种高位为1的双字节汉字编码即为GB2312汉字的机内码,简称为内码。20H+80H=A0H。这也就是常说的 在区位码的区号和位号上分别加上A0H就获得了GB2312编码的由来。
00110001 00000111
+ 10100000 10100000
-------------------
11010001 10100111
用十六进制数表示即为D1A7H。
外码:机外码的简称,就是汉字输入码,是为了经过键盘字符把汉字输入计算机而设计的一种编码。 英文输入时,相输入什么字符便按什么键,外码和内码一致。汉字输入时,可能要按几个键才能输入一个汉字。 汉字输入方案有成百上千个,可是这千差万别的外码输入进计算机后都会转换成统一的内码。
最后总结一下上面的概念。中国国家标准总局把中文经常使用字符编码为94个区,每一个区对应94个位,每一个字符的区号和位号组合起来就是该字符的 区位码, 区位码用10进制数来表示,如4907就表示49区7位,对应的字符是“学”。 因为区位码的取值范围与通讯使用的控制码(00H~1FH)(即0~31)发生冲突。每一个汉字的区号和位号分别加上32(即16进制20H)获得 国标码,交换码。“学”的国标码为5127H。因为文本中一般混合使用汉字和西文字符,为了让汉字信息不会与单字节的ASCII码混淆,将一个汉字当作是两个扩展ASCII码,即汉字的两个字节的最高位置为1,获得的编码为GB2312汉字的 内码。“学”的内码为D1A7H。不管你使用什么输入法,经过什么样的按键组合把“学”输入计算机,“学”在使用GB2312(以及兼容GB2312)编码的计算机里的内码都是D1A7H。
GB2312的出现基本知足了汉字的计算机处理须要,但因为上面提到未收录繁体字和生僻字,从而不能处理人名、古汉语等方面出现的罕用字, 这致使了1995年《汉字编码扩展规范》(GBK)的出现。GBK编码是GB2312编码的超集,向下彻底兼容GB2312,兼容的含义是不只字符兼容, 并且相同字符的编码也相同,同时在字汇一级支持ISO/IEC10646—1和GB 13000—1的所有中、日、韩(CJK)汉字,共计20902字。GBK还收录了GB2312不包含的汉字部首符号、竖排标点符号等字符。CP936和 GBK的有些许差异,绝大多数状况下能够把CP936看成GBK的别名。
GB18030编码向下兼容GBK和GB2312。GB18030收录了全部Unicode3.1中的字符,包括中国少数民族字符,GBK不支持的韩文字符等等,也能够说是世界大多民族的文字符号都被收录在内。GBK和GB2312都是双字节等宽编码,若是算上和ASCII兼容所支持的单字节,也能够理解为是单字节和双字节混合的变长编码。GB18030编码是变长编码,有单字节、双字节和四字节三种方式。
其实,这三个标准并不须要死记硬背,只须要了解是根据应用需求不断扩展编码范围便可。从GB2312到GBK再到GB18030收录的字符 愈来愈多便可。万幸的是一直是向下兼容的,也就是说一个汉字在这三个编码标准里的编码是如出一辙的。这些编码的共性是变长编码,单字节ASCII兼容,对其余字符GB2312和GBK都使用双字节等宽编码,只有GB18030还有四字节编码的方式。这些编码最大的问题是2个。1.因为低字节的编码范围和 ASCII有重合,因此不能根据一个字节的内容判断是中文的一部分仍是一个独立的英文字符。2.若是有两个汉字编码为A1A2B1B2,存在A2B1也是一个有效汉字编码的特殊状况。这样就不能直接使用标准的字符串匹配函数来判断一个字符串里是否包含某一个汉字,而须要先判断字符边界而后才能进行字符匹配判断。
最后,提一个小插曲,上面讲的都是大陆推行的汉字编码标准,使用繁体的中文社群中最经常使用的电脑 汉字 字符集标准叫大五码(Big5),共收录13,060个中文字,其中有二字为重覆编码(实在是不该该)。Big5虽普及于中国的台湾、香港与澳门等繁体中文通行区,但长期以来并不是当地的国家标准,而只是业界标准。倚天中文系统、Windows等主要系统的字符集都是以Big5为基准,但厂商又各自增删,衍生成多种不一样版本。2003年,Big5被收录到台湾官方标准的附录当中,取得了较正式的地位。这个最新版本被称为Big5-2003。
看了上面的多个中文编码是否是有点头晕了呢?若是把这个问题放到全世界n多个国家n多语种呢?各国和各地区本身的文字编码规则互相冲突的状况全球信息 交换带来了很大的麻烦。
要真正完全解决这个问题,上面介绍的那些经过扩展ASCII修修补补的方式已经走不通了,而必须有一个全新的编码系统,这个系统要能够将中 文、日文、法文、德文……等等全部的文字统一块儿来考虑,为每个文字都分配一个单独的编码。因而,Unicode诞生了。Unicode(统一码、万国 码、单一码)为地球上(之后会包括火星,金星,喵星等)每种语言中的每一个字符设定了统一而且惟一的二进制编码,以知足跨语言、跨平台进行文本转换、处理的 要求。在Unicode里,全部的字符被一视同仁,汉字再也不使用“两个扩展ASCII”,而是使用“1个Unicode”来表示,也就是说,全部的文字都 按一个字符来处理,它们都有一个惟一的Unicode码。Unicode用数字0-0x10FFFF来映射这些字符,最多能够容纳1114112个字符, 或者说有1114112个码位(码位就是能够分配给字符的数字)。
提到Unicode不能不提UCS(通用字符集Universal Character Set)。UCS是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。UCS-2用两个字节编码,UCS-4用4个字节编码。Unicode是由unicode.org制定的编码机 制,ISO与unicode.org是两个不一样的组织, 虽然最初制定了不一样的标准; 但目标是一致的。因此自从unicode2.0开始, unicode采用了与ISO 10646-1相同的字库和字码, ISO也承诺ISO10646将不会给超出0x10FFFF的UCS-4编码赋值, 使得二者保持一致。你们简单认为UCS等同于Unicode就能够了。
在Unicode中:汉字“字”对应的数字是23383。在Unicode中,咱们有不少方式将数字23383表示成程序中的数据,包 括:UTF-八、UTF-1六、UTF-32。UTF是“UCS Transformation Format”的缩写,能够翻译成Unicode字符集转换格式,即怎样将Unicode定义的数字转换成程序数据。例如,“汉字”对应的数字是 0x6c49和0x5b57,而编码的程序数据是:
BYTE data_utf8[] = {0xE6, 0xB1, 0x89, 0xE5, 0xAD, 0x97}; // UTF-8编码
WORD data_utf16[] = {0x6c49, 0x5b57}; // UTF-16编码
DWORD data_utf32[] = {0x6c49, 0x5b57}; // UTF-32编码
这里用BYTE、WORD、DWORD分别表示无符号8位整数,无符号16位整数和无符号32位整数。UTF-八、UTF-1六、UTF- 32分别以BYTE、WORD、DWORD做为编码单位。“汉字”的UTF-8编码须要6个字节。“汉字”的UTF-16编码须要两个WORD,大小是4 个字节。“汉字”的UTF-32编码须要两个DWORD,大小是8个字节。根据字节序的不一样,UTF-16能够被实现为UTF-16LE或UTF- 16BE,UTF-32能够被实现为UTF-32LE或UTF-32BE。
下面介绍UTF-八、UTF-1六、UTF-3二、BOM。
UTF-8
UTF-8以字节为单位对Unicode进行编码。从Unicode到UTF-8的编码方式以下:
Unicode编码(16进制) | UTF-8字节流(二进制) |
000000 - 00007F | 0xxxxxxx |
000080 - 0007FF | 110xxxxx 10xxxxxx |
000800 - 00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000 - 10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-8的特色是对不一样范围的字符使用不一样长度的编码。对于0×00-0x7F之间的字符,UTF-8编码与ASCII编码彻底相同。 UTF-8编码的最大长度是4个字节。从上表能够看出,4字节模板有21个x,便可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位。总结了一下规律:UTF-8的第一个字节开始的1的个数表明了总的编码字节数,后续字节都是以10开始。由上面的规则能够清晰的看出UTF-8编码克服了中文编码的两个问题。
例1:“汉”字的Unicode编码是0x6C49。0x6C49在0×0800-0xFFFF之间,使用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,获得:11100110 10110001 10001001,即E6 B1 89。
例2:Unicode编码0x20C30在0×010000-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-16
UTF-16编码以16位无符号整数为单位。咱们把Unicode编码记做U。编码规则以下:若是U<0×10000,U的 UTF-16编码就是U对应的16位无符号整数(为书写简便,下文将16位无符号整数记做WORD)。中文范围 4E00-9FBF,因此在UTF-16编码里中文2个字节编码。若是U≥0×10000,咱们先计算U’=U-0×10000,而后将U’写成二进制形 式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
UTF-32
UTF-32编码以32位无符号整数为单位。Unicode的UTF-32编码就是其对应的32位无符号整数。
字节序
根据字节序(对字节序不太了解的同窗请参考 http://en.wikipedia.org/wiki/Endianness)的不一样,UTF-16能够被实现为UTF-16LE(Little Endian)或UTF-16BE(Big Endian),UTF-32能够被实现为UTF-32LE或UTF-32BE。例如:
Unicode编码 | UTF-16LE | UTF-16BE | UTF-32LE | UTF-32BE |
0x006C49 | 49 6C | 6C 49 | 49 6C 00 00 | 00 00 6C 49 |
0x020C30 | 43 D8 30 DC | D8 43 DC 30 | 30 0C 02 00 | 00 02 0C 30 |
那么,怎么判断字节流的字节序呢?Unicode标准建议用BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被做为BOM的字符”零宽无中断空格”。这个字符的编码是FEFF,而反过来的FFFE(UTF- 16)和FFFE0000(UTF-32)在Unicode中都是未定义的码位,不该该出如今实际传输中。下表是各类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 |
总结一下,ISO与unicode.org都敏锐的意识到只有为世界上每种语言中的每一个字符设定统一而且惟一的二进制编码才能完全解决计算机世界信息交流中编码冲突的问题。由此诞生了UCS和unicode,而这两个规范是一致的。在Unicode里,全部的字符被一视同仁,也就是说,全部的文字都按一个字符来处理,它们都有一个惟一的Unicode码。UTF-八、UTF-1六、UTF-32分别定义了怎样将Unicode定义的数字转换成程序数据。UTF-8以字节为单位对Unicode进行编码,一个英文字符占1个字节,汉字占3个字节;UTF-16以16位无符号整数为单位对 Unicode进行编码,中文英文都占2个字节;UTF-32以32位无符号整数为单位对Unicode进行编码,中文英文都占4个字节。能够在 http://www.unicode.org/charts/unihan.html查看汉字的unicode码以及UTF-八、UTF-1六、UTF-32编码。
介绍了这么多的编码知识,真正的文件内容是什么样子的呢?下面咱们就经过实验看看在笔者Linux机器上 “中文”这两个字在不一样的编码下保存的文件内容。下面是个人实验过程,有兴趣的同窗能够在本身的机器上重作一下。window平台上的状况相似这里就不赘述了。
实验须要须要使用2个工具:
汉字 | Unicode(ucs-2)10进制表示 | Utf-8 | Utf-16 | Utf32 | 区位码 | GB2312/GBK/GB18030 |
中 | 20013 | E4 B8 AD | 4E2D | 00004E2D | 5448 | D6D0 |
文 | 25991 | E6 96 87 | 6587 | 00006587 | 4636 | CEC4 |
机器环境:
os: Red Hat Enterprise Linux AS release 4
Cpu: Intel(R) Xeon(R) CPU
locale:LC_ALL=zh_CN.utf-8
先明确一个概念:程序内部编码和程序外部编码。程序内部编码指的是中文字符在程序运行时在内存中的编码形式。程序外部编码则是中文字符在存储或者传输时的编码形式。程序外部编码的最直观的例子就是当把中文存储到硬盘文件中时选择的编码。
根据程序内部编码和程序外部编码是否一致,C/C++的中文处理有两种常见的方式:
方法1的优势不言而喻,因为内外统一,不须要进行转换。不足是若是不是C标准库支持的编码方式,那么字符串处理函数须要本身实现。好比说标准strlen函数不能计算中文编码&UTF-8等的字符串长度,而须要根据编码标准自行实现。GBK等中文编码除了计算字符串长度的函数外,字符串匹配函数也要本身实现(缘由看上文中文编码总结)。当须要支持的编码格式不断增多时,处理函数的开发和维护就须要付出更大的代价。
方法2针对方法1的不足加以改进。在程序内部能够优先选择C标准库支持的编码方式,或者根据须要本身实现对某一特定编码格式的完整支持,这样任何编码均可以先转换为支持的编码,代码通用性比较好。
那么C标准库对中文编码的支持如何呢?目前Linux平台通常使用GNU C library,内建了对单字节的char和宽字符wchar_t的支持。char你们都很熟悉了,处理中文须要的wchar_t要重点介绍一下。从实现上来讲在linux平台上能够认为wchar_t是4byte的int,内部存储字符的UTF32编码。因为标准库已经内建了对wchar_t比较完备的支持,如使用wcslen计算字符串长度,使用wcscmp进行字符串比较等等。因此比较简单的方式是使用上面的方法2,同时选择wchar_t做为内部字符的表示。作到这一点仍是比较容易的,在输入输出的时候经过mbrtowc/wcrtomb进行单个字符的内外编码转换,以及经过mbsrtowcs/wcsrtombs进行字符串的内外编码转换便可。这里须要注意两点:
关于locale的话题比较大,这里就不深刻了,留待下一篇文章吧.
上面的方法很完美,是吗?不是吗?获得这么多的好处不是无代价的,最明显的代价就是内存,任何一个字符,无论中文仍是英文若是保持在wchar_t里就须要4个byte,就这一个理由就足以限制了这个方案在关注内存使用的应用场景下的使用。
对Python来讲因为内建unicde的支持,因此采用输入输出的时候进行转换,内部保持unicode的方式使用是个不错的方案。 http://docs.python.org/tutorial/introduction.html#unicode-strings这里做为起点,有兴趣的同窗自学吧。