网页中的编码与乱码(2)

深入介绍了文档内编码声明的应用,包括许多在静态文档和动态文档中的实验与测试的细节,以及其它的一些注意事项等。

目录
[隐藏]

接着上一篇中的讨论,也是先从“文档内编码声明”讲起,因为它是最直观也最容易控制的。

不过事实上也没有那么容易,它还是很容易受各种因素干扰,下面会详细介绍整个过程,囊括了静态文档响应和动态文档响应两种情况,以及各种其它注意事项。

文档内的 meta charset 编码声明

通过之前的介绍,你已经知道了所谓的文档内 meta charset 声明:

<meta charset=”gbk”>

以上为 html5 的标准写法。又或者这样(html4)

<meta http-equiv=”Content-Type” content=”text/html; charset=gbk”>

在静态文档中的实验

假如现在有两个文档,一个声明是 gbk 的,一个是 utf-8,然后都正确按照了所声明的编码保存:

html meta charset gbk utf-8

这里特别要注意的一点是,有的时候,不是说你声明了某个编码,你的文档就会自然而然地就用这个编码来保存。这个一致性是要由源码的作者来保证的。详情见本文后面的章节。

之后部署然后在浏览器中打开它们,正常情况下应该都能正确显示:

 html meta charset browser test

如果两者不一致,那么结果将会是乱码。比如下面的页面 meta 宣称是 gbk 编码,实际保存所用的却是 utf-8:

html meta charset 编码声明与实际所用编码不一致

当在浏览器中打开时,将发生乱码:

页面编码声明与实际编码不一致导致的乱码

实际上,这个文件在 IDE 中,比如在 Eclipse 中打开时也是乱码的:

html 源代码乱码

因为较为智能的 IDE html 编辑器在打开这个文件时会参考里面 meta 声明的值来解码,反而被误导了,跟浏览器类似。

事实上,智能的 IDE html 编辑器在保存时也会参考 meta 声明的值来选择保存时的编码。这种不一致的情况要在外部的简易编辑器中去构建,比如上面的 notepad++ 编辑器,更具体的一些介绍参考本文后面的章节。

在某些浏览器中,发生乱码时你可以手动的调整编码以获取正确的显示:

手动调整乱码的网页编码

如上,开始是乱码的,此时展开“菜单–文字编码”会发现选在了 “简体中文”一项上,这正是 meta 中的建议,但很不幸它是错误的建议;修正为 Unicode 后将获得正常的显示结果。

当然,以上实验里要保证一点,那就是响应头中 Content-Type 不应该有 charset 信息。前面说到,它的优先级是要高过文档内编码声明的。

实验中的一项重要原则就是:隔离。要确保变量是受控的,这样才能得出可靠结论。

所以,进行上述实验时请确保响应头中没有编码声明,像这样:

响应头 content-type 不带 charset

或者甚至连 Content-Type 条目本身也不出现。

我实验用的是 tomcat8 这个 web server,对于静态页面,缺省配置下,它不会试图往 Content-Type 中塞入 charset 信息。事实上,它下面可能有非常多的页面,哪个哪个用了什么编码来保存的它也不知道。前面还说过,文本文件中保存后经常是不包含有编码信息的,具体参见:

确定文本文件的编码——乱码探源(2)

一个文本文件最终也是一个字节序列,对于静态文件请求,tomcat 它只要把你请求的文件一字节一字节发到浏览器端就完事了,它根本不会操心这些字节到底是什么编码。这一过程跟 FTP 下载文件没有什么本质区别,就是一个简单的复制传输过程。

那你可能会好奇,那有时不是响应头中 Content-Type 还带有 charset 信息吗?这难道不是 tomcat 尝试去获取后并告诉我们的?还真不是。Content-Type 的 charset 信息是一个“配置”的值,是由我们指定的。

具体如何在 server 或项目中去配置它,后面讲到 Content-Type 中的 charset 声明时再具体说。

现在假如你为所有的 html 响应配置了一个全局的 Content-Type,其中 charset 值为 utf-8,像这样:

