编码歪传——Web篇

继续上一篇。

身为一名Web开发者,这一篇将介绍一下在Web应用当中常会出现编码问题的地方。文中经常会乱用“字符集”和“编码”,不过看明白了第一篇的话相信你不会混淆概念,而且我个人觉得这两个概念很多时候混淆也无妨……

概念

出于把问题描述得稍微清楚一点的目的,我打算先把我们的概念进行一下定义。

一般而言我们常遇到乱码的场景有这样两种:

  1. 作为写入端,我应该用什么编码来存储/传输
  2. 作为读出端,我应该用什么编码来消费我所收到的字节流?

因为我觉得绝大多数具体场景都可以归纳成上述两种,所以这样应该可以简化一下问题。

程序内部处理

现代编程语言一般都内建字符串作为自带的数据类型,一门强大且又实用的编程语言通常来说都有高效的字符串实现以及大量配套的字符串处理函数。

在上一篇中我们有顺带提到,UTF-16因为是一种在处理效率和存储空间之间比较平衡的,同时编码空间又足够大的编码方式,在一些编程语言当中被采用来当作字符串的内部编码。比如C#、Java(可能因JVM/JDK而异)。

一般而言编程的String类型编码都是固定的,但是通常会提供丰富的编码转换函数。一种(我认为)比较可靠的方式是:String用固定编码方式实现,以使得标准的字符串函数能够只关注一种编码,从而保证它的正确性,也能够最大程度地针对性优化;而通过使用类库来将String转换为特定编码的字节流,或将字节流以特定编码转换成String。

反过来看,像PHP里的字符串就比较糙,它的编码有很大问题,如果一个字符串是多字节的(通过上一篇我们了解到除了ASCII以外基本上常用的编码都是多字节的),处理它就要用mb_xxxx系列的函数。这对编程是一种负担,因为这样就意味着String类型对字符的抽象力度不足,还是得花很多精力去关注字符串的编码。对于PHP的程序一个办法就是在整个程序内部统一编码,同时基于此选择好使用那一组字符串处理函数(作为项目规范),避免程序内部关心编码的问题,只把编码暴露在与外界交换数据的地方。

存储/传输

管你是什么程序,程序所生成的东西总要被消费才有意义(不然就变成烤机程序了)。Web应用里最常见的两种对程序结果的消费方式,一是把它存储(数据库、文件)起来,二是把它传输给用户(浏览器)以供展现。

当需要存储/传输文本的时候,就需要高度关心字符编码了。

存储

很多人遇到的问题是把用户表单提交的东西写进MySQL里面以后乱码了,这个问题一些可能的原因有:

  1. 提交内容的字符编码
  2. 服务端程序(如PHP)内部使用的编码
  3. MySQL传输时候使用的编码
  4. MySQL数据库声明和使用的字符集

第1点下一步会更详细的展开。

第2点在上文当中有一定介绍了,PHP程序所接收的字节流被当作字符串看待后,我们的程序必须要选择合适的字符串处理函数,结果才会是对的。比如一个截断程序要能正确处理多字节编码,如果把多字节编码切断成“半个字符”严重的时候甚至会造成PHP出core。

第3点就是PHP中常见的mysqli_set_charset所覆盖的范围,没错,因为MySQL其实是服务,所以这个存储其实也是传输。

第4点就是在建库建表的时候选的那个字符集和编码。

这当中的重点的就是2需要对1的编码有预期,正确的把1的字节流解析出来,转换成程序内部字符串实现所使用的编码,套用正确的算法,接下来与MySQL驱动和服务之间使用双方预期的编码,最终以数据库定义的时候所声明的字符集保存下来。

传输

一个HTTP请求发出的时候,用户代理(UserAgent,通常是浏览器)可以通过HTTP Request Header中的Accept-Charset字段来显式声明预期返回的编码,这是一种协商手段。现在的浏览器都很流弊,啥编码都能解析,于是直接懒得发这个,言下之意就是服务端给返回什么就消化什么。

对于服务端而言,如果收到的请求指定了Accept-Charset那么应该按照请求者的预期来决定响应内容的编码,如果没有指定,则可以“自由发挥”,这种时候理论上说你用什么编码都可以,但最终都必须通过某种手段告诉请求者响应内容是什么编码。

方式1:使用HTTP Response Header中Content-Type来给响应内容声明编码。比如Content-Type: text/html; charset=UTF-8。这里有个小插曲,在IE6(没记错的话)里用Ajax请求的时候如果Response写的是小写utf-8就会跪,必须要大写。别问我为什么知道,说起来都是泪,那是一个风雨交加的深夜……

方式2:通过HTML页面头部的<meta charset="xxx">标签来给页面声明编码。如果Response Header里不写编码,浏览器就会尝试找这个标签,然后将接下来的内容以这个编码解读。这就是为什么我们提倡将<meta charset="xxx">写在<title>标签之前的原因,如果<title>出现在此之前,它里面的字符就不知道该用什么编码来解读了,直观的说就是可能造成title乱码。

