编码问题总结

来源:互联网 发布:dot.js amd 编辑:程序博客网 时间:2024/06/05 14:33

程序员在日常的编程过程中,经常会遇到各种各样跟编码相关的问题。解决问题的根本当然是要弄清楚各个编码的来历以及具体的实现细节。本文参考某大牛的文章总结了常见的几种编码方式,并补充了笔者自己的一点见解,贴出来分享~

Unicode

在Unicode里,所有的字符被一视同仁,汉字不再使用“两个扩展ASCII”,而是使用“1个Unicode”来表示,也就是说,所有的文字都按一个字符来处理,它们都有一个唯一的Unicode码。Unicode用数字0-0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位(码位就是可以分配给字符的数字)。

在Unicode中:汉字“字”对应的数字是23383(0x5b57)。在Unicode中,我们有很多方式将数字23383表示成程序中的数据,包括:UTF-8、UTF-16、UTF-32。UTF是“UCS Transformation Format”的缩写,可以翻译成Unicode字符集转换格式,即怎样将Unicode定义的数字转换成程序数据。例如,“汉字”对应的数字是0x6c49和0x5b57,而编码的程序数据是:

BYTEdata_utf8[] = {0xE6, 0xB1, 0x89, 0xE5, 0xAD, 0x97}; // UTF-8编码WORDdata_utf16[] = {0x6c49, 0x5b57}; // UTF-16编码DWORDdata_utf32[] = {0x6c49, 0x5b57}; // UTF-32编码

这里用BYTE、WORD、DWORD分别表示无符号8位整数,无符号16位整数和无符号32位整数。UTF-8、UTF-16、UTF-32分别以BYTE、WORD、DWORD作为编码单位。“汉字”的UTF-8编码需要6个字节。“汉字”的UTF-16编码需要两个WORD,大小是4个字节。“汉字”的UTF-32编码需要两个DWORD,大小是8个字节。

UTF-8与Unicode的关系

        UTF是为传输而设计的编码,其系列还有UTF-7和UTF-16,其中UTF-16和Unicode编码大致一样, UTF-8就是以8位(一个字节)为单元对Unicode进行编码。从Unicode到UTF-8的编码方式如下:Unicode编码(16进制)UTF-8 字节流(二进制)000000 – 00007F0xxxxxxx000080 – 0007FF110xxxxx 10xxxxxx000800 – 00FFFF1110xxxx 10xxxxxx 10xxxxxx010000 – 10FFFF11110xxx 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编码克服了中文编码的两个问题。

“汉”字的Unicode编码是0x6C49。0x6C49在0×0800-0xFFFF之间,使用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

字节序

顾名思义,“字节序”就是字节的顺序,也即大于一个字节类型的数据在内存中存放的顺序。UTF-8是以单字节为单位的,所以不涉及字节序的问题。而UTF-16可以被实现为UTF-16LE(Little Endian)或UTF-16BE(Big Endian),UTF-32可以被实现为UTF-32LE或UTF-32BE。

那么,怎么判断字节流的字节序呢?Unicode标准建议用BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符”零宽无中断空格”。这个字符的编码是FEFF,而反过来的FFFE(UTF-16)和FFFE0000(UTF-32)在Unicode中都是未定义的码位,不应该出现在实际传输中。下表是各种UTF编码的BOM:

UTF编码Byte Order MarkUTF-8EF BB BFUTF-16LEFF FEUTF-16BEFE FFUTF-32LEFF FE 00 00UTF-32BE00 00 FE FF


总结一下,ISO与unicode.org都敏锐的意识到只有为世界上每种语言中的每个字符设定统一并且唯一的二进制编码才能彻底解决计算机世界信息交流中编码冲突的问题。由此诞生了UCS和unicode,而这两个规范是一致的。在Unicode里,所有的字符被一视同仁,也就是说,所有的文字都按一个字符来处理,它们都有一个唯一的Unicode码。UTF-8、UTF-16、UTF-32分别定义了怎样将Unicode定义的数字转换成程序数据。UTF-8以字节为单位对Unicode进行编码,一个英文字符占1个字节,汉字占3个字节;UTF-16以16位无符号整数为单位对Unicode进行编码,中文英文都占2个WORD;UTF-32以32位无符号整数为单位对Unicode进行编码,中文英文都占2个DWORD。


顺便记一下其它的汉字编码:

GB2312

GB2312码即中华人民共和国国家汉字信息交换用编码,全称《信息交换用汉字编码字符集——基本集》,1981年5月1日实施,通行于大陆。GB2312字符集中除常用简体汉字字符外还包括希腊字母、日文平假名及片假名字母、俄语西里尔字母等字符,未收录繁体中文汉字和一些生僻字。 EUC-CN可以理解为GB2312的别名,和GB2312完全相同。

GB2312是基于区位码设计的,在区位码的区号和位号上分别加上A0H就得到了GB2312编码。这里的A0H其实包含两个部分,用于产生国标码的20H和用于区分于ASCII的80H(20H + 80H = AH),具体细节如下:


中国国家标准总局把中文常用字符编码为94个区,每个区对应94个位,每个字符的区号和位号组合起来就是该字符的区位码, 区位码用10进制数来表示,如4907就表示49区7位,对应的字符是“学”。

