2010年10月15日 星期五

[ Java常見問題 ] 判斷文件編碼是否為UTF-8

轉載自 這裡 
前言 : 
這裡研究一下如何來判斷文件的編碼是否是UTF-8,關於這個問題網絡上一般採用的是判斷文件的BOM頭,但是這種方法有個缺點,就是有一些工具,比如EditPlus,比如Java程序,做出來的UTF-8編碼的文件是不會在文件內容的前面加上BOM頭的,對於這種情況,網絡上的這個辦法就會檢測失敗。 

使用文件的BOM 頭判斷 : 
但這裡還是列出如果是採用BOM 頭的解法可以參考入下代碼 : (參考這裡) 
* 類別 FileEncodeReferee 代碼 : 

  1. package test;  
  2.   
  3. import java.io.*;  
  4.   
  5. public class FileEncodeReferee {  
  6.     private File file;  
  7.       
  8.     public FileEncodeReferee(File f){  
  9.         file = f;  
  10.     }  
  11.       
  12.     public FileEncodeReferee(String path) {  
  13.         file = new File(path);  
  14.     }  
  15.       
  16.     public static String getCharset(File f){  
  17.         String charset = "Big5";  
  18.         byte[] first3Bytes = new byte[3];  
  19.         BufferedInputStream bis = null;  
  20.         try {  
  21.             // boolean checked = false;  
  22.             bis = new BufferedInputStream(new FileInputStream(f));  
  23.             bis.mark(0);  
  24.             int read = bis.read(first3Bytes, 03);  
  25.             if (read == -1) {  
  26.                 return charset;  
  27.             }  
  28.             if (first3Bytes[0] == (byte0xFF && first3Bytes[1] == (byte0xFE) {  
  29.                 charset = "UTF-16LE";  
  30.                 // checked = true;  
  31.             } else if (first3Bytes[0] == (byte0xFE  
  32.                     && first3Bytes[1] == (byte0xFF) {  
  33.                 charset = "UTF-16BE";  
  34.                 // checked = true;  
  35.             } else if (first3Bytes[0] == (byte0xEF  
  36.                     && first3Bytes[1] == (byte0xBB  
  37.                     && first3Bytes[2] == (byte0xBF) {  
  38.                 charset = "UTF-8";  
  39.                 // checked = true;  
  40.             }  
  41.         } catch (Exception e) {  
  42.             e.printStackTrace();  
  43.         } finally {  
  44.             if (bis != null) {  
  45.                 try {  
  46.                     bis.close();  
  47.                 } catch (Exception ex) {  
  48.                     ex.printStackTrace();  
  49.                 }  
  50.             }  
  51.         }  
  52.         return charset;  
  53.           
  54.     }  
  55.       
  56.     public String getCharset() {  
  57.         String charset = "GBK";  
  58.         byte[] first3Bytes = new byte[3];  
  59.         BufferedInputStream bis = null;  
  60.         try {  
  61.             // boolean checked = false;  
  62.             bis = new BufferedInputStream(new FileInputStream(file));  
  63.             bis.mark(0);  
  64.             int read = bis.read(first3Bytes, 03);  
  65.             if (read == -1) {  
  66.                 return charset;  
  67.             }  
  68.             if (first3Bytes[0] == (byte0xFF && first3Bytes[1] == (byte0xFE) {  
  69.                 charset = "UTF-16LE";  
  70.                 // checked = true;  
  71.             } else if (first3Bytes[0] == (byte0xFE  
  72.                     && first3Bytes[1] == (byte0xFF) {  
  73.                 charset = "UTF-16BE";  
  74.                 // checked = true;  
  75.             } else if (first3Bytes[0] == (byte0xEF  
  76.                     && first3Bytes[1] == (byte0xBB  
  77.                     && first3Bytes[2] == (byte0xBF) {  
  78.                 charset = "UTF-8";  
  79.                 // checked = true;  
  80.             }  
  81.         } catch (Exception e) {  
  82.             e.printStackTrace();  
  83.         } finally {  
  84.             if (bis != null) {  
  85.                 try {  
  86.                     bis.close();  
  87.                 } catch (Exception ex) {  
  88.                     ex.printStackTrace();  
  89.                 }  
  90.             }  
  91.         }  
  92.         return charset;  
  93.     }  
  94.       
  95.     public static void main(String args[]) {  
  96.         File utf8f = new File("E:/Temp/TestData/utf8.txt");  
  97.         File big5f = new File("E:/Temp/TestData/big5.txt");       
  98.         System.out.println(utf8f.getAbsolutePath()+" charset="+FileEncodeReferee.getCharset(utf8f));  
  99.         System.out.println(big5f.getAbsolutePath()+" charset="+FileEncodeReferee.getCharset(big5f));  
  100.     }  
  101. }  


文件內容判斷編碼 : 
在經過一些測試之後,研究了一個解決方案。 
考慮如下文件輸入流的代碼, 
  1. FileInputStream fis = null;  
  2. InputStreamReader isr = null;  
  3. BufferedReader br = null;                         
  4. File f = new File(fn);  
  5. fis = new FileInputStream(f);  
  6. isr = new InputStreamReader(fis, "UTF-8");  
  7. br = new BufferedReader(isr);  
推測執行原理如下,(都是根據測試結果的猜測) 
1. fis 根據文件的保存編碼來採用不同的編碼讀取文件。讀取結果為byte[]
2. isr設定的話,那麼根據isr設定的編碼來讀取文件,如果不設定,那麼編碼採用系統默認編碼ansi(window-31j,shift_jis)
3. br.readline,將isr的結果組合為String,如果isr沒有設定編碼,那麼組合String時採用的編碼為系統默認編碼ansi(window-31j,shift_jis),如果isr設定了編碼,那麼採用isr設定好的編碼。
4. new string(byte[],"encode") 根據指定編碼生成string,如果不指定,採用系統默認編碼。系統默認編碼 ansi
5. string.getbyte("encode") 從String根據特定的編碼取得byte[]


問題出在第1步,第一步fis因為讀取文件的時候,調用的是native,也就是系統(windows系統)的東西,他用了系統的東西,系統的這個東西作了編碼判斷,但是因為他調用的是native的東西,這個判定結果沒有返回給java,導致java裡面isr,br沒有辦法跟fis協調一致,isr,br只能採用系統默認編碼 ansi(window-31j,shift_jis),而不是採用fis的判定結果來讀取文件。 
這導致了,當文件使用ansi編碼保存的時候,默認編碼跟fis判定結果一致,不會出任何問題。 
當文件使用了utf-8編碼的時候,默認編碼ansi,跟fis判定結果utf-8不一致,fis採用uft-8編碼讀取出文件內容,而後,br.readline採用系統默認編碼把UTF-8編碼對應的byte[]組合成了ansi編碼對應的字符串,就產生了亂碼。 
我在網絡以及java api裡面查找了一下,沒有找到判定文件保存編碼的方法。推論:因為java是調用了native的方法來實際讀取文件的,判定在native裡面完成,但是沒有把判定結果公開給我們程序員。 
另有一個測試結果的推論,英文字符在任何編碼下面讀取出來的byte[]都是一樣的。因為我們才用任何編碼都不會出現英文字符亂碼的問題,所以大多數時候這個判定對我們沒有影響,這裡不討論特殊情況下因為這個原因造成的影響。 
根據以上推論,考慮如下解決問題的思路, 
1. 通過fis來讀取文件,這個時候讀取來的byte[]根據文件的保存格式是不同的。fis會自動判斷處理。
2. 通過br來讀取文件。
3. 1,2的讀取結果byte[]進行比較,如果相同,那麼可以認為文件的保存格式為UTF-8(雖然存在全英文ansi保存的可能,但是這種狀況認為是utf-8保存不會有影響),如果不同則不是UTF-8,考慮我們目前狀況,那麼不是UTF-8可以認為文件保存編碼就是ANSI,如果不可以這麼認為,其他編碼類型也要做這個判斷。因為英文字符在任何編碼下面讀取出來的byte[]都是一樣的。所以這裡有一個效率問題,必須文件內容全部比較,不能只比較一部分.

如果使用第三方開源代碼common-io.jar的話,可以將以上思路簡化為如下代碼。 
* 類別UTF8EncodingTest 代碼 : 
  1. package test;  
  2.   
  3. import java.io.*;  
  4.   
  5. import org.apache.commons.io.FileUtils;  
  6.   
  7. public class UTF8EncodingTest {  
  8.       
  9.     public static void showBinary(String s){  
  10.         showBinary(s.getBytes());         
  11.     }  
  12.     public static void showBinary(byte[] buf){  
  13.         for(byte b:buf) {  
  14.             System.out.printf("%x ", b);  
  15.         }  
  16.         System.out.println();  
  17.     }  
  18.       
  19.     public static boolean isUTF8(File file) {  
  20.         try {  
  21.             byte[] buf = FileUtils.readFileToByteArray(file);  
  22.             System.out.println("\t<<>>");  
  23.             showBinary(buf);  
  24.             String UTF8Cntent = FileUtils.readFileToString(file, "UTF-8");  
  25.             String big5Cntent = new String(buf, "Big5");  
  26.             String defCntent = new String(buf); //Default is UTF8  
  27.             System.out.println("\t<<>>\n"+UTF8Cntent);  
  28.             showBinary(UTF8Cntent);  
  29.             System.out.println("\t<<>>\n"+big5Cntent);  
  30.             showBinary(big5Cntent);  
  31.             System.out.println("\t<<>>\n"+defCntent);  
  32.             showBinary(defCntent);  
  33.               
  34.             if(buf.length == UTF8Cntent.getBytes().length) {  
  35.                 byte[] buf_utf8 = UTF8Cntent.getBytes();  
  36.                 for(int i=0;i
  37.                     if(buf_utf8[i]!=buf[i]){  
  38.                         return false;  
  39.                     }  
  40.                 }  
  41.                 return true;  
  42.             }  
  43.         } catch (IOException e) {  
  44.             e.printStackTrace();  
  45.         }  
  46.         return false;  
  47.     }  
  48.           
  49.       
  50.     public static void main(String args[]){  
  51.         File utf8f = new File("E:/Temp/TestData/utf8.txt");  
  52.         File big5f = new File("E:/Temp/TestData/big5.txt");  
  53.         if(isUTF8(utf8f)){  
  54.             System.out.println(utf8f.getAbsolutePath()+" is utf8 encoding!\n");  
  55.         } else {  
  56.             System.out.println(utf8f.getAbsolutePath()+" isn't utf8 encoding!\n");  
  57.         }  
  58.         if(isUTF8(big5f)){  
  59.             System.out.println(big5f.getAbsolutePath()+" is utf8 encoding!\n");  
  60.         } else {  
  61.             System.out.println(big5f.getAbsolutePath()+" isn't utf8 encoding!\n");  
  62.         }         
  63.     }  
  64. }  
執行結果 : 
<<>>
ef bb bf e9 80 99 e6 98 af 55 54 46 38 e7 b7 a8 e7 a2 bc d a 54 68 69 73 20 69 73 20 55 54 46 38 20 65 6e 63 6f 64 69 6e 67 <原始Binary>
<<>>
這是UTF8編碼
This is UTF8 encoding
ef bb bf e9 80 99 e6 98 af 55 54 46 38 e7 b7 a8 e7 a2 bc d a 54 68 69 73 20 69 73 20 55 54 46 38 20 65 6e 63 6f 64 69 6e 67 <使用UTF8 Encoding的 Binary, 前三個byte 是BOM>
<<>>
嚜輸��狹TF8蝺函Ⅳ
This is UTF8 encoding
e5 9a 9c e8 bc b8 ef bf bd ef bf bd e7 8b b9 54 46 38 e8 9d ba e5 87 bd e2 85 a3 d a 54 68 69 73 20 69 73 20 55 54 46 38 20 65 6e 63 6f 64 69 6e 67
<<>>
這是UTF8編碼 <預設是utf8 encoding, 所以中文可以正常顯示>
This is UTF8 encoding
ef bb bf e9 80 99 e6 98 af 55 54 46 38 e7 b7 a8 e7 a2 bc d a 54 68 69 73 20 69 73 20 55 54 46 38 20 65 6e 63 6f 64 69 6e 67
E:\Temp\TestData\utf8.txt is utf8 encoding!

<<>>
b3 6f ac 4f 42 69 67 35 bd 73 bd 58 d a 54 68 69 73 20 69 73 20 42 69 67 35 20 65 6e 63 6f 64 69 6e 67 
<<>>
�o�OBig5�s�X <使用utf8 encoding 取出會加入BOM, 並且中文無法正常顯示>
This is Big5 encoding
ef bf bd 6f ef bf bd 4f 42 69 67 35 ef bf bd 73 ef bf bd 58 d a 54 68 69 73 20 69 73 20 42 69 67 35 20 65 6e 63 6f 64 69 6e 67
<<>>
這是Big5編碼 <使用ANSI encoding, 中文可以正常顯示>
This is Big5 encoding
e9 80 99 e6 98 af 42 69 67 35 e7 b7 a8 e7 a2 bc d a 54 68 69 73 20 69 73 20 42 69 67 35 20 65 6e 63 6f 64 69 6e 67
<<>>
�o�OBig5�s�X <預設編碼是utf8, 所以內容為ANSI encoding 的中文無法正常顯示>
This is Big5 encoding
ef bf bd 6f ef bf bd 4f 42 69 67 35 ef bf bd 73 ef bf bd 58 d a 54 68 69 73 20 69 73 20 42 69 67 35 20 65 6e 63 6f 64 69 6e 67
E:\Temp\TestData\big5.txt isn't utf8 encoding!

這個判定有一個效率問題,在這個文章中採用的是讀取整個文件,如果我們文件太大,會比較花時間。這種情況可以考慮按行來讀取判定,某一行不一致就可以退出了。這樣可以提高一些效率。 

補充說明 : 
* Wiki UTF8 介紹 
UTF-8(8 位元Universal Character Set/Unicode Transformation Format)是一種針對Unicode的可變長度字元編碼(定長碼),也是一種前綴碼。它可以用來表示Unicode標準中的任何字元,且其編碼中的第一個位元組仍與ASCII相容,這使得原來處理ASCII字元的軟體無須或只須做少部份修改,即可繼續使用。因此,它逐漸成為電子郵件、網頁及其他儲存或傳送文字的應用中,優先採用的編碼。

* Java JSP 讀取UTF-8檔案 (網路文章) 
* [ Java常見問題 ] Java讀帶有BOM的UTF-8文件亂碼原因及解決方法

沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...