文档章节

引入编码信息的一些实践——乱码探源(3)

国栋
 国栋
发布于 2015/05/22 11:49
字数 1847
阅读 406
收藏 2

前面说到,文本文件中没有编码信息,导致了各种混乱,那么,最关键的就是要指定好所用的编码信息。具体地讲,有以下一些途径。

变相引入

什么是变相引入呢?其实本质与前面提到的一些“文件头”信息是类似的。

xml

我们来看看xml文件的例子,你通常能在最开始看到这样的一行:

<?xml version="1.0" encoding="UTF-8"?>

那么这里面,encoding指明的就是所用编码的信息了。

可是,等等!!为了得到这一编码信息,我得先读取这一文件;可要正确读取文件,我又要先知道编码信息!

这成了一个鸡生蛋,蛋生鸡,又或者说是先有鸡还是先有蛋的问题了。

怎么破呢?考虑这一行信息所有字符都是ASCII中的字符,那么我们可以先使用最基础的ASCII去读取它开头的一些信息,获取到这一编码信息后,再次用这一编码去读取文件即可。

ASCII可谓是这样一个始祖鸟或者始祖蛋一样的存在。

可以动动手做些实验,先建立一个xml文件,比如就叫foo.xml

image

内容如下:

<?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编码,又同时录入了中文,还会出现保存时的警告:

image

自然,像记事本这样傻乎乎的编辑器就没有这么贴心了。这时,保持宣称编码与实际保存用的编码一致就是源文件作者的责任了,否则可能不但没有帮助还会误导编辑器。

ruby,python

像ruby,python之类的语言有时会在文件头加上如下声明:

# -*- coding: utf-8 -*-

那么,这也算是变相引入的编码信息。自然,这需要相应的源码编辑器及编译(解释)器的支持。

但是,像java这样的语言似乎没有这样的约定,那么要怎样才能尽可能避免出错呢?

外部指定

既然没有编码信息,又不打算用变相的方式指定,那么靠谱的方式就是外部显式指定了。

假如没有编码信息?

假如我们用UTF-8编码一个java源文件:

image

然后在cmd下用javac命令手动编译并执行:

image

我们发现乱码了,没有输出“你好”,而是三个怪字。原因实际上就是javac编译器用了缺省的编码,在Windows平台,也就是GBK去读取源文件。

“你好”两个字按UTF-8一个字三个字节,总共6个字节,而按GBK去解码,则两字节一个字,最终成为三个字。

注:也即生成的class文件就已经是有缺陷的了。

明确引入编码参数

纠正的方法也很简单,就是在编译时显式指定所用的编码:

javac -encoding utf-8 Foo.java

在加了“encoding”参数后再编译,就能正确的读取源文件从而生成正确的class文件,

注:如果你观察一下新生成的class文件,会比原来的小3个字节。这与class文件中所使用的“modified UTF-8”编码方式有关,可参考前面乱码探源1中的介绍。

再次执行,就正常了:

image

在工程中指定

每次编译时都要去指定这一编码是件很繁琐的事,通常是对一整个工程在一开始就设置一个明确的编码。

比如对于一个Eclipse下的工程,我们可以在工程属性里指定一个编码,比如用UTF-8:

image

这样之后,新建的文本文件如各种源代码文件都会使用这一编码。而当要编译时,也会使用这一编码去读取源文件。

当然,如果我们从外部引入一些文件,编码是不会自动转换的。

比如引入一些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,字节流及字符流的话题。

© 著作权归作者所有

国栋

国栋

粉丝 388
博文 79
码字总数 154046
作品 0
东莞
程序员
私信 提问
加载中

评论(2)

国栋
国栋 博主

引用来自“jeff712”的评论

坐等#乱码探源#第四篇,#字符集编码系列#都看完了,对字符集编码这一块豁然开朗。做些整理,删减和补充些内容,可以出书了。字数也不少。