由于区位码的取值范围与通信使用的控制码(00H~1FH)(即ASCII码特殊字0~31)发生冲突。于是ISO2022规定每个汉字的区号和位号分别加上32(即16进制20H)得到对应的国标交换码,简称国标码,交换码。

无论什么编码,在计算机系统中使用时都要经过二进制转换,这就是系统内码“学”的国标码为5127H。由于文本中通常混合使用汉字和西文字符,为了让汉字信息不会与单字节的ASCII码混淆,将一个汉字看成是两个扩展ASCII码,即汉字的两个字节的最高位置为1,得到的编码为GB2312汉字的内码

“学”的内码为D1A7H。无论你使用什么输入法,通过什么样的按键组合把“学”输入计算机,“学”在使用GB2312(以及兼容GB2312)编码的计算机里的内码都是D1A7H。

GBK

GB2312的出现基本满足了汉字的计算机处理需要,但由于上面提到未收录繁体字和生僻字,从而不能处理人名、古汉语等方面出现的罕用字,这导致了1995年《汉字编码扩展规范》(GBK)的出现。GBK编码是GB2312编码的超集,向下完全兼容GB2312,兼容的含义是不仅字符兼容,而且相同字符的编码也相同,同时在字汇一级支持ISO/IEC10646—1和GB 13000—1的全部中、日、韩(CJK)汉字,共计20902字。GBK还收录了GB2312不包含的汉字部首符号、竖排标点符号等字符。CP936和GBK的有些许差别,绝大多数情况下可以把CP936当作GBK的别名。

GB18030

GB18030编码向下兼容GBK和GB2312。GB18030收录了所有Unicode3.1中的字符,包括中国少数民族字符,GBK不支持的韩文字符等等,也可以说是世界大多民族的文字符号都被收录在内。GBK和GB2312都是双字节等宽编码,如果算上和ASCII兼容所支持的单字节,也可以理解为是单字节和双字节混合的变长编码。GB18030编码是变长编码,有单字节、双字节和四字节三种方式。

以上总结

这些编码的共性是变长编码,单字节ASCII兼容,对其他字符GB2312和GBK都使用双字节等宽编码,只有GB18030还有四字节编码的方式。这些编码最大的问题是2个。1.由于低字节的编码范围和ASCII有重合,所以不能根据一个字节的内容判断是中文的一部分还是一个独立的英文字符。2.如果有两个汉字编码为A1A2B1B2,存在A2B1也是一个有效汉字编码的特殊情况。这样就不能直接使用标准的字符串匹配函数来判断一个字符串里是否包含某一个汉字,而需要先判断字符边界然后才能进行字符匹配判断。

上面讲的都是大陆推行的汉字编码标准,使用繁体的中文社群中最常用的电脑汉字字符集标准叫大五码(Big5),共收录13,060个中文字,其中有二字为重覆编码。

使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。

说一下iso8859-1编码和Web服务器(Tomcat) 默认行为

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

get方式

get方式是通过url来传数据的,tomcat默认即是使用ISO-8859-1的方式来编码数据。要改变get的默认编码方式可以通过修改tomcat下的conf/server.xml文件,找到如下代码:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
这段代码规定了Tomcat监听HTTP请求的端口号等信息。

可以在这里添加一个属性:URIEncoding,将该属性值设置为UTF-8,即可让Tomcat(默认ISO-8859-1编码)以UTF-8的编码处理get请求。

更改后的代码如下所示: 
<Connector port="8080" URIEncoding="UTF-8" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

此外还可以增加属性useBodyEncodingForURI="true" ,表示是否用request.setCharacterEncoding
参数对URL提交的数据和表单中GET方式提交的数据进行重新编码,
在默认情况下,该参数为false

更改后的代码如下所示: 
<Connector port="8080"  useBodyEncodingForURI="true"  URIEncoding="UTF-8" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

URIEncoding参数指定对所有GET方式请求(包括URL提交的数据和表单中GET方式提交的数据)进行统一的重新编码(解码)的编码。URIEncoding和useBodyEncodingForURI区别是,URIEncoding是对所有GET方式的请求的数据进行统一的重新编码(解码),而useBodyEncodingForURI则是根据响应该请求的页面的request.setCharacterEncoding参数对数据进行的重新编码(解码),不同的页面可以有不同的重新编码(解码)的编码。所以对于URL提交的数据和表单中GET方式提交的数据,可以修改URIEncoding参数为浏览器编码或者修改useBodyEncodingForURI为true,并且在获得数据的JSP页面中request.setCharacterEncoding参数设置成浏览器编码。

post提交

在post方法里所要传送的数据也要URL encode,那么他是用什么编码方式的呢? 

在form所在的html文件里如果有段<meta http-equiv="Content-Type" content="text/html; charset=字符集(GBK,utf-8等)"/>,那么post就会用此处指定的编码方式编码。一般大家都认为这段代码是为了让浏览器知道用什么字符集来对网页解释,所以网站都会把它放在html代码的最前端,尽量不出现乱码,其实它还有个作用就是指定form表单的post方法提交数据的 URL encode编码方式。从这里可以看出对于get方法来数,浏览器对数据的URL encode的编码方式是有浏览器设置来决定,(可以用js做统一指定),而post方法,开发人员可以指定。 


参考文献:中文编码杂谈,Tomcat Wiki等


原创粉丝点击