前面说到, 文本文件中没有编码信息, 导致了各种混乱, 那么, 最关键的就是要指定好所用的编码信息. 具体地讲, 有以下一些途径.
变相引入
什么是变相引入呢? 其实本质与前面提到的一些"文件头"信息是类似的.
xml
我们来看看 xml 文件的例子, 你通常能在最开始看到这样的一行:
<?xml version="1.0" encoding="UTF-8"?>
那么这里面, encoding 指明的就是所用编码的信息了. 可是, 等等!! 为了得到这一编码信息, 我得先读取这一文件;可要正确读取文件, 我又要先知道编码信息!
这成了一个鸡生蛋, 蛋生鸡, 又或者说是先有鸡还是先有蛋的问题了.
怎么破呢? 考虑这一行信息所有字符都是 ASCII 中的字符, 那么我们可以先使用最基础的 ASCII 去读取它开头的一些信息, 获取到这一编码信息后, 再次用这一编码去读取文件即可.
ASCII 可谓是这样一个始祖鸟或者始祖蛋一样的存在.
可以动动手做些实验, 先建立一个 xml 文件, 比如就叫 foo.xml
内容如下:
<?xml version="1.0" encoding="UTF-8"?> <foo>向我开炮</foo>
然后初步测试读取编码信息
package org.jcc.core.encode;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.junit.Test;
public class EncodingDetectTest {
@Test
public void testEncodingDetect() throws Exception {
File foo = FileUtils.toFile(getClass().getResource("/foo.xml"));
// 以ASCII方式读取文件
String content = FileUtils.readFileToString(foo, StandardCharsets.US_ASCII);
// 匹配到首行, 并用group方式抓取编码的值
Pattern headerPattern = Pattern.compile("<\?xml[\s\S]*encoding="([^"]*)"\?>");
Matcher headerMatcher = headerPattern.matcher(content);
assertThat(headerMatcher.find()).isTrue();
assertThat(headerMatcher.group(1)).isEqualTo("UTF-8");
// 匹配foo节点中的内容"向我开炮"
Pattern fooPattern = Pattern.compile("<foo>([\s\S]*)</foo>");
Matcher fooMatcher = fooPattern.matcher(content);
assertThat(fooMatcher.find()).isTrue();
// 四个UTF-8字符, 每个三字节, 共12字节.
// 由于最高位都为1, 都不是有效的ASCII字节, 最终被替换成了12个�(见乱码探源2中的介绍)
assertThat(fooMatcher.group(1)).isEqualTo("������������");
}
}
注: 仅为演示用, 就写得比较粗糙了. 比如直接就把全部内容读取上来了, 精细一点应该是读取一行或者读取到所需信息就行了. 正则表达式也还可以写得更严谨些.
之后就可以进一步测试了:
@Test
public void testRereadUsingDetectedEncoding() throws Exception {
File foo = FileUtils.toFile(getClass().getResource("/foo.xml"));
// 获取xml中的编码信息
String encoding = getXmlEncoding(foo);
// 使用检测到的编码再次读取文件
String content = FileUtils.readFileToString(foo, encoding);
// 这次, 内容正确了.
assertThat(getTextInFooNode(content)).isEqualTo("向我开炮");
}
private String getXmlEncoding(File foo) throws Exception {
String content = FileUtils.readFileToString(foo, StandardCharsets.US_ASCII);
Pattern headerPattern = Pattern.compile("<\?xml[\s\S]*encoding="([^"]*)"\?>");
Matcher headerMatcher = headerPattern.matcher(content);
headerMatcher.find();
return headerMatcher.group(1);
}
private String getTextInFooNode(String content) {
Pattern fooPattern = Pattern.compile("<foo>([\s\S]*)</foo>");
Matcher fooMatcher = fooPattern.matcher(content); fooMatcher.find();
return fooMatcher.group(1);
}
这次内容正常了, 表明我们的策略是可行的.
html, jsp
像 Html 文件也常常会这样去引入一些编码的信息, 如在 header 里常会包括以下元信息:
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
又或者是像这样:
<meta charset="UTF-8">
智能一点的文本编辑器还会根据这一信息来作为保存时的编码. 比如在 Eclipse 中, 如果设定了用 ISO-8859-1 编码, 又同时录入了中文, 还会出现保存时的警告:
自然, 像记事本这样傻乎乎的编辑器就没有这么贴心了. 这时, 保持宣称编码与实际保存用的编码一致就是源文件作者的责任了, 否则可能不但没有帮助还会误导编辑器.
ruby, python
像 ruby, python 之类的语言有时会在文件头加上如下声明:
# -*- coding: utf-8 -*-
那么, 这也算是变相引入的编码信息. 自然, 这需要相应的源码编辑器及编译(解释)器的支持.
但是, 像 java 这样的语言似乎没有这样的约定, 那么要怎样才能尽可能避免出错呢?
外部指定
既然没有编码信息, 又不打算用变相的方式指定, 那么靠谱的方式就是外部显式指定了.
假如没有编码信息?
假如我们用 UTF-8 编码一个 java 源文件:
然后在 cmd 下用 javac 命令手动编译并执行:
我们发现乱码了, 没有输出"你好", 而是三个怪字. 原因实际上就是 javac 编译器用了缺省的编码, 在 Windows 平台, 也就是 GBK 去读取源文件.
"你好"两个字按 UTF-8 一个字三个字节, 总共 6 个字节, 而按 GBK 去解码, 则两字节一个字, 最终成为三个字. 注: 也即生成的 class 文件就已经是有缺陷的了.
明确引入编码参数
纠正的方法也很简单, 就是在编译时显式指定所用的编码:
javac -encoding utf-8 Foo.java
在加了 encoding 参数后再编译, 就能正确的读取源文件从而生成正确的 class 文件,
注: 如果你观察一下新生成的 class 文件, 会比原来的小 3 个字节. 这与 class 文件中所使用的 modified UTF-8 编码方式有关, 可参考前面乱码探源1中的介绍.
再次执行, 就正常了:
在工程中指定
每次编译时都要去指定这一编码是件很繁琐的事, 通常是对一整个工程在一开始就设置一个明确的编码. 比如对于一个 Eclipse 下的工程, 我们可以在工程属性里指定一个编码, 比如用 UTF-8:
这样之后, 新建的文本文件如各种源代码文件都会使用这一编码. 而当要编译时, 也会使用这一编码去读取源文件. 当然, 如果我们从外部引入一些文件, 编码是不会自动转换的.
比如引入一些 css 文件, 话说天下 css 一大抄, 你可能是从某网站直接抓取来的, 而很多网站由于历史等原因可能还是用 GBK 等编码.
这时你需要手工转换一下编码, 或者用一些批量转换的工具(如果数量很多的话)
手工转的话, 比如可以在记事本中先正确打开它, 再拷贝到工程中的一个新建文件再保存. 注: 编辑时, 内容在内存中都是转换成了统一的编码(在 Windows 下, 就是 UTF-16), 所以不同编码的文件间互相拷贝也是 OK 的, 只是保存时才再次转换成相应的编码.
在构建文件中指定
也可以在构建文件中指定源文件的编码, 比如 java 中用 maven 时可以这样指定:
<project>
// ...
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
// ...
</project>
如果你用的是 ant 或者 gradle 之类的, 也可自行查查文档要如何设置. 至于其它语言平台的构建平台, 如 grunt, make 之类, 读者可自行去了解.
总而言之, 越是明确地设置了编码, 才越能避免混乱的出现. 在下一篇, 我们再谈下在内存中的编码及相关的 string, 字节流及字符流的话题.