工作中發現從某公司的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: \n" + "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\n"+"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\n"+"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\n"+"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; } }