工作中发现从某公司的BI系统中导出的csv文件,其中所有的中文字符都不能正常显示,但是英文、数字、换行符、Tab均正常显示。
使用Word和Notepad++,试了所有的Encoding,都不能正常所显示。于是怀疑是数据遭到了不正确的二次转换所致。后经反复试验,发现果然如此。原始数据在数据库中应该是以GBK形式储存,在导出csv文件时,程序错误使用了不支持中文的Windows-1252 to UTF8函数,把所有用GBK表示的两个字节的汉字拆开,每个字节当成一个带音调符号的拉丁字母(十进制128-255范围内的字符,比如ÈÕÏú),然后把这些拉丁字母转换成了UTF-8,导致乱码。
在纯英文的Windows系统环境下 ,可以直接使用Notepad++对此类乱码进行转码处理。
具体方法为:
一、首先确保操作系统的System Locale也设为英语: Control Pannel -- Region -- Administrative -- Language for non-Unicode programs也需要设置为English。
二、使用Notepad++打开包含乱码的文件,点击菜单栏中的Encoding -- Convert to ANSI,将文件转换为系统默认的ANSI-US编码,即Windows-1252。如果是中文系统,这步操作会将就文件转换为GBK,导致转换失败。因为ANSI是一个广义的编码标准,根据不同的语言环境会变化,GBK也是一种ANSI编码标准。
三、再点击Encoding -- Character sets -- Chinese -- GB2312(Simplified Chinese),以GB2312编码解析二进制源码,就会看到熟悉的汉字!
如果手边没有纯英文Windows系统的机器,可以尝试用Microsoft App Locale(Win 7) 或Locale Emulator (Win 10)来模拟纯英文系统环境。
如果,你不确定你乱码的原始编码是什么、应该如何转换回去,可以尝试 segment fault 网友rebiekong介绍的乱码原始编码推测网站:http://www.mytju.com/classCode/tools/messyCodeRecover.asp?from=RebieKong
另外,我又写了一个Java代码来解决这个问题:
使用方法为:
1、支持Windows、Mac,但首先你需要安装Java虚拟机,请访问java.com下载安装;
2、已经装过的朋友,请在腾讯微云上下载该程序的Jar包:http://url.cn/53sc8Dg ;
3、下载之后,把Jar包和需要转换的乱码文件,放在一个文件夹内;
4、Windows用户请在开始菜单内搜索“命令提示符”(command prompt),Mac用户请用spotlight搜索 终端(Terminal),找到后单击打开;
5、在命令行界面内,使用cd命令进入存放jar包的文件夹内。比方说,对于Windows用户,如果你的jar包存在D:文件,请先在命令提示符内键入D:,敲回车,然后再键入cd 文件。Mac用户没有分区的问题,直接cd + 绝对路径就可以了,比如cd /Users/username/Desktop/ ;
6、键入 java -cp convert_jre1.8x64.jar convert.twoTimeConvert + 参数 ;
该程序支持的参数列表为:
inputFilePath, outputFilePath, [inputEncoding], [middleEncoding], [originEncoding], [outputEncoding]
参数使用空格分隔。其中前两个参数必填,后4个参数可选。
inputFilePath:需转换的乱码文件的文件名。
outputFilePath:转换后文件的文件名。如该文件已存在,将覆盖。
inputEncoding:乱码文件目前的编码方式。以前文的例子为例,该参数应填写UTF-8。默认值为UTF-8。
middleEncoding:首次转换需要转至的编码。以前文的例子为例,该参数应填写Windows-1252。默认值为Windows-1252。
originEncoding:乱码文件最原始的编码。以前文的例子为例,该参数应填写GBK。默认值为GBK。
outputEncoding:最后输出文件的编码。以前文的例子为例,该参数可填写:GBK、UTF-8、UTF-16等支持中文字符的编码。默认值为UTF-8。
比如:java -cp convert_jre1.8x64.jar convert.twoTimeConvert 乱码.txt 转换结果.txt UTF-16 Windows-1252 GBK UTF-8
或者:java -cp convert_jre1.8x64.jar convert.twoTimeConvert 乱码.txt 转换结果.txt UTF-16
输入完成后按回车,如果有报错信息,屏幕上会输出。如果没有错误,转换结果.txt 应该已经出现在文件夹里了。
该程序的代码如下。托管在Github上,欢迎添砖加瓦:https://github.com/kind03/Job/blob/master/src/convert/twoTimeConvert.java
package convert; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Arrays; /**<p> * 本段代码用于恢复中文乱码,主要针对被错误转换后导致无法通过直接选择文件内码进行恢复的乱码。 * 比如一段GBK编码的文本,某程序错误使用了不支持中文的Windows-1252 to UTF-8函数进行转换, * 导致所有中文全部变成了带音调符号的拉丁字母,比如Æ·Ãû。这时候可以把乱码从UTF-8转换回Windows-1252, * 再使用GBK解析,得到中文。</p><p> * 本程序可以使用2-6个参数: * inputFilePath, outputFilePath, [inputEncoding], [middleEncoding], [originEncoding], [outputEncoding] * </p><p>参数使用空格分隔。其中前两个参数必填,后4个参数可选。</p><p> * inputFilePath:需转换的乱码文件的路径。</p><p> * outputFilePath:转换后文件的路径。如该路径指向的文件已存在,将覆盖。</p><p> * inputEncoding:乱码文件目前的编码方式。以前文的例子为例,该参数应填写UTF-8。默认值为UTF-8。</p><p> * middleEncoding:首次转换需要转至的编码。以前文的例子为例,该参数应填写Windows-1252。默认值为Windows-1252。</p><p> * originEncoding:乱码文件最原始的编码。以前文的例子为例,该参数应填写GBK。默认值为GBK。</p><p> * outputEncoding:最后输出文件的编码。以前文的例子为例,该参数可填写:GBK、UTF-8、UTF-16等支持中文字符的编码。默认值为UTF-8。</p><p> * 该程序所支持的编码为所有Java所支持的编码类型,请参考:http://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html</p><p> * 我在GitHub上提供了测试用乱码文件,可以进行测试。https://github.com/kind03/Job/blob/master/test_resources/MessyCodeGBK-Windows1252-UTF.txt</p> * @author 何晶 He, Jing * @version 1.3 2017/11/9 * */ public class twoTimeConvert { //由于转换大文件需要分块处理,segmentSize为分块大小,默认为4096字节,可以自行改动。 //关于文件分块的介绍请见segmentConvert()方法。 public static final int segmentSize = 4096; private static String inputCode = "UTF-8"; //ISO-8859-1 or Windows-1252 are both fine private static String middleCode = "Windows-1252"; private static String originCode = "GBK"; private static String outputCode = "UTF-8"; private static String inputPath; private static String outputPath; public static void main(String[] args) throws IOException { if (args.length >= 2) { inputPath = args[0]; outputPath = args[1]; } if (args.length >= 3) inputCode = args[2]; if (args.length >= 4) middleCode = args[3]; if (args.length >= 5) originCode = args[4]; if (args.length >= 6) outputCode = args[5]; if (args.length > 6 || args.length<2) { System.err.println("Wrong number of arguments! Got " + args.length + " arguments. This script requires 2 to 6 arguments: " + "inputFilePath, outputFilePath, " + "[inputEncoding], [middleEncoding], [originEncoding] ,[outputEncoding]." + "Arguments should be divided by spaces."); return; } segmentConvert(); } /** * <p>由于Java的CharsetEncoder Engine每次处理的字符数量有限,String类的容量也有限, 所以对于大文件,必须要拆分处理。</p><p> 但是由于UTF-8格式中每个字符的长度可变,且经过两次转换, 原来的GBK编码已经面目全非,不太好区分每个汉字的开始和结束位置。 所以干脆查找UTF-8中的标准ASCII的字符,即单个字节十进制值为0-127范围内的字符, 以ASCII字符后的位置来对文件进行分块(Segementation),再逐块转换。 但如果在默认的分块大小(Segment Size)一个ASCII字符都找不到的话,就会导致转换失败。</p><p> UTF-16也按照此原理进行转换。但由于UTF-16有大端(BE)和小端(LE)之分, 文件头部有时还有BOM,所以增加了BOM信息读取并通过BOM来判断是BE还是LE。</p><p> 对于其他编码,只要和ASCII码兼容,都适用于对UTF-8进行分割的方法。</p> * @throws IOException */ public static void segmentConvert() throws IOException { FileInputStream fis = new FileInputStream(inputPath); FileOutputStream fos = new FileOutputStream(outputPath); byte[] buffer = new byte[segmentSize]; int len; int counter = 0; byte[] validBuffer; byte[] combined; byte[] left0 = null; byte[] left1 = null; byte[] converted; //文件头部BOM信息读取 if ("UTF-16".equals(inputCode) || "UTF-16LE".equals(inputCode) ||"UTF-16BE".equals(inputCode)) { byte[] head = new byte[2]; fis.read(head,0,2); if (head[0]==-1 && head[1]==-2) { inputCode = "UTF-16LE"; } else if (head[0]==-2 && head[1]==-1) { inputCode = "UTF-16BE"; } else { left0 = head; counter++; } } while((len=fis.read(buffer)) == segmentSize) { //to check the value of len // System.out.println("len = " + len); int i = segmentSize - 1; if ("UTF-16LE".equals(inputCode)) { while (i>-1) { if ((buffer[i-1] >= 0 && buffer[i-1] <= 127) && buffer[i] ==0) { break;} i--; if (i==0) { //报错 System.err.println("File Segmentation Failed. Failed to find an " + "ASCII character(0x0000-0x0009) in a segment size of "+ segmentSize +" bytes "+"Plese adjust the segmentation size."); break; } } // i = segmentSpliter(buffer,"(buffer[i-1] >= 0 || buffer[i-1] <= 127) " // + "&& (buffer[i]==0)"); }else if ("UTF-16BE".equals(inputCode)) { while (i>-1) { if ((buffer[i] >= 0 && buffer[i] <= 127) && buffer[i-1] ==0) { break;} i--; if (i==0) { //报错 System.err.println("File Segmentation Failed. Failed to find an " + "ASCII character(0x0000-0x0009) in a segment size of "+ segmentSize +" bytes "+"Plese adjust the segmentation size."); break; } } }else { // the following segmentation method is not suitable for UTF-16 or UTF-32 // since they are not compatible with ASCII code while (i>-1) { if (buffer[i] <= 127 && buffer[i] >= 0) { break;} i--; if (i==0) { //报错 System.err.println("File Segmentation Failed. Failed to find an " + "ASCII character(0-127) in a segment size of "+ segmentSize +" bytes "+"Plese adjust the segmentation size."); break; } } } validBuffer = Arrays.copyOf(buffer, i+1); if (counter%2==0){ left0 = Arrays.copyOfRange(buffer,i+1,segmentSize); combined = concat(left1,validBuffer); left1 = null; } else { left1 = Arrays.copyOfRange(buffer,i+1,segmentSize); combined = concat(left0,validBuffer); left0 = null; } counter++; converted = realConvert2(combined,combined.length); fos.write(converted); } //for the end part of the document //can't use len=fis.read(buffer) since buffer has been read into in the while loop for the last time. if(len < segmentSize) { //to check the value of len System.out.println("len = " + len); if (len>0) { validBuffer = Arrays.copyOf(buffer, len); } else { //in case the file length is the multiple of 8 //in this case, the length of last segment will be 0 //only need to write what's in the left0 or left1 validBuffer = null; } if (counter%2==0){ //there is nothing left when dealing with the last part of the document //therefore no need to give value to left0 or left1 combined = concat(left1,validBuffer); } else { combined = concat(left0,validBuffer); } converted = realConvert2(combined,combined.length); fos.write(converted); // for test purpose System.out.println("================= last segment check ====================="); System.out.println(new String(converted)); } fos.close(); fis.close(); } public static byte[] concat(byte[] a, byte[] b) { //for combining two arrays if (a==null) return b; if (b==null) return a; int aLen = a.length; int bLen = b.length; byte[] c= new byte[aLen+bLen]; System.arraycopy(a, 0, c, 0, aLen); System.arraycopy(b, 0, c, aLen, bLen); return c; } /** * 由于realConvert方法使用的CharsetEncoder Engine转换方法比较繁琐,不能直接对byte[]操作, * 要先把byte[]转换为String再转换为char[]再转换为CharBuffer,而且使用CharsetEncoder转UTF-8时 * 还有bug,会导致结果中最后产生大量null字符,所以改用realConvert2()。 * realConvert2直接使用String类的构造方法String(byte[] bytes, String charsetName) * 和getBytes(String charsetName)方法,更加简洁明了。 * @param in 输入字节数组 * @param len 该字节数组的有效长度。用以处理 * java.io.FileInputStream.read(byte[] b)方法产生的byte[]数组中包含部分无效元素的情况。 * 如果in数组中所有元素都有效,该变量可直接填入in.length * @return * @throws UnsupportedEncodingException */ public static byte[] realConvert2 (byte[] in, int len) { byte[] valid = Arrays.copyOf(in, len); try { String step1 = new String(valid,inputCode); byte[] step2 = step1.getBytes(middleCode); String step3 = new String(step2,originCode); byte[] step4 = step3.getBytes(outputCode); return step4; } catch (UnsupportedEncodingException e) { System.err.println("Unsupported Encoding. Please check Java 8 " + "supported encodings at: http://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html"); e.printStackTrace(); } return valid; } }