Content-Type: text/html; charset=utf-8

那么这时你再次请求先前正常的 gbk 文档时就会发生乱码:

content-type charset 优先导致的页面乱码

因为它的优先级要高过文档内的声明:

响应头编码优先页面编码声明

如果草率地配置一个全局性的带 charset 的响应头,是有可能带来混乱的!

而你请求那个原本正常的 utf-8 文档时就不会有问题,虽然浏览器还是优先采用这个响应头中的声明,但它也正好与文档内的声明一致,所以不会有问题。

事实上,此时请求那个原本因不一致而不能正常显示的页面此时却也正常了,因为响应头的编码盖过了那个错误的 meta 声明,而又恰好与实际所用编码一致,所以反而正常了。

这时,反而是掩盖了实际存在的问题!所以,多种方式并存并相互影响的实际会让事情变得很复杂。

那么以上就是静态文档中实验,就介绍到这里。

动态文档中的实验

除了静态方式,还可以动态的构建一个 html 响应。这时服务端并不存在一个具体的 html 文件,一切都是动态生成并实时发送到浏览器客户端。

这样的技术有很多,什么 php,asp,jsp,不管什么 p,简单讲,最终还是生成 html。

在 Java Web 中,可以使用 servlet 或 jsp(本质上也是 servlet) 的方式,还可以灵活地控制响应头信息。比如像下面这样:

servlet 中动态构建带 meta charset 声明的响应

这里为简单起见,往浏览器端的输出流中省略了 doctype,html 根元素,body 元素等,一般情况下你应该包含一个 html 文档的完整结构,像前面那些静态文档那样。(但这种情况下在 servlet 中直接构建响应流会特别繁琐,这也是 jsp 这些模板技术出现的原因。)

或这样:

servlet 中动态构建带 meta charset 声明的响应

响应头中的 Content-Type 没有 charset 信息,

这里也可以直接用 response.setContentType(“text/html”) 来达到同样目的。

浏览器依赖包含在输出流中的 meta charset 声明,也能正确解析文档:

动态构建的带 meta charset 声明的页面在浏览器中的展示

注意:动态构建的文档你同样需要保证字节流与宣称编码的一致性。这里具体就是 getBytes 中使用的编码要与 meta 声明的一致:

getBytes 使用的编码与 meta 声明的一致的示例

如果不一致:

getBytes 使用的编码与 meta 声明的不一致的示例

同样会导致乱码:

getBytes 使用的编码与 meta 声明的不一致导致的乱码示例 

关于动态文档响应的实验就介绍到这里。

鸡生蛋与蛋生鸡的问题

看到这个小标题你可能有点困惑,这是要讲什么?其实当我们说到利用文档内的编码声明来解析文档时,你是否有过一些疑惑呢?我们打个比方来说吧:

假如你想打开一个锁着的箱子,可钥匙却在箱子里锁着,那你该怎么办呢?

文档内的编码声明跟这个有点类似,注意这个“内”字,编码信息是文档内容本身的一部分。你想要正确的解析文档,你首先要知道编码;而要知道编码,你又要先解析文档……

所以这构成了一个死局,关于怎么破解这个死局,其实在之前的引入编码信息的一些实践——乱码探源(3)中有过介绍,如果你感兴趣,可以去看看,其中还有一段利用正则表达式去获取编码信息的小程序示例。

简单讲,就是先用所谓的“嗅探”加“猜测”的方式先做些尝试,以尝试获取编码信息,然后再用所获取的编码重新解析整个文档。

为提高嗅探的效率,html 规范建议“文档内的 meta 编码信息”应尽量放在前 1024 个字节内。简单讲,就是你最好就把它声明在 head 标签的第一行,在 doctype 声明前面也不要写太多的注释,否则,嗅探算法扫描了前 1024 个字节仍然没有找到 meta charset 声明的特征字节码时,就可能放弃,认为你的文档内没有包含编码声明。

其实像 xml 那样把编码信息声明在第一行是最好的:

