字符编码

参考博客:

1、字符集和字符编码(Charset & Encoding)

2、 字符常见的几种编码方式

3、字符编码详解——彻底理解掌握编码知识,“乱码”不复存在

 

无论是做客户端还是服务器,无论是做网页还是做游戏,可能都存在一个绕不开的问题----乱码

想搞清楚为什么乱码,首先得从编码说起。

1、*础知识

计算机中储存的信息都是用二进制数表示的;而我们在屏幕上看到的英文、汉字等字符是二进制数转换之后的结果。通俗的说,按照何种规则将字符存储在计算机中,如'a'用什么表示,称为"编码";反之,将存储在计算机中的二进制数解析显示出来,称为"解码",如同密码学中的加密和解密。在解码过程中,如果使用了错误的解码规则,则导致'a'解析成'b'或者乱码。

字符集(Charset:是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。

字符编码(Character Encoding:是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。即在符号集合与数字系统之间建立对应关系,它是信息处理的一项*本技术。通常人们用符号集合(一般情况下就是文字)来表达信息。而以计算机为*础的信息处理系统则是利用元件(硬件)不同状态的组合来存储和处理信息的。元件不同状态的组合能代表数字系统的数字,因此字符编码就是将符号转换为计算机可以接受的数字系统的数,称为数字代码。简单来说,就是按照某种规则,用一串二进制来表示一个字符。Unicode 标准始终使用十六进制数字,而且在书写时在前面加上前缀“U+”,所以“A”的编码书写为“U+0041”

2、编码的演化

常见的一些字符编码方式无非有:Unicode、ASCII、GBK、GB2312、UTF-8。下面先对常见的这一些字符编码方式作下说明:

1.ASCII

这是美国在19世纪60年代的时候为了建立英文字符和二进制的关系时制定的编码规范,它能表示128个字符,其中包括英文字符、阿拉伯数字、西文字符以及32个控制字符。它用一个字节来表示具体的字符,但它只用后7位来表示字符(2^7=128),最前面的一位统一规定为0。

2.扩展的ASCII

原本的ASCII码对于英文语言的国家是够用了,但是欧洲国家的一些语言会有拼音,这时7个字节就不够用了。因此一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使 用的编码体系,可以表示最多256个符号。但这时问题也出现了:不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码 中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。这个问题就直接促使了Unicode编码的产生。

2.1 编码标准:ISO-8859-1  

ISO-8859-1编码是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
此字符集支持部分于欧洲使用的语言,包括阿尔巴尼亚语巴斯克语布列塔尼语、加泰罗尼亚语、丹麦语、荷兰语法罗语弗里西语加利西亚语、德语、格陵兰语冰岛语爱尔兰盖尔语、意大利语、拉丁语、卢森堡语挪威语、葡萄牙语、里托罗曼斯语、苏格兰盖尔语、西班牙语及瑞典语。
英语虽然没有重音字母,但仍会标明为ISO/IEC 8859-1编码。除此之外,欧洲以外的部分语言,如南非荷兰语斯瓦希里语印尼语马来语、菲律宾他加洛语等也可使用ISO/IEC 8859-1编码。
法语及芬兰语本来也使用ISO/IEC 8859-1来表示。但因它没有法语使用的 œŒ、Ÿ 三个字母及芬兰语使用的 Š、š、Ž、ž ,故于1998年被ISO/IEC 8859-15所取代。(ISO 8859-15同时加入了欧元符号)

3. GBXXXX字符

为了显示中文,必须设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数。

天朝专家把那些127号之后的奇异符号们(即EASCII)取消掉,规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。

上述编码规则就是GB2312GB2312GB2312-80中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·*本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。GB2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB2312。GB2312的出现,*本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。对于人名古汉语等方面出现的罕用字,GB2312不能处理,这导致了后来GBKGB 18030汉字字符集的出现。

由于GB 2312-80只收录6763个汉字,有不少汉字,如部分在GB 2312-80推出以后才简化的汉字(如"啰"),部分人名用字(如中国前总理***的"*"字),台湾及香港使用的繁体字日语朝鲜语汉字等,并未有收录在内。于是厂商微软利用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。

GB 18030,全称:国家标准GB 18030-2005《信息技术 中文编码字符集》,是中华人民共和国现时最新的内码字集,是GB 18030-2000《信息技术 信息交换用汉字编码字符集 *本集的扩充》的修订版。与GB 2312-1980完全兼容,与GBK*本兼容,支持GB 13000Unicode的全部统一汉字,共收录汉字70244个。GB 18030主要有以下特点:

    • UTF-8相同,采用多字节编码,每个字可以由1个、2个或4个字节组成。
    • 编码空间庞大,最多可定义161万个字符。
    • 支持中国国内少数民族的文字,不需要动用造字区。
    • 汉字收录范围包含繁体汉字以及日韩汉字

4. BIG5字符集

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

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

0x8140-0xA0FE

保留给用户自定义字符(造字区)

0xA140-0xA3BF

标点符号希腊字母及特殊符号,包括在0xA259-0xA261,安放了九个计量用汉字:兙兛兞兝兡兣嗧瓩糎。

0xA3C0-0xA3FE

保留。此区没有开放作造字区用。

0xA440-0xC67E

常用汉字,先按笔划再按部首排序。

0xC6A1-0xC8FE

保留给用户自定义字符(造字区)

0xC940-0xF9D5

次常用汉字,亦是先按笔划再按部首排序。

0xF9D6-0xFEFE

保留给用户自定义字符(造字区)

 

【插录】. ANSI编码

  ANSI编码区别于上面的任何一种。因为,它的含义是【编码标准】。受制于当时的条件,每个国家都制定了一套自己的标准,所以同一范围,可能表示的是不同意思。

ANSI编码是一种对ASCII码的拓展:ANSI编码用0x00~0x7f (即十进制下的0到127)范围的1 个字节来表示 1 个英文字符,超出一个字节的 0x80~0xFFFF 范围来表示其他语言的其他字符。也就是说,ANSI码仅在前128(0-127)个与ASCII码相同,之后的字符全是某个国家语言的所有字符。

值得注意的是,两个字节最多可以存储的字符数目是2的16次方,即65536个字符,这对于一个语言的字符来说,绝对够了。还有ANSI编码其实包括很多编码:中国制定了GB2312编码,用来把中文编进去另外,日本把日文编到Shift_JIS里,韩国把韩文编到Euc-kr里,各国有各国的标准。受制于当时的条件,不同语言之间的ANSI码之间不能互相转换,这就会导致在多语言混合的文本中会有乱码。也因此,Unicode编码字符集才横空出世了。

5.Unicode符号集

像天朝一样,当计算机传到世界各个国家时,为了适合当地语言和字符,设计和实现类似GB232/GBK/GB18030/BIG5的编码方案。这样各搞一套,在世界上于是存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。为了解决这个问题,一个伟大的创想产生了——Unicode。Unicode编码系统为表达任意语言的任意字符而设计。因此Unicode就是这样一种编码:它包含了世界上所有的符号,并且每一个符号都是独一无二的。比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表 。很多人都说Unicode编码,但其实Unicode是一个符号集(世界上所有符号的符号集),而不是一种新的编码方式。

但是正因为Unicode包含了所有的字符,而有些国家的字符用一个字节便可以表示,而有些国家的字符要用多个字节才能表示出来。即产生了两个问题:第一,如果有两个字节的数据,那计算机怎么知道这两个字节是表示一个汉字呢?还是表示两个英文字母呢?第二,因为不同字符需要的存储长度不一样,那么如果Unicode规定用2个字节存储字符,那么英文字符存储时前面1个字节都是0,这就大大浪费了存储空间。

上面两个问题造成的结果是:1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。2)unicode在很长一段时间内无法推广,直到互联网的出现。

6.UTF-X

可以这样理解:Unicode是字符集,UTF-32/ UTF-16/ UTF-8是三种字符编码方案。

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上*本不用。重复一遍,这里的关系是,UTF-8Unicode的实现方式之一。

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8的编码规则很简单,只有两条:

 1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

一字节:0*******

两字节:110*****,10******

三字节:1110****,10******,10******

四字节:11110***,10******,10******,10******

就是这四种情况。相信你已经知道我们怎么来判断一个字是几字节的了。规则就是:

如果是以0开头的,那么他就是一个1字节编码,取到他一字节的数据去一字节表中找就OK了。

如果是以110开头的,那么他就是一个2字节编码,取到他两字节的数据去两字节表中找就OK了,而且他的第二个字节一定要是10开头,不然就是乱码了。

后面类推。

核心之处就是把0,10,110,1110,11110这五种比特的情况用在不同的位置而区分开各种编码。这在TCP/IP网络协议中也有很多类似的用法。比如几类IP地址的划分。下面这个图就是说这个:

   

7、Unicode与UTF

  Unicode是内存编码表示方案(是规范),而UTF是如何保存和传输Unicode的方案(是实现)。
 
  7.1 UTF的字节序和BOM
  7.1.1 字节序
  
UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?
 
Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一个有点小聪明的想法:
 
在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。
 
UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF(读者可以用我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。
 
7.1.2 BOM
 
(1)BOM的来历 
 
为了识别 Unicode 文件,Microsoft 建议所有的 Unicode 文件应该以 ZERO WIDTH NOBREAK SPACE(U+FEFF)字符开头。这作为一个“特征符”或“字节顺序标记(byte-order mark,BOM)”来识别文件中使用的编码和字节顺序。
 
(2)不同的系统对BOM的支持 
 
因为一些系统或程序不支持BOM,因此带有BOM的Unicode文件有时会带来一些问题。
 
①JDK1.5以及之前的Reader都不能处理带有BOM的UTF-8编码的文件,解析这种格式的xml文件时,会抛出异常:Content is not allowed in prolog。“对于解决方法,之后我会写篇文章专门讨论该问题。”
②Linux/UNIX 并没有使用 BOM,因为它会破坏现有的 ASCII 文件的语法约定。
③不同的编辑工具对BOM的处理也各不相同。使用Windows自带的记事本将文件保存为UTF-8编码的时候,记事本会自动在文件开头插入BOM(虽然BOM对UTF-8来说并不是必须的)。而其它很多编辑器用不用BOM是可以选择的。UTF-8、UTF-16都是如此。
 
(3)BOM与XML 
 
XML解析读取XML文档时,W3C定义了3条规则: 
①如果文档中有BOM,就定义了文件编码;
②如果文档中没有BOM,就查看XML声明中的编码属性;
③如果上述两者都没有,就假定XML文档采用UTF-8编码。
 
7.2 决定文本的字符集与编码
 
软件通常有三种途径来决定文本的字符集和编码。
 
(1)对于Unicode文本最标准的途径是检测文本最开头的几个字节。如:
 
开头字节        Charset/encoding
 EF BB BF    UTF-8
 FE FF     UTF-16/UCS-2, little endian(UTF-16LE)
 FF FE     UTF-16/UCS-2, big endian(UTF-16BE)
 FF FE 00 00  UTF-32/UCS-4, little endian.
 00 00 FE FF  UTF-32/UCS-4, big-endia
 
(2)采取一种比较安全的方式来决定字符集及其编码,那就是弹出一个对话框来请示用户。
 
然而MBCS文本(ANSI)没有这些位于开头的字符集标记,现在很多软件保存文本为Unicode时,可以选择是否保存这些位于开头的字符集标记。因此,软件不应该依赖于这种途径。这时,软件可以采取一种比较安全的方式来决定字符集及其编码,那就是弹出一个对话框来请示用户。
 
(3)采取自己“猜”的方法。
 
如果软件不想麻烦用户,或者它不方便向用户请示,那它只能采取自己“猜”的方法,软件可以根据整个文本的特征来猜测它可能属于哪个charset,这就很可能不准了。使用记事本打开那个“联通”文件就属于这种情况。(把原本属于ANSI编码的文件当成UTF-8处理,详细说明见:http://blog.csdn.net/omohe/archive/2007/05/29/1630186.aspx)
 
7.3 记事本的几种编码
 
(1)ANSI编码 
记事本默认保存的编码格式是:ANSI,即本地操作系统默认的内码,简体中文一般为GB2312。这个怎么验证呢?用记事本保存后,使用EmEditor、EditPlus和UltraEdit之类的文本编辑器打开。推荐使用EmEditor,打开后,在又下角会显示编码:GB2312。
 
(2)Unicode编码 
用记事本另存为时,编码选择“Unicode”,用EmEditor打开该文件,发现编码格式是:UTF-16LE+BOM(有签名)。用十六进制方式查看,发现开头两字节为:FF FE。这就是BOM。
 
(3)Unicode big endian 
用记事本另存为时,编码选择“Unicode”,用EmEditor打开该文件,发现编码格式是:UTF-16BE+BOM(有签名)。用十六进制方式查看,发现开头两字节为:FE FF。这就是BOM。
 
(4)UTF-8 
记事本另存为时,编码选择“UTF-8”,用EmEditor打开该文件,发现编码格式是:UTF-8(有签名)。用十六进制方式查看,发现开头三个字节为:EF BB BF。这就是BOM。

8、几种误解,以及乱码产生的原因和解决办法

8.1 误解一
 
在将“字节串”转化成“UNICODE 字符串”时,比如在读取文本文件时,或者通过网络传输文本时,容易将“字节串”简单地作为单字节字符串,采用每“一个字节”就是“一个字符”的方法进行转化。 
而实际上,在非英文的环境中,应该将“字节串”作为 ANSI 字符串,采用适当的编码来得到 UNICODE 字符串,有可能“多个字节”才能得到“一个字符”。
通常,一直在英文环境下做开发的程序员们,容易有这种误解。
 
8.2 误解二
 
在 DOS,Windows 98 等非 UNICODE 环境下,字符串都是以 ANSI 编码的字节形式存在的。这种以字节形式存在的字符串,必须知道是哪种编码才能被正确地使用。这使我们形成了一个惯性思维:“字符串的编码”
当 UNICODE 被支持后,Java 中的 String 是以字符的“序号”来存储的,不是以“某种编码的字节”来存储的,因此已经不存在“字符串的编码”这个概念了。只有在“字符串”与“字节串”转化时,或者,将一个“字节串”当成一个 ANSI 字符串时,才有编码的概念。 
不少的人都有这个误解。
 
8.3 分析与解决
 
第一种误解,往往是导致乱码产生的原因。第二种误解,往往导致本来容易纠正的乱码问题变得更复杂。
 
在这里,我们可以看到,其中所讲的“误解一”,即采用每“一个字节”就是“一个字符”的转化方法,实际上也就等同于采用 iso-8859-1 进行转化。因此,我们常常使用 bytes = string.getBytes("iso-8859-1") 来进行逆向操作,得到原始的“字节串”。然后再使用正确的 ANSI 编码,比如 string = new String(bytes, "GB2312"),来得到正确的“UNICODE 字符串”。
 

总的来说:

ASCII编码:用来表示英文,它使用1个字节表示,其中第一位规定为0,其他7位存储数据,一共可以表示128个字符。

拓展ASCII编码:用于表示更多的欧洲文字,用8个位存储数据,一共可以表示256个字符

GBK/GB2312/GB18030表示汉字。GBK/GB2312表示简体中文,GB18030表示繁体中文。

Unicode编码:包含世界上所有的字符,是一个字符集。

UTF-8是Unicode字符的实现方式之一,它使用1-4个字符表示一个符号,根据不同的符号而变化字节长度。

 

posted on 2016-11-24 17:43  Ming明、  阅读(669)  评论(0编辑  收藏  举报