本文同时也发表在我另外一篇独立博客 《关于Unicode和URL encoding入门的一切以及注意事项》(管理员请注意!这两个都是我本身的原创博客!不要踢出首页!不是转载!已经误会三次了!)javascript
有感于,咱们天天用各类的编辑器,嘴里喊着utf-8,BOM头,gbk,encode,decode,却鲜有人知道它们的由来和为何这样作(好吧,也有可能就我一我的不知道)。最近找了不少资料,在这里作一个整理,和你们分享。php
总所周知计算机只能处理数字而不能处理字符,字符也老是使用数字进行表示,因此把哪些字符由哪些数字表示统一块儿来(而不是每台计算机有每台计算机的标准)很是重要。html
好比说个人计算机用1表示A,2表示B,3表示C,诸如此类。而你的计算机用0表示A,1表示B,2表示C等等。那么当我发送给你一条内容为“HELLO”的消息时,数字8,5,12,12,15就经过光缆传输到了你那,但由于数字表示的字符不一样,当你收到这串数字时,你会把它解码(decode)成“IFMMP”。因此为了有效的交流,咱们必须对如何编码(encoding)这些字符达成一致。java
终于,在十九世纪60年代,美国标准协会(American Standards Association)发明了7位(7-bit)编码方式,称为美国信息交换标准码(American Standard Code for Information Interchange),就是咱们熟知的ASCII。在这种编码标准下,HELLO表示的数字是72,69,76,76,79,而且会以二进制1001000 1000101 1001100 1001100 1001111的形式进行传输。7位编码一共提供了128种可能,从0000000到1111111。那时全部的拉丁字符 的大小写,通用的标点,缩进,空格和其它一些控制符都能在ASCII中有一席之地。在1968年,美国总统Lyndon Johnson宣布全部的计算机必须使用和能读懂ASCII标准,ASCII成为官方标准。python
电报一类的工具固然乐于使用七位编码去传输信息,可是到了七十年代,计算机的处理器更乐意与2的次方打交道——他们已经能够用8位来存储字符,也就是说这将提供256种可能。mysql
一个8位字符被存储的数字最大是255,可是ASCII标准最大只到127。这就意味着从128到255被空了出来。IBM用这些多余的数字来存储一些形状,希腊字母。好比200表明一个盒子的左下角╚,244表明小写的希腊字母α。这种编码方式的全部字符编码都列在代码页(Code page)437中web
但不像ASCII标准,128至255的字符序列历来都没有被标准化过。不一样国家都用本身的字母表来填充这些多余的序列。不是所人都赞成224表明α,甚至希腊人本身都有分歧,由于在希腊另外一代码页737中,224表明小写ω。这个时期至关数量的新的代码页出现了,好比俄罗斯的IBM计算机使用的是代码页885,224表明的是西里尔(Cyrillic)字母Я。sql
即便在存在分歧的状况下,十九世纪八十年代微软也推出了本身的代码页,在西里尔文代码页Windows-1251中,224表明西里尔文字母a,而以前的Я的位置是223.数据库
到了九十年代末期,你们作了一次标准化的尝试。15种8位字符集被推出,涵盖不一样的字母表,好比西里尔文,阿拉伯文,希伯来文,土耳其文和泰文。它们被称为 ISO-8859-1 up to ISO-8859-16(字母12被抛弃)。在西里尔标准ISO-8859-5中,224表明字母р,而Я的位置是207.windows
若是一位俄罗斯朋友发给你一份文档,你必须知道他使用的是哪种字符集。文档自己只是数字序列而已,224表明的多是Я,a 或者р。若是用错了字符集打开这份文档,那会是很是糟糕的一件事。
在1990年左右的状况大体是这样的,文档能够用不一样的语言书写,保存,而且在不一样语言间交换。可是你必须得知道他们用的是哪种字符集。固然不可能在一份文档中用多种语言。像中文和日文只能用彻底不一样的编码体系。
终于,互联网出现了!国际化和全球化让这个问题被放的更大,一个新的标准亟需出现。
从八十年代后期开始,一种新的标准已经被提出。它能给每一种语言中的每个字符赋予惟一的标识,固然远远大于256.它被称为Unicode,至今为止它的版本是6.1,包括了超过110000个字符。
Unicode的头128个字符与ASCII一致。128至255则包含了货币符号,经常使用符号,还有附加符号和变音符(accented characters)。大部分都是借鉴自ISO-8859-1。在编号256以后,还有更多的变音符。在编号880以后开始进入希腊文字符集,而后是西里尔文,希伯来文,阿拉伯文,印度语,泰文。中文,日文和韩文从11904开始,其中也夹杂了其余的语言。
绝不含糊的说这的确是一件福利,每个字符都由属于本身独一无二的数字表示。西里尔文的Я永远是1071,希腊文的α永远是945.而224永远是à,H仍然是72.注意官方书写的Unicode编码是以U+为开头的十六进制表示。因此H的正确写法应该是U+48而不是72(把十六进制转化为十进制:4*16+8=72)
最主要的问题是超出256的那部分。想固然8位已经容不下这些字符了。而Unicode并非一个字符集或者是代码页。因此这也不是Unicode制定协会的问题。他们只是给出了一个解决问题的想法而剩下实际操做的问题则留给其余人去办了。下两节咱们会讨论这个问题。
8位已经容不下Unicode了,甚至16也已经容不下,虽然只有110116个字符真正被使用,可是已经定义字符已经升至了1114112个,这就意味着至少须要21位
从七十年代开始计算机已经变得很是先进了。八位的处理器早就过期了。如今的计算机已经拥有64位的处理器,因此咱们为何不能够把超出8位容纳范围的字符转移至32位或64位呢
咱们固然能够这么作!
大部分的软件都是用C或者C++完成的,这两种语言支持一种名为"wide character"的数据类型。这种32为的字符数据类型被称为wchar_t
,它是对C语言的8位数据类型char
的一种拓展。
从理论上来讲,现代浏览器彻底可使用上面所说的这种字符类型,理论上它们就能够毫无压力的处理超过40亿个彻底不一样的字符,这对Unicode来讲是莫大的喜讯——现代浏览器是可使用Unicode的
既然浏览器能够应付32位的Unicode字符,那么还有什么问题?如今的瓶颈是,字符的传输和读写。
使用Unicode的障碍仍然存在是由于:
虽然浏览器内部对处理Unicode来讲毫无压力,但你仍需从服务器端获取数据或者发送数据到服务器,你也须要把数据存在文件或者数据库中。因此你仍然须要用8位来存储110000个字符。
UTF-8用一种很睿智的方式办到了。它的工做原理相似于键盘上的shift换挡键,通常来讲你按下键盘上的H键打印出的是小写的h,可是若是按住shift键的同时按下H键,你就能打印出大写H
UTF-8把0至127留给ASCII,192至247做为换挡键(key),128至192做为被换挡的对象。举个例子,使用字符208和209能切换进入西里尔文字符范围,字符175后跟换挡键208对应符1071,也就是西里尔文中的Я,具体的计算方法是 (208%32)*64 + (175%64) = 1071。字符224至239是双倍换挡键。字符190跟换挡键226,在加上字符128对应字符12160:⾀ 字符240和以后的换挡键是三倍换挡键。
所以UTF-8被称为多字节(multi-byte)可变宽度(variable-width)编码,称之为多字节是由于好比Я这种的字符须要不止一个字节标识它,称之为可变宽度是由于一些像H这样的字符可能须要1个字节也可能须要4个字节。
Unicode还有一个好处在于它向前兼容ASCII。不像其余的一些解决方案,因此用纯ASCII编码的文档,也都能转为有效的UTF-8编码。这大大的解决了带宽和转化的麻烦。
UTF-8已经称为互联网最受欢迎的国际字符集。但当你浏览一份非英语语言文档时,你仍然须要知道它用的是什么字符集。为了能让全部的网页能最大范围的流通,网页全部者们必须保证他们的网页使用的是UTF-8编码。
若是每个人使用都是UTF-8编码——很是好,天下太平。但事实并不是如此,这便致使了一些混乱,想象一下一个很是典型的操做,一个用户给一篇博客添加评论:
就这简简单单的流程会以不少方式走岔了,产生了如下这些问题:
假设你对字符集一无所知,刚刚半小时看的东西全忘了。上面提到的那篇博客是用ISO-8859-1字符集显示的,字符集不能识别俄语,泰文或者中文,只能识别小部分希腊文。若是你把任意拷贝来的东西粘贴而且提交,现代浏览器会尝试把它们转化为HTML实体集,好比把Я转化为Я
转化后的结果会存在数据库中,当评论被显示在网页上时是没有问题的,可是当你想把它导出为PDF或者输出在Email中,又或者在数据库中进行搜索时,问题就来了。
想象一下你有一个俄语网页,可是你忘了给你的网页指定字符集。有一个俄罗斯用户使用的默认字符集是ISO-8859-5。他想打一声招呼,"Hi",他会用键盘打出Привет,当这个用户提交时,对字符的编码是根据的是发送页面的字符编码。在这个例子中,Привет被编码成的序列是191,224,216,210,213,226。这些数字会经过网络传输到服务器,被存进数据库中
若是其余人也用ISO-8859-5字符集去查看这条评论,那么他能看到正确的文本。可是若是其余人使用的不一样的俄罗斯字符集Windows-1251,他看的结果是їаШТХв,虽然仍然是俄文,可是已经彻底没有意义了
在英国一个很是广泛的问题是,英镑符号£被转成了£。英镑标志£在Unicode和ISO-8859-1中的代码都是163.回忆一下在UTF-8中任何大于127的字符被表示是至少须要两个数字。在这种状况下,在UTF-8的序列应该是194/163,由于这个序列计算的结果(194%32)*64 + (163%64) = 163
这样看来,若是你用ISO-8859-1字符集来阅读UTF-8序列,它的前面必然会多一个Â,也就是ISO-8859-1中的194.一样的事情也会发生在全部的Unicode序列为161至191的字符上,包括©,®, ¥
相反,若是把俄文Привет用ISO-8859-5来保存,他储存的序列就是191,224等。若是你尝试用UTF-8打开,你会发现不少带有问号的黑钻石�。不像其余多字节(multi-byte)字符编码,你老是能知道你在UTF-8的哪一个位置。当你看见一个数字介于192-247之间,你就知道你在一个多字节序列的头,若是你看到的数字位于128-191之间,你就知道在一个多字节序列的中间。
但这也就意味着191后跟224这种状况是不可能出现的,因而浏览器不知道该怎么办了,因而显示了��。
这个问题也可能出如今£带有©的词中。在ISO-8859-1中 £50表明的数字序列是163,53和48。53和48都不会引发问题,可是163历来都不会独自出现,因而在UTF-8中就会显示�50。同理若是你看见了�2012,八成是由于©2012在 ISO-8859-1中被输入,而在UTF-8中显示
即便你使用所有都是UTF-8编码,浏览器也有可能出现不知如何正确一个字符的状况,ASCII字符的1-31大多数是用来控制电报(teleprinters)传送的(好比收到(Acknowledge )和中止(Stop)),若是你想尝试显示它们,浏览器可能会打印出一个问号,空或者一个带有数字的方形
须要注意的还有,由于Unicode定义了超过110000个字符,你的浏览器不可能有全部的字体来显示它们。有一些字体缺省的字符就会显示问号,空或者方形。在一些古老的浏览器中,一般只要不是英文字符就会被显示方形。
上面的讨论都忽略一个步骤,就是把数据存进数据库中。相似MySQL的数据库都能对数据库,表,列指定字符集。可是这些指定都没有对网页指定字符集重要。
当从数据库中读和写数据时,MySQL实际上只是和数字打交道。若是你告诉储存163,它就这么作。若是你给他208/159,那么它就储存这两个数字。当你从数据库读数据时,你读出的也是一样的数据。
字符集对于数据库重要的地方在于,当你使用数据库函数进行比较,转化,测量数据的时候。好比一个字段的`LENGTH`属性就依赖它的字符集,用`LIKE`和`=`进行比较的时候也是。尤为是对两个字符串进行定序(collation)比较时。详细状况能够参考这篇博客
一个解决方案
上面全部的问题都是由于提交和读取使用的字符集不一致引发的。因此解决之道是确保你的每个网页使用的都是UTF-8编码。你能够在你的<head>
标签后添加:
<metacharset="UTF-8"><metahttp-equiv="Content-type"content="text/html; charset=UTF-8">
这必须是你在制做网页时候的第一件事,它会让浏览器从新审视网页的代码。考虑到及时和效率,这个动做必须越早完成越好。你也能够给你的数据库指定UTF-8编码
须要注意的是,用户是能够用他们的浏览器编码中覆盖你的网页比编码,这种状况很是少,可是也说明这个解决方案并非万能的。为了安全起见,你能够再网站后端对用户的输入进行编码校验。
若是你的网页已经海纳了百国语言,你须要把这些数据都转为UTF-8编码。若是数量很少,你能够在网页中把这些个字符找出来,用浏览器把这些数据转为UTF-8编码
相信上面的文章已经能够解决了不少疑惑很问题了,我还想聊几个我遇到的问题。
印象中Mac下的terminal显示字符用的就是Unicode编码,因此使用命令行时基本不会碰见编码问题。可是在Window下的不管是cmd就悲剧了。
先把下面的文本存在window下的记事本中,好比叫text.txt中,供测试使用
ASCII abcde xyz German äöüÄÖÜß
Polish ąęźżńł
Russian абвгдежэюя CJK 你好
首先,若是你用原生的记事本工具编辑确定是不能把上面的文本保存在记事本中,默认记事本使用的ASCII编码,它会提示你另存一份Unicode版本。咱们固然须要使用的是这个Unicode版本。
在CMD中查看文本内容type text.txt
,你看到的结果是
ASCII abcde xyz German 盲枚眉脛脰脺脽
Polish 膮臋藕偶艅艂
Russian 邪斜胁谐写械卸褝褞褟 CJK 浣犲ソ
为何输出的全是乱码?综上一节所述,缘由无非是下面两个
首先看第一个问题:CMD使用的编码是什么?
输入命令chcp
(change code page),结果是活动页代码936
。这是罪魁祸首之一。code page 936是什么?咱们在维基百科上能够找到答案:
936– GBK SupportsSimplifiedChinese
没错,cmd默认使用的编码居然是GBK——GBK是什么?GB这两个字母表明“国标”二字拼音首字母,大家感觉一下吧……
据我stackoverflow的结果,cmd在不一样语言中使用的是当地语言的编码,而不是Unicode。
那么咱们能够把它的编码改成Utf-8吗?固然能够,执行命令chcp 65001
,更多code page代码能够从上面的那个维基百科页面上找到。
让咱们再用cmd查看文本结果:
S C I I a b c d e x y z G e r m a n äöüÄÖÜß P o l i s h
为何仍是有问题?由于刚刚说过的字体。
此时须要改变cmd的字体,在cmd窗体标题单击鼠标右键,选择属性,在字体中选择Lucida Console
此时再进行打印,你就能看到正确的结果了。
若是你有python开发经验的话,在python文件也须要注明文件的编码(至少在3.0版本之前须要这么作):
# -*- coding: utf-8 -*-
做用与在html文件head中声明的做用是同样的。让编译器从新审视文件编码
即便如此,当你在python直接定义一个包含字符串的变量而且尝试打印时,结果依然是乱码:
# -*- coding: utf-8 -*- str ="测试"print str // 娴嬭瘯
其实你看到是已经被UTF-8编码过的字符串,而后又被控制台显示出来。若是你但愿显示正确,必须再通过解码
print str.encode("utf-8")// 测试
固然最好的办法在定义时就定义为Unicode编码
str = u"测试"
其实Unicode还有不少地方能够聊,好比UTF-16 LE与UTF-16 BE,也就是big endian和little endian等等。但这就要开始牵扯更深,好比CPU,进制转换等,这些就不是我所擅长的范围了。在文章最后悔会给出一些文章连接,有兴趣的同窗能够继续深刻研究。
在谈URL encode以前,先要了解一下URL的语法。
你可能会以为奇怪URL还有语法?有固然有。
通常咱们接触的网页URL好比像http://qingbob.com
,语法(如下的翻译可能不许确)很是简单:
协议(Scheme) | http |
主机地址(Host address) | qingbob.com |
但一个包含了完整语法的URL应该是这个样子的:
https://bob:bobby@qingbob.com:8080/file;p=1?q=2#third
对应的语法应该是
协议(Scheme) | https |
用户(User) | bob |
密码(Password) | bobby |
主机地址(Host address) | qingbob.com |
端口(Port) | 8080 |
路径(Path) | /file |
路径参数(Path parameters) | p=1 |
查询参数(Query parameters) | q=2 |
片断(Fragment) | third |
更详细的图解能够参考维基百科
看过了上面的语法,你大概已经回忆起十之八九,单个部分就不详解了,直接进入URL语法。
URL语法定义了如何把URL的各个部分组装起来,以及如何把他们区分开来。好比://
符号就是把协议与主机名区分开;主机名与路径用/
区分开;查询参数只能跟在?
以后。这也就意味着有一些符号做为保留字为语法服务。可能有的符号在整个URL中都做为保留字,有的只是在某个部分做为保留字。一旦某个保留字在它不该该出现的地方出现了(好比在文件名中出现了一个?
)。这个字符就须要被编码(encode)。
URL编码就是把一个字符转变成另外一种对URL无损的表现形式,而且让它失去URL语法含义。经过字符编码(character encoding)把这个字符转化为十六进制,而且以百分号%开头,好比一个问号通过URL编码以后就是%3F
。
想象咱们有一条指向某张图片的URL:http://example.com/to_be_or_not_to_be?.jpg
。转化以后的结果就是http://example.com/to_be_or_not_to_be%3F.jpg
,这样就避免了URL中有歧义的问号以后的查询语法。
一个空格在路径参数(path fragment)部分会被编码成%20
(毫不是"+"),而+
在参数路径部分则能够保留而不被编码。
而在查询部分(query)中,空格会被编码成"+"(为了向前兼容),或者"%20",而"+"会被编码成"%2B"。
举个例子,一样的字符串blue+light blue
在不一样的部分会被编码成不一样样子:
var url ="http://example.com/blue+light blue?blue+light blue" encodeURL(url)// http://example.com/blue+light%20blue?blue%2Blight+blue
大部分人都忽略了"+"在路径部分是能够被容许的。还有一些其余的例子:
也就是说下面这个URL片断,是不会被编码是合法的:
http://qingbob.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+,=:@-._~!$&'()*+,==?/?:@-._~!$'()*+,;=/?:@-._~!$'()*+,;
咱们把上面这条URL好好总结一下:
协议(Scheme) | https |
主机地址(Host address) | qingbob.com |
路径(Path) | /:@-._~!$&'()*+,= |
路径参数名称(Path parameters name) | :@-._~!$&'()*+, |
路径参数值(Path parameters value) | :@-._~!$&'()*+,== |
查询参数名称(Query parameters name) | /?:@-._~!$'()* ,; |
查询参数值(Query parameters value) | /?:@-._~!$'()* ,;== |
片断(Fragment) | /?:@-._~!$&'()*+,;= |
URL语法只在用来读被编码以前的URL。
假设咱们有这么一条URLhttp://example.com/blue%2Fred%3Fand+green
,分析结果以下
协议(Scheme) | http |
主机(Host) | example.com |
路径碎片(Path segment) | blue%2Fred%3Fand+green |
解码以后的路径碎片(Decoded Path segment) | blue/red?and+green |
这样翻译的结果就成了,咱们在寻找一个名为"blue/red?and+green"的文件。
可是若是咱们在解码以后再进行翻译http://example.com/blue/red?and+green
:
协议(Scheme) | http |
主机(Host) | example.com |
路径碎片(Path segment) | blue |
路径碎片(Path segment) | red |
查询参数名称(Query parameter name) | and green |
翻译的结果是,在"blue"文件夹中的名为"red?and+green"的文件。
明显结果就彻底不一样了,对URL语法进行分析必定要在编码以前进行进行。
最后参考文献: