前言 preface

因为编程的原因,经成会遇到字符编码的问题。如开发工具使用的是UTF-8编码(推荐使用),然后需要导入一个从其它地方获取的工程项目,但是这个工程使用了GBK编码方式。这就导致了一个常见的问题——乱码。

android 中文转码格式_目标文件


虽然这里的代码是不影响,但是中文注释全部都乱码了,这可不好玩了,很影响对于代码的阅读,尤其是那种比较多源文件的项目。通常,我的处理方式就是,对于每一个文件,使用记事本依次另存为另一种编码或者使用Notepad++的编码转换。这里推荐一下Notepad++,编码转换使用还是比较方便的。但是一个一个的转换,这都是重复性的操作,有时候也是挺烦的。所以,就萌生了一个使用代码来解决的想法。虽然是一个看似很简单的问题,但是其实是很考验我们对于基础知识的掌握的。

想法 idea

这里对于文本文件要有一个理解,任何文件本质上都是字节文件,只不过文本文件使用了字符集编码。也就是说,打开文件显示的字符并不是文件里面存储的真实字符,文件里面存储的永远只是字节。 大致可以按照下图这样来比较简单理解这个过程,打开文件的过程是一个字符映射的过程。

从字节到符号的过程是解码,从符号到字节的过程是编码。

android 中文转码格式_目标文件_02


我们看到的字符是字节经过编码映射,然后机器将特定的字符显示到屏幕上面。例如下面这副图片,以“”字符为例说明。

android 中文转码格式_java_03


对于不同的编码集,字符""具有不同的编码值,这里编码值指的是编码字节的值。

注意这里这个Unicode编码不是龙字的UTF-8编码,这里有点误导人了,但是我们可以去其它地方查询龙字的UTF-8编码,或者也不用舍近求远,如果你了解这方面的知识的话,直接使用代码输出就行了。

演示 demo

这里通过一段代码来演示编解码过程,理解这个话的,基本上就知道上面所说的东西了。
这段代码需要一些关于字符、字节和编码的基础知识。

package dragon;

import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.nio.charset.Charset;

public class TestMain {
	public static void main(String[] args) {
		int 龙_GBK = 0xC1FA;
		int 龙_UTF8 = 0xe9be99;
		ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write((龙_GBK >>>  8) & 0xFF);    //依次将GBK编码的龙和UTF-8编码的龙写入输出流,这里由于GBK
        out.write((龙_GBK >>>  0) & 0xFF);    //是双字节编码, 因此只写入低两位即可,
        out.write((龙_UTF8 >>>  16) & 0xFF);  //UTF-8是可变长的编码方式,这里的话 龙 是三字节编码。
        out.write((龙_UTF8 >>>  8) & 0xFF);
        out.write((龙_UTF8 >>>  0) & 0xFF);
        byte[] data = out.toByteArray();
        Charset GBK = Charset.forName("GBK");
        Charset UTF_8 = Charset.forName("UTF-8");
        System.out.println("不同编码显示同一字符:");
        System.out.println("使用GBK编码表示的汉字: 龙--->" + new String(data, 0, 2, GBK));
        System.out.println("使用UTF-8编码表示的汉字:龙--->" + new String(data, 2, 3, UTF_8));
        System.out.println("同一字符编码为不同字符集表示:");
        System.out.println("龙 使用GBK编码表示:" + new BigInteger(1, "龙".getBytes(GBK)).toString(16));
        System.out.println("龙 使用UTF-8编码表示:" + new BigInteger(1, "龙".getBytes(UTF_8)).toString(16));
	}
}

运行结果

android 中文转码格式_android 中文转码格式_04

小结 brief summary

这个结果是不是很有趣呢!

0xc1fa(字节)——>GBK解码——>龙(字符)
龙(字符)——>UTF-8编码——>0xe9be99(字节)

一个很自然的想法是,如果把上面这两个过程合起来,不就可以完成不同字符集编码的转换了吗?
当然了,注意前提是需要转换的部分的字符在两个编码集中都是存在的。毕竟你是无法把一个字符集中不存在的字符映射过去。

通过上面的演示可以看出来,想要实现编码的转换,就是先将字节解码(本来的编码方式)为符号,再编码(希望的编码)为字节。

即:字节——解码——字符——编码——字节

注意解码和编码对应的字符集不同,所以原始字节和最终生成的字节是不同的。

android 中文转码格式_System_05


这里画的图是从UTF-8到GBK的转换,当然了,也可以反过来的。

实现 implement

测试文件 test file

首先提供这样的两个文件,因为我的window的cmd使用是默认的编码,应该是GBK,但是我的这个两个文件都是UTF-8编码的,所以,这里打开是乱码。你也不知道里面写的是啥了。

android 中文转码格式_android 中文转码格式_06


android 中文转码格式_System_07

最终代码 final code

package dragon;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.Objects;
import java.util.stream.Stream;

/**
 * 编码转换
 * UTF_8
 * GBK
 * */
public class EncodeTranslate {
	private File[] files;    //需要转码的文件名字符串
	private File desDir;       //转码后存放的文件目录
	private Charset srcCharset;  //文件当前编码
	private Charset desCharset;  //文件目标编码
	
