关于URL编码
转载自:http://www.ruanyifeng.com/blog/2010/02/url_encoding.html与http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
一、问题的由来
URL就是网址,仅仅要上网,就一定会用到。
一般来说,URL仅仅能使用英文字母、阿拉伯数字和某些标点符号,不能使用其它文字和符号。比方,世界上有英文字母的网址 “http://www.abc.com”,可是没有希腊字母的网址“http://www.aβγ.com”(读作阿尔法-贝塔-伽玛.com)。这是 由于网络标准RFC 1738 做了硬性规定:
"...Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*'()," [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL."
“仅仅有字母和数字[0-9a-zA-Z]、一些特殊符号“$-_.+!*'(),”[不包含双引號]、以及某些保留字,才干够不经过编码直接用于 URL。”
这意味着,假设URL中有汉字,就必须编码后使用。可是麻烦的是,RFC 1738没有规定详细的编码方法,而是交给应用程序(浏览器)自己决定。这导致“URL编码”成为了一个混乱的领域。
以下就让我们看看,“URL编码”究竟有多混乱。我会依次分析四种不同的情况,在每一种情况中,浏览器的URL编码方法都不一样。把它们的差异解释 清楚之后,我再说怎样用Javascript找到一个统一的编码方法。
二、情况1:网址路径中包括汉字
打开IE(我用的是8.0版),输入网址“http://zh.wikipedia.org/wiki/春节 ”。 注意,“春节”这两个字此时是网址路径的一部分。
查看HTTP请求的头信息,会发现IE实际查询的网址是“http://zh.wikipedia.org/wiki/%E6%98%A5%E8%8A%82 ”。 也就是说,IE自己主动将“春节”编码成了“%E6%98%A5%E8%8A%82”。
我们知道,“春”和“节”的utf-8编码各自是“E6 98 A5”和“E8 8A 82”,因此,“%E6%98%A5%E8%8A%82”就是依照顺序,在每一个字节前加上%而得到的。(详细的转码方法,请參考我写的《字符编码笔记》 。)
在Firefox中測试,也得到了相同的结果。所以,结论1就是,网址路径的编码,用的是utf-8编码。
三、情况2:查询字符串包括汉字
在IE中输入网址“http://www.baidu.com/s?wd=春节 ”。注意,“春节”这两个字此时 属于查询字符串,不属于网址路径,不要与情况1混淆。
查看HTTP请求的头信息,会发现IE将“春节”转化成了一个乱码。
切换到十六进制方式,才干清楚地看到,“春节”被转成了“B4 BA BD DA”。
我们知道,“春”和“节”的GB2312编码(我的操作系统“Windows XP”中文版的默认编码)各自是“B4 BA”和“BD DA”。因此,IE实际上就是将查询字符串,以GB2312编码的格式发送出去。
Firefox的处理方法,略有不同。它发送的HTTP Head是“wd=%B4%BA%BD%DA”。也就是说,相同採用GB2312编码,可是在每一个字节前加上了%。
所以,结论2就是,查询字符串的编码,用的是操作系统的默认编码。
四、情况3:Get方法生成的URL包括汉字
前面说的是直接输入网址的情况,可是更常见的情况是,在已打开的网页上,直接用Get或Post方法发出HTTP请求。
依据台湾中兴大学吕瑞麟老师的试验 ,这时的编码方法由网页的编码决定,也就是由HTML源代码中字符集的设定决定。
<meta http-equiv="Content-Type" content="text/html;charset=xxxx">
假设上面这一行最后的charset是UTF-8,则URL就以UTF-8编码;假设是GB2312,URL就以GB2312编码。
举例来说,百度是GB2312编码,Google是UTF-8编码。因此,从它们的搜索框中搜索同一个词“春节”,生成的查询字符串是不一样的。
百度生成的是%B4%BA%BD%DA,这是GB2312编码。
Google生成的是%E6%98%A5%E8%8A%82,这是UTF-8编码。
所以,结论3就是,GET和POST方法的编码,用的是网页的编码。
五、情况4:Ajax调用的URL包括汉字
前面三种情况都是由浏览器发出HTTP请求,最后一种情况则是由Javascript生成HTTP请求,也就是Ajax调用。还是依据吕瑞麟老师的 文章,在这样的情况下,IE和Firefox的处理方式全然不一样。
举例来说,有这样两行代码:
url = url + "?q=" +document.myform.elements[0].value; // 假定用户在表单中提交的值是“春节”这两个字
http_request.open('GET', url, true);
那么,不管网页使用什么字符集,IE传送给server的总是“q=%B4%BA%BD%DA”,而Firefox传送给server的总是“q=%E6%98 %A5%E8%8A%82”。也就是说,在Ajax调用中,IE总是採用GB2312编码(操作系统的默认编码),而Firefox总是 採用utf-8编码。这就是我们的结论4。
六、Javascript函数:escape()
好了,到此为止,四种情况都说完了。
假定前面你都看懂了,那么此时你应该会感到非常头痛。由于,实在太混乱了。不同的操作系统、不同的浏览器、不同的网页字符集,将导致全然不同的编码结 果。假设程序猿要把每一种结果都考虑进去,是不是太恐怖了?有没有办法,可以保证client仅仅用一种编码方法向server发出请求?
回答是有的,就是使用Javascript先对URL编码,然后再向server提交,不要给浏览器插手的机会。由于Javascript的输出总是一致 的,所以就保证了server得到的数据是格式统一的。
Javascript语言用于编码的函数,一共同拥有三个,最古老的一个就是escape()。尽管这个函数如今已经不提倡使用了,可是因为历史原因, 非常多地方还在使用它,所以有必要先从它讲起。
实际上,escape()不能直接用于URL编码,它的真正作用是返回一个字符的Unicode编码值。比方“春节”的返回结果 是%u6625%u8282,也就是说在Unicode字符集中,“春”是第6625个(十六进制)字符,“节”是第8282个(十六进制)字符。
它的详细规则是,除了ASCII字母、数字、标点符号“@ * _ + - . /”以外,对其它全部字符进行编码。在/u0000到/u00ff之间的符号被转成%xx的形式,其余符号被转成%uxxxx的形式。相应的解码函数是 unescape()。
所以,“Hello World”的escape()编码就是“Hello%20World”。由于空格的Unicode值是20(十六进制)。
还有两个地方须要注意。
首先,不管网页的原始编码是什么,一旦被Javascript编码,就都变为unicode字符。也就是说,Javascipt函数的输入和输出, 默认都是Unicode字符。这一点对以下两个函数也适用。
其次,escape()不正确“+”编码。可是我们知道,网页在提交表单的时候,假设有空格,则会被转化为+字符。server处理数据的时候,会把+号处 理成空格。所以,使用的时候要小心。
七、Javascript函数:encodeURI()
encodeURI()是Javascript中真正用来对URL编码的函数。
它着眼于对整个URL进行编码,因此除了常见的符号以外,对其它一些在网址中有特殊含义的符号“; / ? : @ & = + $ , #”,也不进行编码。编码后,它输出符号的utf-8形式,而且在每一个字节前加上%。
它相应的解码函数是decodeURI()。
须要注意的是,它不正确单引號'编码。
八、Javascript函数:encodeURIComponent()
最后一个Javascript编码函数是encodeURIComponent()。与encodeURI()的差别是,它用于对URL的组成部分 进行个别编码,而不用于对整个URL进行编码。
因此,“; / ? : @ & = + $ , #”,这些在encodeURI()中不被编码的符号,在encodeURIComponent()中统统会被编码。至于详细的编码方法,两者是一样。
它相应的解码函数是decodeURIComponent()。
PS1 :
网页里的form编码事实上不全然取决于网页编码,form标记中有一个accept-charset属性,在非ie浏览器种,假设将其赋值(比方
accept-charset="UTF-8"),则表单会依照这个值表示的编码方式进行提交。
在ie下,我的兼容解决的方法是:
form1.onsubmit=function(){
document.charset=this.getAttribute('accept-charset');
}
PS2 :字符编码笔记:ASCII,Unicode和 UTF-8
1. ASCII码
我们知道,在计算机内部,全部的信息终于都表示为一个二进制的字符串。每个二进制位(bit)有0和1两种状态,因此八个二进制位就能够组合出 256种状态,这被称为一个字节(byte)。也就是说,一个字节一共能够用来表示256种不同的状态,每个状态相应一个符号,就是256个符号,从 0000000到11111111。
上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。
ASCII码一共规定了128个字符的编码,比方空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制 01000001)。这128个符号(包含32个不能打印出来的控制符号),仅仅占用了一个字节的后面7位,最前面的1位统一规定为0。
2、非ASCII编码
英语用128个符号编码就够了,可是用来表示其它语言,128个符号是不够的。比方,在法语中,字母上方有注音符号,它就无法用ASCII码表示。 于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比方,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使 用的编码体系,能够表示最多256个符号。
可是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比方,130在法语编码 中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表还有一个符号。可是无论如何,全部这些编码方式中,0—127表示的符号是一样的,不一样的仅仅是128—255的这一段。
至于亚洲国家的文字,使用的符号就很多其它了,汉字就多达10万左右。一个字节仅仅能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。 比方,中文简体常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多能够表示256x256=65536个符号。
中文编码的问题须要专文讨论,这篇笔记不涉及。这里仅仅指出,尽管都是用多个字节表示一个符号,可是GB类的汉字编码与后文的Unicode和 UTF-8是毫无关系的。
3.Unicode
正如上一节所说,世界上存在着多种编码方式,同一个二进制数字能够被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则 用错误的编码方式解读,就会出现乱码。为什么电子邮件经常出现乱码?就是由于发信人和收信人使用的编码方式不一样。
能够想象,假设有一种编码,将世界上全部的符号都纳入当中。每个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就 像它的名字都表示的,这是一种全部符号的编码。
Unicode当然是一个非常大的集合,如今的规模能够容纳100多万个符号。每一个符号的编码都不一样,比方,U+0639表示阿拉伯字母 Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。详细的符号相应表,能够查询unicode.org ,或者专门的汉字相应表 。
4. Unicode的问题
须要注意的是,Unicode仅仅是一个符号集,它仅仅规定了符号的二进制代码,却没有规定这个二进制代码应该怎样存储。
比方,汉字“严”的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示 至少须要2个字节。表示其它更大的符号,可能须要3个字节或者4个字节,甚至很多其它。
这里就有两个严重的问题,第一个问题是,怎样才干差别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号 呢?第二个问题是,我们已经知道,英文字母仅仅用一个字节表示就够了,假设unicode统一规定,每一个符号用三个或四个字节表示,那么每一个英文字母前都必 然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
它们造成的结果是:1)出现了unicode的多种存储方式,也就是说有很多种不同的二进制格式,能够用来表示unicode。2)unicode 在非常长一段时间内无法推广,直到互联网的出现。
5.UTF-8
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其它实现方式还包含UTF- 16和UTF-32,只是在互联网上基本不用。反复一遍,这里的关系是,UTF-8是Unicode的实现方式之中的一个。
UTF-8最大的一个特点,就是它是一种变长的编码方式。它能够使用1~4个字节表示一个符号,依据不同的符号而变化字节长度。
UTF-8的编码规则非常easy,仅仅有二条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是同样的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制 位,所有为这个符号的unicode码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
以下,还是以汉字“严”为例,演示怎样实现UTF-8编码。
已知“严”的unicode是4E25(100111000100101),依据上表,能够发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码须要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位開始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是 “11100100 10111000 10100101”,转换成十六进制就是E4B8A5。
6. Unicode与UTF-8之间的转换
通过上一节的样例,能够看到“严”的Unicode码是4E25,UTF-8编码是E4B8A5,两者是不一样的。它们之间的转换能够通过程序实 现。
在Windows平台下,有一个最简单的转化方法,就是使用内置的记事本小程序Notepad.exe。打开文件后,点击“文件”菜单中的“另存 为”命令,会跳出一个对话框,在最底部有一个“编码”的下拉条。
里面有四个选项:ANSI,Unicode,Unicode big endian 和 UTF-8。
1)ANSI是默认的编码方式。对于英文文件是ASCII编码,对于中文简体文件是GB2312编码(仅仅针对Windows中文简体版,假设是繁体 中文版会採用Big5码)。
2)Unicode编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码。这个选项用的little endian格式。
3)Unicode big endian编码与上一个选项相相应。我在下一节会解释little endian和big endian的涵义。
4)UTF-8编码,也就是上一节谈到的编码方法。
选择完”编码方式“后,点击”保存“button,文件的编码方式就立马转换好了。
7. Little endian和Big endian
上一节已经提到,Unicode码能够採用UCS-2格式直接存储。以汉字”严“为例,Unicode码是4E25,须要用两个字节存储,一个字节 是4E,还有一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。
这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时到底是从大头(Big- Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,还有一个皇帝丢了王位。
因此,第一个字节在前,就是”大头方式“(Big endian),第二个字节在前就是”小头方式“(Little endian)。
那么非常自然的,就会出现一个问题:计算机怎么知道某一个文件究竟採用哪一种方式编码?
Unicode规范中定义,每个文件的最前面分别增加一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,并且FF比FE大1。
假设一个文本文件的头两个字节是FE FF,就表示该文件採用大头方式;假设头两个字节是FF FE,就表示该文件採用小头方式。
8. 实例
以下,举一个实例。
打开”记事本“程序Notepad.exe,新建一个文本文件,内容就是一个”严“字,依次採用ANSI,Unicode,Unicode big endian 和 UTF-8编码方式保存。
然后,用文本编辑软件UltraEdit中 的”十六进制功能“,观察该文件的内部编码方式。
1)ANSI:文件的编码就是两个字节“D1 CF”,这正是“严”的GB2312编码,这也暗示GB2312是採用大头方式存储的。
2)Unicode:编码是四个字节“FF FE 25 4E”,当中“FF FE”表明是小头方式存储,真正的编码是4E25。
3)Unicode big endian:编码是四个字节“FE FF 4E 25”,当中“FE FF”表明是大头方式存储。
4)UTF-8:编码是六个字节“EF BB BF E4 B8 A5”,前三个字节“EF BB BF”表示这是UTF-8编码,后三个“E4B8A5”就是“严”的详细编码,它的存储顺序与编码顺序是一致的。
9. 延伸阅读
* The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (关于 字符集的最基本知识)
* RFC3629:UTF- 8, a transformation format of ISO 10646 (假设实现UTF-8的规定)