<?xml version=”1.0″ encoding=”UTF-8″?>

构建静态文档时的注意事项

在前面,提到构建静态文档时有一些注意事项,为保持行文紧凑,没有过多展开,现在在这里统一做个补充。

在现代智能的 IDE 中,比如我用的 Eclipse,它的 html 编辑器会关注你所用的 meta 声明。如果你的文档中带有 meta 如下:

<meta charset=”gbk”>

那么它就会自动用 gbk 编码保存此文档,即便所在工程的缺省编码是 utf-8 时也是如此。还要注意文档中不能包含 gbk 不支持的字符。

如果加入了 gbk 不支持的字符,则保存不了。你可能好奇保存前为啥又还能显示呢?这是因为保存前内容都存在于内存中,在 Windows 系统中,统一用 UTF-16 编码,直到按下保存时才会转换成 gbk 的字节数组写入硬盘。这也是你为何可以在不同编码间的文档中能互相拷贝内容的原因。

这样的一个好处就是保证了“保存文档实际所用的编码与 meta charset 中宣称的编码一致”。

如果你此时又把 charset 的值改为 utf-8,则 html 编辑器又会按照这个编码保存文档。

你会注意到文件的大小发生了不小的变化,这不仅仅是因为 “utf-8”这个字符串 本身比 “gbk” 多两个字符(5:3),更重要是因为 gbk 用两字节保存一个中文字符,而 utf-8 多数情况下用三字节保存一个中文字符,所以如果原文档中中文字符较多,最终文件大小就会发生较大变化。

注意,如果以 gbk 保存后你又想把它构建成一个缺省的不带 meta 声明的 gbk 文档,你不能简单地把 meta 信息删掉然后保存,这时 Eclipse 会按工程的缺省编码(有可能是 utf-8)而不是之前保存所用的编码来保存。这样你得到的有可能不是 gbk 编码的文档。

正确的做法是用其它的文本编辑器来保存,那些可以让你选择编码的编辑器。比如在 Windows 下你可以简单用记事本程序来做,使用“另存为”在弹出的框中的编码一栏里选择缺省编码(ANSI)即可(Windows 大陆版缺省的 ANSI 就是指 gbk(或是能兼容 gbk 的))。

另,如果你在记事本中编辑一个 html 文档,然后加入一个 meta charset 为 utf-8 的声明,如下所示:

<meta charset=”utf-8”>

请特别注意,记事本没有 IDE 中的那些专职的 html 编辑器那么聪明,它不会按你这里声称的编码来保存(根本不知道这是什么鬼),除非你用“另存为”然后显式选择 utf-8 编码(还要指出一点,这里的 utf-8 是带 BOM 的~),否则它就用缺省的 ANSI 也即是 gbk 来保存。这样就导致了“实际所用编码与宣称的不一致”,而后将有极大概率误导浏览器的解析过程,最终造成页面展示时的乱码。

所以,在你亲自来进行实验时,在你把文档交付给浏览器解析前,请务必确保两者的一致性,否则你的实验将建立在错误的前提下,你将受到干扰,无法得到可靠的结论。

如果你不确定你的文档实际到底用了什么编码,终极的方式是查看二进制。至少就 gbk 与 utf-8 的中文编码而言,utf-8 的二进制模式具有鲜明的特征,这是由它的编码方案决定的,与 gbk 的区分度还是非常大的。

关于 utf-8 与 gbk 这些编码特征的一些介绍,参考字符集与编码(四)——Unicode字符集与编码(九)——GB2312,GBK,GB18030

确定文本文件的编码——乱码探源(2)里曾介绍了一个查看二进制的工具,有兴趣可以去试试。

不过我现在发现这个二进制工具有时不太稳定,有的情况下会把 gbk 错误转换成了 utf-8.

如果你能深入到二进制里,了解这些模式,了解两种编码的区别,对你解决很多乱码问题会有很多帮助。

关于文档内编码声明的介绍就到这里,下一篇将具体分析响应头中的编码信息及冲突时的优先级选择问题。