	/**
	 * @param path 目标文件的路径
	 * @Param desPath 目的文件的存放路径
	 * @param type 目标文件的类型
	 * @param srcEncode 目标文件的编码
	 * @param desEncode 目的文件的编码
	 * 
	 * */
	public EncodeTranslate(String path, String desPath, String type, String srcEncode, String desEncode) throws FileNotFoundException {
		File dirFile  = new File(path);
		if (!dirFile.exists()) {
			throw new FileNotFoundException("文件路径不存在:" + path);
		}
		
		this.files = dirFile.listFiles((dir, name)->name.contains(type));
		if (this.files.length == 0) {
			throw new FileNotFoundException("文件路径下不存在相关类型的文件:" + path + "->" + type);
		}
		
		desDir = new File(desPath);
		if (!desDir.exists()) {   //目标文件夹不存在,就创建
			if (!desDir.mkdirs()) {  //目标文件夹创建失败,路径冲突
				throw new FileNotFoundException("无法创建目标路径:" + desPath);
			}
		} else {
			if (!desDir.isDirectory()) {
				throw new FileNotFoundException("目标路径不是文件夹");
			}
		}
	
		this.srcCharset = Charset.forName(srcEncode);
		this.desCharset = Charset.forName(desEncode);
	}
	
	/**
	 * java 8 的 Stream 的简单应用
	 * */
	public void encodeTranslate() {
		Stream.of(files).forEach(this::translate);
	}
	
	/**
	 * 把需要转码文件的中字符串按行读取出来,然后对其使用它本身的字符集解码成字节数组,
	 * 再对字节数组按照需要的字符集编码成字符串,然后写入目的文件中。
	 * */
	public void translate(File file) {
		
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), srcCharset))) {
			String fileName = file.getName();
			File newFile = new File(desDir, fileName);
			try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newFile), desCharset))) {
				String line = null;
				while (Objects.nonNull(line = reader.readLine())) {
					byte[] data = line.getBytes(desCharset);         // 完成转码最为关键的两步
					String newLine = new String(data, desCharset);
					writer.write(newLine);
					writer.newLine();
				}
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}



package dragon;

import java.io.FileNotFoundException;

public class Main {
	public static void main(String[] args) throws FileNotFoundException {
		if (args.length != 5) {
			System.out.println("请正确输入各个参数!");
			return ;
		}
		String path = args[0];
		String desPath = args[1];
		String type = args[2];
		String srcEncode = args[3];
		String desEncode = args[4];
		// 直接抛出异常,不做处理!
		EncodeTranslate encodeTranslate = new EncodeTranslate(path, desPath, type, srcEncode, desEncode);
		encodeTranslate.encodeTranslate();
		System.out.println("File Encode Translate Success!");
	}
}

简要说明

这里无论是读取还是写入都必须指定编码集合,否则是会使用系统默认的编码集,那样也是会出现问题的,因此建议对于任何的文本文件读取和写入都应该显示的指定编码集,以杜绝因此导致的问题。

例如:我最开始没有指定写入文件时的编码集,因为IDE使用的是UTF-8,但是我写入的是GBK编码的内容(我这里只是测试 UTF-8 转 GBK),导致打开文件仍然是乱码。这是一个需要注意的地方。!

//使用指定的编码集读取文件
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), srcCharset))

//使用指定的编码集写入文件
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newFile), desCharset))

测试输入 test input

这里我就不在cmd里面测试了,我直接使用IDE来测试了,注意下面的五个输入参数,以空格来分隔。

或者你也可以在cmd里面使用,也挺方便的。

android 中文转码格式_android 中文转码格式_08

测试结果 test result

测试输出信息

android 中文转码格式_android 中文转码格式_09


测试输出文件

android 中文转码格式_字符编码转换_10

测试输出文件是否转换成功

这里只选取Test1.java来测试即可 。

android 中文转码格式_System_11

总结 summary

自己动手实现想法的过程中,也学到了不少东西,因为编码的过程中会遇到一些问题,可能某些概念掌握的不清楚,或者自认为掌握了,实现起来的时候,才会发现,其实不是自己想的那么回事,哈哈!

这是我最开始认为的实现方式,我还沾沾自喜呢。我以为我先按照srcCharset的编码方式解码,再将其按照desCharset的编码方式编码就行了。当时如果你理解上面的话,就会发现这个根本就是不对的嘛! 结果是得到了,完全的乱码文件了,使用notepad++怎么转换也不行了。(当然了,如果再反过来使用代码,也许还有救吧!)

byte[] data = line.getBytes(srcCharset);         // 完成转码最为关键的两步
   String newLine = new String(data, desCharset);

后来我反应过来了(思索了一会儿),只需要 byte[] data = line.getBytes(desCharset); 这样即可,把字符直接转成desCharset编码即可。并且对于这个字符、编码、字节的概念理解又多了一点,这才是写这篇博客最大的收获吧!

我以前一直对这个编码集和字节的关系感到很好奇,当我开始学习Java的IO流的时候,乱码伴随了我好久,例如一开始遇到:UTF-8 BOM 问题时,网上有人回答的是无法解决,我当时还是相信的了(可见错误的答案对人的误解),当时还是坚持去查找,最后发现了这个BOM这个关键字,也就解决了开头的那个文件内容输出开头的那个 ? 问题。编码转换本身是包含很多细枝末节的知识的,就算是现在的我也只是了解到了一些皮毛的知识,这些暂时是够用了,但是还是需要继续学习才行。
随便提一下,这里我主要使用的 InputStreamReaderOutputStreamWriter 正好是我刚开始解决编码,学会使用的两个类,哈哈!这也不算是巧合吧,应该说是一种积累!

附 PS

这个程序还可以继续完善,我这里没有涉及文件夹嵌套,例如一个目录里面含有多个文件夹,每个文件夹还含有需要转换的文件。对于通常的项目工程来说,很多的包是必备的,所以对于这种类型的转换直接使用上面的代码是不行的。上面的代码只是一个简单的技术实现,如果想要适应更复杂的情况,还需要继续完善,例如加一个递归就更完美了。有时间的话,也许会补上吧!