编码
字符
我们的语言基本上都围绕着字符,就是character,常简称char,很多时候字符会是文本的最小组成单位(注意只是“很多时候”,因为世界是奇妙的)。
不是一定要文字才叫字符,一些注音字符、数学符号、某些文字里的修饰符号、特殊符号、表格符号、甚至Emoji等等,其实都是字符。
字符集
字符要成套才有用,比如“英文字母”就是一个字符集,当然这么说听起来对计算机毫无意义。
一般我们所说的字符集(Character Set)就是一个规范,它收录若干字符,并且给这些字符逐一分配了一个编号当作索引(为了不过早引入编码的概念造成混淆,这里我称其为“编号”)。
ASCII
ASCII是当今计算机世界最经典的字符集,它收录了英文字母和若干标点,还有一些专门供计算机使用(不是给人看)的控制字符。
GB系列
GB2312是一个常见的中文字符集,其中“GB”就是“国标”(咱们国家很多不同行业的标准代号都是这样命名的)。它收录了大概几千个汉字,以及几百个西文字符。
GBK是微软最早在Win95里实现的对GB2312进行的扩展,追加了很多繁体字和西文字符,总计收录的字符数大概有20000多个。K就是拼音“扩展”。
GB18030是国标对GB2312的升级(当然中间还有其他升级,但大多被淹没在历史潮流里面了),它一下就收录了70000多个字符,最大的升级部分有繁体中文字符、新字、生僻字、少数民族文字、日韩字符等。
上面三个GB系列的字符集都是为中文设计的,当然也扩充了一些东亚语文的内容(CJK字符,Chinese, Japanese, Korean),因为这些邻居的字符在中国也挺常出现的。
Big5
又称大五码,是繁体中文地区,例如台湾、香港、澳门常用的字符集,大概收录了一万多个字符,其中以繁体中文为主。因为被Windows所接受成为繁中版的默认编码而成为了事实标准。
UCS
就像中文一样,几乎每种语言都存在一个给自己的语言设计字符集的问题。
ISO意识到这个问题之后,设计了一套通用字符集UCS(Universal Character Set),目的是用一套字符集表示全世界(甚至外星人!?)的所有字符!
结果UCS成功了,因为互联网发展的太快了,任何国家的人每天都在互联网上浏览来自全世界的不同语言不同文字的内容,大家当然希望用一套字符集就能收录全世界所有的字符。
字符编码
很多人会把字符集和字符编码的概念搞混,其实不怪,因为这俩东西好多时候都是捆绑定义的。
字符编码(Character Encoding)就是按照一定的技术要求(比如以8bit为单位)对字符集中的每一个字符进行编码,以便文本能够在计算机和网络传输上使用。
简单的说,就是把字符集里面的那个每个字符的编号,给弄成计算机能懂的格式。
很多字符集在制定的时候,就已经配套了它的编码方案,比如ASCII、GB系列、Big5。对于这种字符集/编码,称呼上虽然模糊,但结合技术语境而言一般也不会有什么误解。
ASCII
标准ASCII只收录了128个字符,使用7bit可以完美编码。例如英文字母A的ASCII编码就是十六进制0x41,然后1字节剩下的一位就没啥用了,可以用来当奇偶校验。
后来ASCII被扩展到了8bit,供256个字符,用8bit也就是1字节可以完美编码,并且低7字节完全兼容。
ASCII是国际标准而扩展ASCII并不是,下文所说到的“兼容ASCII”都是指兼容7bit的标准ASCII。
GB2312
GB2312使用1/2字节变长编码,单字节部分是兼容ASCII,其他几千个字符都是用双字节编码。
GB2312在编码的时候使用了一个“分区”的概念,小时候家里有一本区位码表,就是配合Windows里古老的“区位输入法”用的。
GBK
GBK的编码方案是GB2312的超集,它完全兼容GB2312,不过把GB2312里面没定义的那些编码空间都用起来了。
GB18030
GB18030的编码方案稍微复杂一点,它用的是1/2/4字节变长编码方案。它完全兼容GB2312,基本上兼容GBK。
Big5
Big5使用固定两字节编码,它的首字节避开了ASCII的范围,因此实际在程序实现上面它可以近似兼容ASCII,由于它的低字节包含了一些ASCII字符,这个兼容也是不完美的,具体情况可以看看维基百科,非常有趣。
Unicode
Unicode有一个非常高大上的中文名字叫万国码,其实也是个字符集,它和UCS之间有微妙的高度雷同关系,好在两边的组织都意识到了搞分化是不好的,于是互相之间达成了高度的一致。虽然它们的确是两个不同的标准,但很多时候混淆来看也无妨。
Unicode是定长编码,根据版本不同,它有2字节(对应UCS-2)、4字节(对应UCS-4)的版本。
因为Unicode是定长的,它实在太简单粗暴了。例如如果用4字节的Unicode来传输英文文本就浪费了3倍的体积,而用2字节的版本也不爽,一来容量较小,二来对于英文文本也还是浪费的。于是在实现上对它进行了一定的优化,称为Unicode转换格式(Unicode Transformation Format)也就是我们耳熟能详的UTF了。
UTF-32
UTF-32是UCS-4的最朴素的实现方式,就是简单地用定长4字节。
缺点嘛很明显就是很浪费体积。
优点也是有的,首先就是把它转换到Unicode最简单,而且对于“第[i]个字符”这种随机访问也很好计算,直接字节数/4就是了对不?
但因为组合字符(比如越南语,网上用来搞一个超长的流泪图标破坏排版那种)的存在,一个UTF-32码元(4字节)严格上也并非一个文本编辑上的单元,这种情况下对于排版系统而言UTF-32没有太多优势。
UTF-16
UTF-16是使用2/4字节实现的UCS-4变长编码。
因为大多数时候用到的字符不超过65536个,所以UTF-16在大多数时候1个字符都只占2字节,这样比起UTF-32省了接近一半体积,同时它的解析也不会太麻烦。
固定长的编码方式对于计算机程序而言有一个非常大的优势就是字符串处理会容易的多,尤其是正则表达式的实现。因此很多现代语言,例如C#/Java的字符串内部实现使用UTF-16,因为它是一种效率和体积比较平衡的编码方式。
UTF-8
UTF-8应该是现在互联网上使用最广泛的统一语言编码实现方式了。
它是1-4字节变长编码(原本是1-6字节,但是因为后面那些超出了Unicode定义了,后来就改成1-4字节了)。单字节的情况兼容ASCII,在这个由英文主宰的互联网环境里面这是非常好的特性,因为它在很多时候会非常节省体积,而且这种时候完全不需要编码转换。
但它的缺点也相当明显,将UTF-8转换到Unicode的算法会更加复杂,效率降低。
对于中文环境而言UTF-8也比较吃亏,因为使用UTF-8编码大多数中文字符需要3字节,这就比GB系列和UTF-16浪费空间。
UTF-8并未编码0x10FFFF以上的部分,所以严格的说它只是UCS-4的子集。好在缺失的那部分本身就不受UCS/Unicode的重视,估计实在是太犄角旮旯了。
我觉得UTF-8最终成为互联网主流很大一定程度是因为它的单字节是兼容ASCII的。
阶段性小结
字符集
收录了很多字符,并且编号,给人看的。
编码
实现一个字符集,将它的编号以一定规则用二进制实现,给计算机看的。
GB系列
中国的国标字符集/编码,GB2312和GBK已经基本上过时了,如果要良好的支持中日韩文,又逃不开GBK的魔爪(比如历史代码束缚),那可以考虑升级到GB18030,这是国标的最新版,也是最先进的一版。
UCS/Unicode
把全世界上百万个你见过的或者你没见过的字符全部收录进一套字符集,已经被全世界接受成为了国际标准。
UTF
UCS/Unicode转换格式,就是实用的编码方案,用于计算机实现。
UTF-16
UCS/Unicode的一种折衷了处理效率和存储空间的编码实现方案,常被各种现代语言当作字符串内部编码使用。
UTF-8
UCS/Unicode的一种倾向于节省空间的编码实现方案,因为对ASCII兼容,对英文文本非常有利,成为了当今互联网的主流(甚至事实标准)。
如果你的网站没有任何历史包袱,直接上UTF-8别商量!
如果你的网站有一些历史包袱,商量商量还是上UTF-8吧,包袱的接口上转换一下编码。
典型乱码
乱码、问号、方块
用文本编辑器打开一个文件,如果编码不兼容,有时候会看到??????的东西,有时候会看到一团乱七八糟的文字,通常我们就统称乱码了。怎么用编码的知识来理解呢?
前文中我们有说到实用的很多编码方式都用的是变长字节编码,很多字节都要结合它的上下文去解释才是对的。例如:用UTF-8的算法去解析GBK的文件,就很容易发些这么些种情况:
- 一个字节序列并不是合法的UTF-8字符,比如以11111110开头的字节序列。
- 一个字节序列碰巧符合UTF-8规则。
反过来看,用GBK的算法去解析UTF-8的文件其实也差不多,遇到第一种情况在显示的时候可能就用问号代替,而遇到第二种情况就是出现一些风马牛不相及的杂乱文字。
方块其实和问号本质上一样的,但方块在现代浏览器里还有个很常见的情况,就是一个字符的编号在字体当中并没有定义,于是在排版和渲染的适合“智能”地用一个方块来表示它了。看到方块可以结合上下文,如果上下文当中的非英字符显示正确的,那么方块可能是一些特殊符号,比如Emoji。
在写服务端程序的时候要小心处理“半个字符”的问题,例如我们在前级对超长的数据进行截断处理,刚好截断掉一个变长编码的字节序列,就会出现“半个字符”。一般半个字符都是铁定会乱码,一些容错比较差的程序甚至会挂,比如一些做的不好的PHP的C扩展,严重的时候会出core。所以程序不懂编码就别瞎截,甚至考虑到某些语言文字里的组合字符,就是知道编码也别瞎截(真是细思恐极);
BOM
BOM(Byte-Order Mark,字节序标记)是Unicode码点U+FEFF
。它被定义来放在一个UTF-16文件的开头,如果字节序列是FEFF
那么这个文件就是大端序,如果字节序列是FFFE
那么这个文件就是小端序。
UTF-8本身是没有字节序的问题的(因为它是以单个字节为最小单位),但是Windows里面很多编辑器(比如记事本)会多此一举的在UTF-8文件开头加入EF BB FF
也就是U+FEFF
的UTF-8编码。
如果你的PHP文件里面有一个这东西你就倒了大霉了,可能会:
- 什么也看不见,可能是PHP引擎根本处理不了这个源代码。
- 页面展现错乱的情况,一般是因为在
<doctype>
之前输出的非空格内容造成了浏览器选择错误的doctype。 - 页面上面有及格乱七八糟的字符,浏览器把它当字符展示出来了。
于是建议在Windows上做开发的同学,一定要选择“使用UTF-8无BOM格式”保存,用Notepad++的可以注意选一下,它支持的文件编码格式挺丰富的,用一些比较先进的跨平台编辑器比如WebStorm、SublimeText它们都是没BOM的。
锟斤拷
乱码之所以叫乱码,就是因为它是“乱”的。但是乱码当中最出名的就是“锟斤拷”,他出现次数太多了以至于看起来根本就没那么“乱”。这就纳了闷了,为什么全中国的网站乱码里面都会有这个?
原因是,在将一些国家语言编码体系,比如GB、BIG-5、EUC-JP等,转换为Unicode的过程中,多少有一些字符是不在Unicode中的(比如一些偏旁部首在Unicode里是后来才收录的),甚至它本身在原来的编码体系里面就是非法字符的情况。
Unicode规定了U+FFFD
当作一个占位符用来表示这些字符,用UTF-8编码它就是EF BF BD
,连续多个这样的字节序列出现就成了EF BF BD EF BF BD
。如果是一个UTF-8的解析程序还好,而如果用一个GB的解析程序去打开,一个汉字2字节,就成了“锟斤拷”。这里就是一个例子,用UTF-8编码打开是问号,用GBK编码打开的话就会看到锟斤拷,用hexdump或者UltraEdit这类任何16进制编辑器看的话就能看到里面都是EF BF BD
。
要避免锟斤拷一个重要的点就是尽量减少程序当中的编码转换。比如输入是UTF-8,但是一个旧的模块是GBK,把UTF-8转成GBK交给旧的模块处理,处理过程中旧模块多多少少有些BUG的可能,再转回来的时候就容易锟斤拷了。一个项目的源代码在团队里面被不同的人(他们编辑器配置不尽相同)开来开去,存来存去,也很容易出现锟斤拷。
烫烫烫、屯屯屯
这个和编码转换其实没啥关系,在VC的DEBUG模式下,会把未初始化的栈内存全部填成0xCC
,未初始化的堆内存填成0xCD
,这样做是让你一眼就能看出来你开了内存没初始化。
而用GBK编码的话,CC CC
就是“烫”,CD CD
就是“屯”。
URL Encode和Base64
URL Encode
URL Encode又称为“百分号编码”它主要用来在URI里面将特殊字符进行转义,因为像/
、&
、=
等等这类字符在URI里面本身是有功能性的。
对于ASCII字符的编码很简单就是用%
后跟ASCII编码的16进制表示,例如/
的ASCII char code是47
,16进制表示是2F
,于是它的URL Encode结果就是%2F
。
对于非ASCII字符,将它的每个字节进行相同规则的转换,例如中文“编码”的Unicode char code是U+7F16 7801
,UTF-8编码的字节序列是E7 BC 96 E7 A0 81
,所以它按照UTF-8编码的URL Encode结果就是%E7%BC%96%E7%A0%81
。
可以看出,URL Encode编码非ASCII字符的时候,结果与使用的字符编码有关。因此在页面上提交表单、发起Ajax请求等操作的时候需要注意编码。浏览器会按照当前页面所使用的字符编码对表单体提交进行URL Encode,但使用JavaScript的encodeURI
和encodeURIComponent
的时候则总是会使用UTF-8(参考MDN)。
表单提交的时候编码是非常非常重要的,一旦错了服务端解开数据的时候就会跪。比如Github在它们的搜索表单里面放了一个<input name="utf8" type="hidden" value="
✓
">
,其中那个对钩✓是U+2713
,UTF-8编码是E2 9C 93
,他们可以在服务端检测这个参数的值对不对从而对URL里用的编码进行一个初步检测。虽然我没有看到他们使用其他编码的情况,不过这样也算是一个编码协商和Check的手段吧。
在JavaScript中使用escape
也可以达到URL Encode的效果,但是它对于非ASCII字符使用了一种非标准的的实现,例如“编码”会被escape
成%u7F16%u7801
这种%uxxxx
奇怪的表示,W3C把这个函数废弃了,身为一名前端还用是打脸的哦。
Base64
Base64是一种用可见字符表示二进制数据的方法。它用了64个可见字符[A-Za-z0-9+/]
。
Base64的编码程序非常简单,由于64=2^6,6和8的最小公倍数是24,也就是3byte,因此对输入数据以3byte为一个单位,查表把它转换成4个可见字符。
如果输入末尾不足3byte,那就补足,补1个byte就在输出末尾添加一个=
,补2个byte同理。
Base64经常用来在一些文本协议里面保存二进制数据,比如HTTP协议,或者电子邮件的附件啊什么的。同时因为它的输出对于人类而言不可读,可以起到一些“混淆加密”的作用,事实上就有修改64个字符的排布来做一个变形Base64实现一个简单加密算法的例子。从密码学的角度看它基本上没什么强度可言,但是足够简单,可以起到防君子不防小人的作用。
由于一个字符只能编码6bit,自身却占了8bit,8/6=1.33,因此使用Base64来表示数据的时候会浪费1/3的体积。对于在CSS里面用Base64的data-url方式表示图片,用之前不妨简单估算一下,膨胀的体积和一个HTTP请求头比起来会相差多少,说不定涨太多了已经损失掉省一个请求的收益了。