一旦决定了编码,服务端程序就会将字符以该种编码最终写入字节流,传给客户端。

那如果两种方式都用了,口径却不一致会怎么办?首先当然是给开发者赏两耳光,然后有兴趣的可以做做实验看看不同的浏览器会有什么不同的兼容策略。

用户提交内容

上面有说表单提交也有个编码的问题,其实包括Ajax请求等,只要是客户端向服务端发送内容,都一样,但通过上面的例子我想你已经明白了,这完全是镜像的,这次浏览器扮演着信息的生产者的角色,本质是完全一样的。

消费

给你一本书,你怎么知道它是中文版还是英文版?“我靠,它用英文写的就是英文版,用中文写的就是中文版啊。”

人类的大脑简直聪明得要命了,这种问题根本不需要动脑子,计算机就要笨多了。其实并不是计算机笨,而是这个问题在计算机的领域里面太难了。比如上一篇文章说到GB2312是兼容ASCII的,那么如果收到的内容前几个字节是3C 68 74 6D 6C 3E也就是<html>的ASCII编码,也许臆想它是ASCII的,于是后面出现的双字节字符可能就会遭殃了。UTF-8有一个很不错的性质是它比较容易识别,但是也有错误率和效率问题。所以这些你猜来我猜去的不靠谱的倒霉事情就只让它出现在男女情爱当中吧不要来污染我们纯净的计算机世界了好吗。

上面一节当中有说到,一个靠谱的信息生产者,会在给你传递信息的时候协商或声明编码。身为一个合格的信息消费者,浏览器可以通过这些声明来选择正确的编码,解读字节流。

浏览器也是个程序,于是它内部也会有字符串实现,也许它用自带字符串的语言实现的,也许它用自己实现的字符串(如C/C++),不管怎样,有了明确的编码,浏览器都能够将所获得的字节流转换成自己所使用的内部编码。

事已至此,似乎只要生产者靠谱,消费者要注意的问题就非常少了。在服务端我们小心翼翼地处理那么多环节的编码问题,到了浏览器好像已经完事儿了。不管这之前有再多波折,浏览器内部各种对字符的处理再多,基本上都不会有编码的问题了,简直太没劲了,于是这里稍微发散思维一下。

接下来浏览器就需要把字符显示出来,我们考虑浏览器通过操作系统给它提供的API。API要么规定编码要么协商/声明编码对吧,如果是前者,浏览器需要把自己内部用的编码转换成API所预期的编码,然后调用API——在这个场景里面,浏览器又从信息的消费者变成生产者了对吧,而这次操作系统是消费者。

然后我们假设操作系统将会用某种字体渲染这段字符,字体文件内部一般都对每个字符进行编号,现代的字体一般都会用Unicode,没错,我们又回到了字符集的概念。操作系统将字符编码还原到字符集当中的字符编号(显然对于变长字节编码这个过程要一些运算),在字体文件内通过编号查到这个字符,一个设计良好的字体可能对同一个字符会设计了多个字形(Glyph),比如Regular体一个、粗体一个、斜体一个,甚至还有更多更多,比如组合字符、一些特殊规则下的变形字符,不展开讨论。

这些渲染规格都是在API里指定好的,然后就用对应的字形来进行渲染。渲染字形这事儿还不是一个简单的事情,字体分点阵的、矢量的(甚至图片的?),不同的渲染引擎,例如Windows上的GDI、DirectWrite、第三方的GDI++、MacType,还有OSX的渲染引擎,Linux不同的桌面系统的渲染引擎,在最终把字形绘制成像素点的算法上有细节区别。

上面说的还只是渲染单个字符的时候的问题,在此之前还要做文字的排版啊什么的,哪怕看起来很小一件事情也够人钻大半辈子了。我的天,人类为了在计算机上展示文字到底下了多少功夫?

好的好的,刚才似乎发散的太多了,就此打住,总之就浏览器而言对于一个HTML页面的消费差不多是可以理解了。

阶段性小结

把乱码的问题从一个信息的生产者和消费者两个角度来看,中间所经历的哪些环节涉及到编码,哪些环节涉及到编码的协商与声明,就明确多了。上面的例子其实很容易就可以举一反三。

于是一些常见的诸如“PHPMyAdmin里看是正常的,页面上是乱码”或者“页面上是正常的,PHPMyAdmin里看着是乱码”这种问题可能会是哪些个环节闯的祸心里就已经有谱了。对于各种接口,比如与MySQL通讯,比如与后端之间的接口,如何协商/声明编码,什么时候需要转换编码,心里面也有谱了。

预报

呵呵呵呵,这次的内容虽然没那么理论,但是还是太简单了嘛,看到乱码就查编码呗你当我是傻X呢。

这时候也有观众吐槽:“那么各种程序当中用的编码比如URL Encode、Base64又是些啥玩意啊老湿?”

也有好奇心过盛的观众要问:“问号和方块是怎么回事?屯屯屯烫烫烫锟斤拷又是些什么鬼呢老湿?”

对于上面的问题我只想说四个字:请联系我请看终篇:《编码歪传——番外篇》