转载自: http://hi.baidu.com/baiduqa/blog/item/5c937b418264581f73f05d20.html
1. 字符编码基础知识
1.1. 字符编码基本概念
现代编码模型的编码思想包括:有什么字符、他们的编号、这些编号如何编码成一系列的码元,以及最后这些单元如何编码为8位字节流。对应于如下术语:
1)字符表 一个系统所支持的所有抽象字符的总合。
2)编码字符集 定义了如何使用称为码点的非负整数集表示一个字符集,一个整数对应一个抽象的字符。
3)字符编码形式 定义将编码字符集的整数代码转换成有限大小整数代码值以利于使用固定位的二进制表示数字的形式的系统存储。例如使用8位或16位单元存储数字信息。字符编码形式定义了如何用单个或多个码值表示码点的方法。例如utf8是一种编码形式,utf-16则是另一种编码形式。
4)字符编码机制 定义固定大小的整数代码如何映射到基于8位字节数据的文件系统存储或者基于8位字节网络传输。在多数使用unicode的场合,一个简单的字符编码机制用来指定每个整数的字节顺序是大字节在先顺序还是小字节在先顺序。还有其他复杂的字符编码机制。
1.2. 字符编码发展
字符编码的历史大致可以分为三个阶段:
1)ascii阶段
刚开始只支持英语,其他语言不能够在计算机上存储和显示。使用一个字节来存一个字符。
2)ansi编码(本地化)
为使计算机支持更过语言,通过使用0x80~0xFF范围的2个字节来表示1个字符。不同的国家和地区制定了不同的标准,由此产生了各种各样的编码标准,如gb2312、big5、jis等。这些使用两个字节来表示一个字符的各种汉字延伸编码方式,称为ansi编码。
3)Unicode阶段(国际化)
为了使国际间信息交流更加方便,国际组织制定了unicode字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode仅仅制定了字符集,用来给unicode编码的标准有utf-7、utf-8、utf-16、unicodeLittle、unicodebig等。
1.3. 主要编码
1.3.1. Ascii
ascii全称美国信息互换标准代码(american standard code for information interchage)。主要用于显示现代英语和其他西欧语言,是现今最通用的单字节编码,等于国标标准iso 646。包含控制字符32个和可打印字符94个。编码单元为8位,取值单位从0x00-0x7F,最高为0。
1.3.2. 汉字编码
汉字编码均采用双字节编码,编码单元为8位。
1.3.2.1. Gb2312-80
Gb2312是对ascii的中文扩展,是中华人民共和国国家标准汉字信息交换用编码。收录简化汉字及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母等共7445个图形字符。其中汉字以外的图形字符682个,汉字6763个。为了与系统中基本的ascii字符集区分开,所有汉字编码的每个字节的第一位都是1。
Gb2312的汉字编码规则是:第一个字节的值在0xB0到0xF7之间,第二个字节的值在0xA0到0xFE之间。但是gb2312收录的汉字太少,以致很多常用字都没有收录,如朱鎔基的“鎔”字。为了解决这些问题,以及配合unicode的实施,全国信息技术化技术委员会制定了gb13000,即gbk。Gbk向下与gb2312完全兼容,向上支持iso-10646国际标准。
1.3.2.2. Gbk
Gbk包含了20902个汉字,其编码范围是0x8140-0xfefe,剔除高位0x80的字位。收录汉字包括:
1)gb2312中全部汉字、非汉字字符
2)big5中的全部汉字
3)与iso-10646相应的国家标准gb13000中的其他cjk汉字
4)其他汉字、部首、符号等。
其编码区分成三个部分:
1)汉字区 包括
a) Gbk/2:0xb0a1-f7fe,收录gb2312汉字6763个,按原序排列,0xd7fa-0xd7fe为空洞。
b) Gbk/3:0x8140-a0fe,收录cjk汉字6080个,0x817f-0xa07f为空洞
c) Gbk/4:0xaa40-fea0,收录cjk汉字和增补汉字8160个,0xaa7f-0xfe7f为空洞
2)图形符号区 包括
a) Gbk/1:0xa1a1-0xa9fe,除gb2312的符号外,还增补了其他符号
b) Gbk/5:0xa840-0xa9a0,扩充非汉字区
3)用户自定义区
1.3.2.3. Gb18030-2000
GB18030-2000是2000年推出的国家标准。它可以视为GBK的升级,因为它主要增加了Unicode 3.0中新增的一些字符。除了GBK的字符,它能表示UNICODE中所有的字符。中国出售的所有软件产品都要求支持GB18030。
GB18030与GBK完全兼容,除了欧元符号 :在GB18030中是A2E3,在GBK中是0x80。
采用单字节、双字节和四字节三种方式对字符编码,编码范围如下:
1)单字节: 0x00-0x7f
2)双字节: 0x81-0xfe + 0x40-0x7e, 0x80-0xfe
3)四个字节: 0x81-0xfe + 0x30-0x39 + 0x81-0xfe + 0x30-0x39
1.3.2.4. Big5
Big5又称五大码,是使用繁体中文字社群众最常用的计算机汉字字符集标准,由台湾5家大公司的方案拼凑而成。
Big5共收录13053个中文字,其中有两个字重码,为兀(0xa461及0xc94a)和嗀(0xdcd1-0xddfc)。
Big5使用双八码存储方式,以两个字节来安放一个字。高位字节使用了0xa1-0xf9,低位字节使用了0x40-0x7e及0xa1-0xfe。
原始的BIG-5 只包括一些常用的字,甚至不包括日文的假名等,在实际的应用中很多系统给BIG-5加上了自己的扩展。例如,MS code page 950,欧元符号A3E1。
1.3.3. Unicode
Unicode是一个大一统的方案,它是http://www.unicode.org制定的编码机制,要将全世界常用文字都函括进去。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。随着计算机工作能力的增强,Unicode也在面世以来的十多年里得到普及。
但自从unicode2.0开始,unicode采用了与ISO 10646-1相同的字库和字码,ISO也承诺ISO10646将不会给超出0x10FFFF的UCS-4编码赋值,使得两者保持一致。
Unicode的编码方式与ISO 10646的通用字符集(Universal Character Set,UCS)概念相对应,目前的用于实用的Unicode版本对应于UCS-2,使用16位的编码空间。也就是每个字符占用2个字节,基本满足各种语言的使用。实际上目前版本的Unicode尚未填充满这16位编码,保留了大量空间作为特殊使用或将来扩展。
Unicode和ucs只是分配整数给字符的编码表,即只是一个编码字符集合。现在存在好几种将一个字符表示为若干个字节的方法。最显而易见的方法是将unicode文本存储为2个或4个字节序列的串。这两种方法的正式名称为ucs-2和ucs-4。
但是在unix下使用ucs-2或ucs-4会导致非常严重的问题。用这些编码的字符串会包含一些特殊的字符,比如’\0’或’/’,他们在文件名和其他c库函数里都有特别的含义。另外,大多数使用ascii文件的unix下的工具,如果不进行重大修改是无法读取16位的字符的。基于这些原因,在文件名,文本文件、环境变量等地方,ucs-2不适合作为unicode的外部编码。
因此需要一种新的编码方案称为utf(unicode transfer format)运用在unix/linux环境中。Utf-7,utf-8,utf-16都是广泛接受的方案。Rfc2781和rfc3629定义了utf-8和utf-16的编码方式。
1.3.3.1. Utf-8
Utf-8就是以8位为单元对ucs-2进行编码。从ucs-2到utf-8的编码方式如下:
U-00000000 - U-0000007F: 0xxxxxxx
U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF:
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF:
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 - U-7FFFFFFF:
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
Utf-8有如下特性:
1)UCS 字符 U+0000 到 U+007F (ASCII) 被编码为字节 0x00 到 0x7F (ASCII 兼容)。这意味着只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 两种编码方式下是一样的。
2)所有 >U+007F 的 UCS 字符被编码为一个多个字节的串, 每个字节都有标记位集。因此, ASCII 字节 (0x00-0x7F) 不可能作为任何其他字符的一部分.
3)表示非 ASCII 字符的多字节串的第一个字节总是在 0xC0 到 0xFD 的范围里, 并指出这个字符包含多少个字节。多字节串的其余字节都在 0x80 到 0xBF 范围里。这使得重新同步非常容易,并使编码无国界, 且很少受丢失字节的影响.
4)可以编入所有可能UCS 代码
5)UTF-8 编码字符理论上可以最多到 6 个字节长, 然而 16 位 BMP 字符最多只用到 3 字节长。
6)Bigendian UCS-4 字节串的排列顺序是预定的.
7)字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到.
1.3.3.2. Uft-16
Utf-16是以16位为编码单元的,在范围u+0000到u+ffff间的码点使用一个单一的16位编码单元表示;而在范围u+10000到u+10FFFF间的码点则使用一对16位编码单元表示,称作代理对。Utf-16优化了基本多语言平面(bmp)中字符的表示,即位于u+0000到u+FFFF范围内的字符。该范围包含了目前世界上所使用的书写系统中的绝大数字符,每个字符只需要一个16位的编码单元。对于基本多语言平面,utf-16可作为固定宽度的编码格式来有效使用。但是对于增补字符,utf-16需要两个16位的编码单元,意味着正式的utf-16是一个变宽的编码格式。Utf-16是早期unicode遗留下的历史产物,原本被设计成具有固定宽度的16位编码格式,为支持超过u+ffff的增补字符,设立了代理机制。
1.3.3.3. Utf-32
Utf-32是一种最简单的unicode编码格式。每个unicode码点直接被表示为一个32位的编码单元。Utf-32是一种固定宽度的字符编码格式。每个utf-32编码单元的值与unicode码点的值完全相同。
1.4. Tips
1.4.1. 编码字节序
Big endian和Little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。如果将49写在前面,就是little endian。
“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,一个皇帝送了命,另一个丢了王位。
我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。
对于任何字符编码,编码单元的顺序是由编码方案指定的,与endian无关。例如gbk的编码单元是字节,用两个字节表示一个汉字,这两个字节的顺序是固定的,不受cpu字节序的影响。Utf-16的编码单元是word,word之间的顺序是编码方案指定的,word内部的字节排列才会收到endian的影响。Utf-8也是以字节为编码单元,没有字节序的问题。
一个使用utf-16编码的文件如何进行解释呢?以一个例子来说明,打开记事本,写上一段文字,然后另存,在保存的对话框中可以看到有四种编码方式可以选择,分别是:ansi,unicode,unicode big endian和utf-8。
Ansi是默认编码方式,也是系统的默认编码方式,由缺省代码页决定。
Utf-8不用解释了。
Unicode和unicode big endian都是utf-16编码的两种,区别在于前者采用little endian,后者采用big endian。还有一种方式采用bom标记字节序列,bom即“byte order mark”,是一个有点小聪明的想法。在ucs编码中有一个叫做“zero width no-break space”的字符,他的编码是feff。而feff在ucs中是不存在的字符,所以不应该出现在实际传输中。Ucs规范建议我们在传输字节流前,西安传输字符“zero width no-break space”。这样如果接受者收到fef,就表明这个字节流是big-endian;如果fffe,就表明这个字节流是little-endian的。
“ABC”这三个字符用各种方式编码后的结果如下:
utf-16be 00 41 00 42 00 43
utf-16le 41 00 42 00 43 00
utf-16(bom be) fe ff 00 41 00 42 00 43
utf-16(bom le) ff fe 41 00 42 00 43 00
utf-16(不带bom) 00 41 00 42 00 43
1.4.2. Windows代码页
Unicode推出后,microsoft将windows的内核都改成支持unicode字符集。但是由于现有的大量程序和文档都采用了某种特定语言的编码,例如gbk,windows不可能不支持现有的编码而全部改用unicode。Windows使用代码页来适应各个国家和地区。Gbk对应的code page是cp936,gb18030的code page为cp54936。但是由于gb18030有一部分四字节编码,而windows的代码页只支持单字节和双字节编码,故cp54936是无法真正使用的。
Windows可以同时支持多个代码页,只要文件能够说明自己使用什么编码,用户又安装了对应的代码页,windows就能正确显示,例如在html文件中指定charset。
Windows中有缺省代码页的概念,可以通过控制面板的区域选项设置,其作用是缺省用什么编码来解释字符。Windows的记事本的存储格式有一项是ansi,其实就是按照缺省代码页的编码方式保存。
2. 网页编码方式
2.1. url编码基础知识
一个http请求需要经过如下几个环节:
1)浏览器把url以及提交的内容经过编码后发送给服务器
2)服务器处理完毕后将结果编码返回给浏览器
3)浏览器按照指定的编码显示网页
一个完整的url由如下方式组成:
域名:端口/contextPath/servletPath/pathInfo?queryString
其中pathInfo和queryString是需要编码的部分。
Rfc1738中定义了url的语法语义,限制了url中可以出现的字符。对于不可在url中出现的字符需要按照一定的方式进行编码,叫做url encode。需要进行encode的符号包括如下:
1)ascii中的控制字符,原因很简单,因为他们是不可见的,范围为00-1F和7F;
2)非ascii字符,比如中文字符等,这是因为url中没有安全的办法指定字符集(rfc2396);
3)保留字符,url语法中用到的字符,$&+,/:;=?@
4)不安全字符:空格#%<>{}I\~^[]`,出于各种原因;
url encode采用%XX方式,XX为字符的十六进制编码。
但是在实际应用中,浏览器是否进行url encode,采用何种字符集进行url encode,与浏览器和服务器的设置都有关系,分析如下,以下分析均在windows中文环境中:
1)对用户在地址栏中直接输入的url,编码方式与浏览器的设置有关
浏览器(模式) PathInfo QueryString
Ie(utf-8模式,默认) Utf8编码,无url encode Gbk编码,无urlencode
Ie(ansi模式) Gbk编码,无urlencode Gbk编码,无urlencode
Firefox(utf8模式) Utf8编码,urlencode Utf8编码,urlencode
Firefox(ansi模式,默认) Gbk编码,urlencode Gbk编码,urlencode
Opera(utf8模式,默认) Utf8编码,urlencode Utf8编码,urlencode
2)对网页中的链接,与该网页本身的编码方式有关。
在不改变浏览器默认选项的情况下,各个浏览器的编码方式如下
浏览器(网页编码方式) PathInfo QueryString
Ie(utf8网页) UTF-8编码、urlencode UTF-8编码、无urlencode
Ie(gbk网页) UTF-8编码、urlencode GBK编码、无urlencode
Firefox(utf8网页) UTF-8编码、urlencode UTF-8编码、urlencode
Firefox(gbk网页) GBK编码、urlencode GBK编码、urlencode
3)对用户提交的数据,不论是get方式还是post方式,其编码方式由网页中的编码方式和相关调用有关。
页面的编码方式由http头指定或网页的meta标记指定。http头中含有content-type参数,其中指定了charset:
Content-type:text/html;charset=gb2312
Meta标记的方式如下:
<meta http-eq后台v=”content-type” content=”text/html;charset=gb2312”/>
不同的浏览器处理方式也会不同,ie可能通过文件内容识别,firefox偏向meta标签识别。
对于传统的表单提交,其编码方式是由页面的编码方式决定的;而ajax提交的数据则与其调用方式有关,如果采用了escape类似的编码函数,则编码成utf-8进行发送。
2.2. 服务器对编码的处理
假设后端均采用gbk存储数据,那么对提交的数据需进行编码识别并进行相应的编码转换,主要针对两种情况:
1)url路径部分:
由于需要支持中文,这部分是不可控的,取决于操作系统和浏览器,因此需要进行判别到底是什么编码,然后再进行编码转换。
2)提交数据部分:
提交的数据也既有可能是utf-8编码也有可能是gbk编码。一种办法是判断是否是utf-8编码,但这种判断存在一定的失败率;还有一种办法从页面上控制,提交时强制增加一个字段表示这是否是utf-8编码,apache无需考虑其他的,只根据该字段判断是否需要做utf-8到gbk的编码转化,这个办法是不会出现误伤的。
判断是否是utf-8编码有一定的失败率,有如下几种情况:
1)判断错误。
主要发生在utf8与gbk编码重叠的部分,需根据实际应用进行处理。
2)判断成功,但是转换失败。
没有对应的gbk编码会导致转换失败,例如韩文字符,在这种情况下可转成实体。
3)判断是否是utf-8时判断错误,并且转换失败,这种情况下不会出现问题。
编码的识别和转换完成后,后端的模块也需要根据实际应用进行各种各样的处理,如:控制字符过滤、繁简转换、全半角转换、半个汉字处理、字符集过滤等等。
当获取到数据组装将要返回的页面时,对出现在页面上的信息需进行一定的转义,比如对于一些由html标签组成的文本,如果不进行特殊处理,那么浏览器会当做标签来进行解析,从而引起页面展现错误。
3. 编码问题分类
3.1. gbk字符集中的特殊字符
1) 0x80欧元符号
【分析】
数据库支持有问题,会自动截断;gbk对其编码与其他编码方式不兼容。非必要条件下建议过滤掉。
【Bad case】
我们在进行编码设计时将包含欧元符号的信息插入数据库,因为数据库将欧元符号以后的部分截断,记录的信息的长度与实际长度不一致从而引起下游模块的逻辑错误。
2) 0x00-0x31控制字符部分
【分析】
控制字符无法打印。
【bad case】
当用户构造了由控制字符组成的信息,这部分信息显示在页面上,会使页面上本应出现文字的地方出现空白,并且如果这部分文字存在链接会使得链接失效从而导致不可点击。
3) 0x7F 空白区位
【分析】如果输入会是不可见字符,建议过滤掉。
4) 0x5C反斜杠
【分析】
它的特殊性在于两个原因:1、它作为转义符标示的特殊用法;2、它编码区间
落在GBK字符集的后半个汉字允许的编码区间内。由于这两个原因,再结合GBK字符集本身存在雪崩问题的隐患,当末尾存在半个汉字,和0x5C字符结合就可能导致转义符号无效,裸露出后续的’等,导致转义符号实效,带来一些安全问题和js失效等问题。或者和半个汉字结合导致mysql转义处理失效。
5) 空洞区:0xd7fa-0xd7fe,0x817f-0xa07f,0xaa7f-0xfe7f
【分析】
GBK前半个汉字的范围在0x81-0xfe之间,不含0xff,如果用户构造了0xff这样的半个汉字上来,由于部分浏览器支持的问题,例如ie,就会把0xFF字符当作GBK的前半个汉字和后续字符结合。
除此之外,码区的中间也有一些空洞
【bad case】
如果黑客构造0xff 0x5c 0x27这样的字符串(0x5C是\,作为转义标示符号,0x27是单引号’),那么0xff和0x5c结合成为一个不可见字符,导致原本\’的转义失效,’暴露出来造成安全漏洞。
6) Sql语句中的特殊字符:’、\
【分析】
mysql对于特殊字符如"\"等需要处理后才能存储,采用的函数是mysql_real_escape(),将其中的特殊字符转义成/*,对这个函数的要求是页面必须存在一个可用的mysql连接。
7) 字符外形和全角英文字符完全一致的字符:0xA6A2与0xA7A2,0xA976与0xA3A8
8) 全角空格:0xA1A1
9) 扩展汉字区域:主要是非汉字区域和特殊字符
10)Cp936和其它码表定义不一致的地方
11)GBK编码中和其他编码方式有冲突或者有处理方式不一样的个别字符
12)可以构成强制转义字符的字符:&#
13)字符串结束符:\0
14)可能被作为数据结构分隔符的字符
15)边界字符:丂 (0x8140),亐 (0x8180),儈 (0x837E),凗 (0x83FE),狛 (0xA0FE),癄 (0XAFA0), (0xFE7E),鳌 (0xF7A1),(0XFEA0),齄 (0XF7FE)。
16)Trailing byte 在 low-ansi 范围中 (4 个例子)
腀 (0xC440 / 0x8140) 儬 (0x83A0 / 0x512c)
爘 (0xA07C / 0x7218) 爢 (0xA086 / 0x7222)
17)Leading 和 trailing byte 大小写是相同的表示 (3 个例子)
'C' / 'c' 丆 (0x8143) / 乧 (0x8163)
'M' / 'm' 鱉 (0xF74D) / 鱩 (0xF76D)
'S' / 's' S (0XA053) / s (0XA073)
18)Trailing byte 和 Leading byte 范围相同 (4 个例子)
亖 (0x8181) 汉 (0xBABA)
牋 (0xA0A0) 鼢 (0xF7F7)
19)leading 或 trailing byte 是 0xAA, 0xAE or 0xBF, 容易变成乱码(3 个例子) 煪 (0x9FAA) 伄 (0x81AE) 骺 (0XF7BF)
20)以下特殊字符的unicode 和汉字的 byte是一样的,在解码的时候容易出错,应该有20个左右,只列举12个 á (U+00EA) (0xA8A2) à (U+00E0) (0xA8A4) é (U+00E9) (0xA8A6) è (U+00E8) (0xA8A8) ì (U+00EC) (0xA8AC) í (U+00ED) (0xA8AA) ó (U+00F3) (0xA8AE) ò (U+00F2) (0xA8B0) ú (U+00FA) (0xA8B2) ù (U+00F9) (0xA8B4) ü (U+00FC) (0xA8B9) ê (U+00EA) (0xA8BA)
3.2. utf8与gbk编码冲突部分
utf8与gbk编码冲突部分,会导致编码识别失败,从而在展现页面时出现问题。utf8与gbk有127个重叠的编码。
【bad case 1】
“璎玥“这个用户名,此用户的正确的GBK是E8ACAB68,最后一个字节就是ascii的‘h‘。例如当用户在ff访问http://www.test.com/璎玥 时,E8ACAB 是utf8编码的”謫”,68是‘h’,所以看到的就是访问用户”謫h”。在ie下是正常的utf8编码字符,所以无问题。
【bad case 2】
编码0xc2b7,在gbk中表示“路”这个字,在uft8中表示为“•”这个字符。如果用户的url中含有maria•sharapova,当以utf8编码发送时,被优先判断为gbk编码,则服务器识别为maria路sharapova。
重叠部分的编码既需要在不同的浏览器中测试,也需要在url路径和提交的数据中测试。
3.3. Gbk中不存在的编码
1)韩语字符
2)其他少数语种
3)GB18030相对GBK增加的字符
3.4. 相邻字节组合部分
1) 内容过滤中相邻字节组合:哈林
【bad case】
汉字“哈林”相连的2个字节被识别为“妓”,正好是个过滤词,导致正常信息被过滤。
2) 页面展现中相邻字节组合:牛肩猪肉
页面上如果存在“牛肩猪肉”的文字则出乱码的现象,原因在于牛的第二个字节和肩的第一个字节正好构成了全角的‘>’字符,后台对这类字符进行了转义。
3) Sql语句中相邻字节组合成\:
当用户输入字符ó' 等一些字符时,出现了组装SQL语句时错误。这类字符的特点是: 一个大于ascii128编码的字符加上一个',在GBK编码过程中会被解析成为\汉',然后造成SQL语句的错误。ó字符为ASCII码的243,可以用小键盘敲入。此时,经过mysql的转码,被转义成为ó\', 而汉字编码会将ó\形成一个汉字笈',这时就会出现mysql错误的问题。
作者:qabloger