顺便问两个问题:
1、Java做文件下载时,为什么要将文件名用ISO8859_1编成一堆乱码,浏览器中才能显示正常。这个是不是跟客户机的操作系统和浏览器都有关系?
2、字符集是如何转换为实实在在显示在屏幕上的文字,这里是否又有对应关系。是字体里面包含了默认的字符集,还是字符集会配个默认的字体?
1.这点还没有认真去研究过,要正确显示的关键是编码和解码的一致,用什么编码这不是重点,也可能是html规范中有相关规定。 2.没有深入研究过显示的问题。我感觉首先是要在内存中统一编码,比如用UTF-16,再用这一编码去检索字库。
jeff712
jeff712
坐等#乱码探源#第四篇,#字符集编码系列#都看完了,对字符集编码这一块豁然开朗。做些整理,删减和补充些内容,可以出书了。字数也不少。

顺便问两个问题:
1、Java做文件下载时,为什么要将文件名用ISO8859_1编成一堆乱码,浏览器中才能显示正常。这个是不是跟客户机的操作系统和浏览器都有关系?
2、字符集是如何转换为实实在在显示在屏幕上的文字,这里是否又有对应关系。是字体里面包含了默认的字符集,还是字符集会配个默认的字体?
确定文本文件的编码——乱码探源(2)

在上一篇中,探讨了文件名编码以及非文本文件中的文本内容的编码,在这里,将介绍更为重要的文本文件的编码。 混乱的现状 设想一下,如果在保存文本文件时,也同时把所使用的编码的信息也保存...

国栋
2015/05/11
6K
35
字符集与编码(一)——charset vs encoding

注:由于两边同步的麻烦,更多更改及调整可参考我的网站:xiaogd.net 上的字符集编码与乱码系列,已将字符集编码系列与乱码探源系列合并,更新及勘误等不再更新到这边。 许多时候,字符集与编...

国栋
2014/08/19
5.7K
18
文本在内存中的编码(2)——乱码探源(5)

在前面我们探讨了String是什么的问题,现在来看String从哪来的问题。 String从哪里来? 所谓从哪里来也可以看作是String的构造问题,因此我们会从String的构造函数说起。 String的构造函数 ...

国栋
2015/06/26
2K
2
文本在内存中的编码(1)——乱码探源(4)

让我们从一个故事开始说起。话说北大是很有哲学传统的,当你准备踏进北大校门时,连门卫都会连问你三个终极哲学问题: 你是谁?你从哪里来?你要到哪里去? 那么这与我们的问题又有何关系呢?...

国栋
2015/06/26
889
0
文件,文本文件以及编码——乱码探源(1)

在前面的字符集编码系列中,已经探讨了几大主要的字符集编码。在此基础之上,这里将进一步探讨编码的应用及乱码的根源,我们先从基本的文件说起。 文件 文件(内容)就是字节序列。文本文件也...

国栋
2015/05/05
1K
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring Cloud 笔记之Spring cloud config client

观察者模式它的数据的变化是被动的。 观察者模式在java中的实现: package com.hxq.springcloud.springcloudconfigclient;import org.springframework.context.ApplicationListener;i...

xiaoxiao_go
今天
4
0
CentOS7.6中安装使用fcitx框架

内容目录 一、为什么要使用fcitx?二、安装fcitx框架三、安装搜狗输入法 一、为什么要使用fcitx? Gnome3桌面自带的输入法框架为ibus,而在使用ibus时会时不时出现卡顿无法输入的现象。 搜狗和...

技术训练营
今天
4
0
《Designing.Data-Intensive.Applications》笔记 四

第九章 一致性与共识 分布式系统最重要的的抽象之一是共识(consensus):让所有的节点对某件事达成一致。 最终一致性(eventual consistency)只提供较弱的保证,需要探索更高的一致性保证(stro...

丰田破产标志
今天
7
0
docker 使用mysql

1, 进入容器 比如 myslq1 里面进行操作 docker exec -it mysql1 /bin/bash 2. 退出 容器 交互: exit 3. mysql 启动在容器里面,并且 可以本地连接mysql docker run --name mysql1 --env MY...

之渊
今天
7
0
python数据结构

1、字符串及其方法(案例来自Python-100-Days) def main(): str1 = 'hello, world!' # 通过len函数计算字符串的长度 print(len(str1)) # 13 # 获得字符串首字母大写的...

huijue
今天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部