程式扎記: [ Java常見問題 ] Java讀帶有BOM的UTF-8文件亂碼原因及解決方法

標籤

2010年10月17日 星期日

[ Java常見問題 ] Java讀帶有BOM的UTF-8文件亂碼原因及解決方法

轉載自 這裡 
前言 : 
最近在處理文件時發現了同樣類型的文件使用的編碼可能是不同的。所以想將文件的格式統一一下(因為UTF-8的通用性,決定往UTF-8統一),遇見的第一個問題是:如何查看現有文件的編碼方式。 上網找了一下,找到幾篇比較好文章,這裡就不轉載啦把鏈接搞過來。 
文件編碼問題集錦 
字符串編碼(charset,encoding,decoding)問題原理 
Java編碼淺析 
判定文件編碼或文本流編碼的方法 

問題描述 : 
上面的幾篇文章可以看成認識編碼問題的“從入門到精通” 
如果你看完了上面的文章,一定了解到了,在java中,class文件採用utf8的編碼方式,JVM運行時採用utf16。 Java的字符串是永遠都是unicode的,採用的是UTF-16的編碼方式。 
想測試一下,java對UTF-8文件的讀寫的能力,結果發現了一個很鬱悶的問題,如果通過java寫的UTF-8文件,使用Java可以正確的讀,但是如果用記事本將相同的內容使用UTF-8格式保存,則在使用程序讀取是會從文件中多讀出一個不可見字符。 
測試代碼如下: 
* 類別 UTF8Reader 代碼 : 

  1. package test;  
  2.   
  3. import java.io.*;  
  4.   
  5. public class UTF8Reader {  
  6.       
  7.     public static void main(String argsp[]) {  
  8.         String path = "E:\\EclipseProjects\\FlibTest\\Data\\utf8.txt";  
  9.         File f = new File(path);  
  10.         try{  
  11.             FileInputStream fis = new FileInputStream(f);  
  12.             BufferedReader br = new BufferedReader(new InputStreamReader(fis,"UTF-8"));  
  13.             String line;  
  14.             while((line=br.readLine())!=null) {  
  15.                 byte[] lineBinary = line.getBytes("UTF-8");  
  16.                 System.out.println(line);  
  17.                 for(int i=0;i
  18.                     int tmp = lineBinary[i];    
  19.                     String hexString = Integer.toHexString(tmp);    
  20.                     // 1個byte變成16進制的,只需要2位就可以表示了,取後面兩位,去掉前面的符號填充    
  21.                     hexString = hexString.substring(hexString.length() -2);    
  22.                     System.out.print(hexString.toUpperCase());    
  23.                                     System.out.print(" ");    
  24.                 }  
  25.                 System.out.println();  
  26.             }  
  27.             br.close();  
  28.             fis.close();  
  29.         }catch(IOException e){  
  30.             e.printStackTrace();  
  31.         }catch(Exception e){  
  32.             e.printStackTrace();  
  33.         }  
  34.     }  
  35.   
  36. }  
utf8.txt通過記事本創建,另存時使用指定utf-8編碼,其內容為: 

This is the first line.
This is second line.

正常的測試結果應該是直接輸出utf.txt的文本內容。可是實際上卻輸出了下面的內容: 

?This is the first line.
EF BB BF 54 68 69 73 20 69 73 20 74 68 65 20 66 69 72 73 74 20 6C 69 6E 65 2E 20
This is second line.
54 68 69 73 20 69 73 20 73 65 63 6F 6E 64 20 6C 69 6E 65 2E 20

第一行多出了一個問號。 
通過上面的幾篇文章應該可以想到是Java讀取BOM(Byte Order Mark)的問題,在使用UTF-8時,可以在文件的開始使用3個字節的"EF BB BF"來標識文件使用了UTF-8的編碼,當然也可以不用這個3個字節。 
上面的問題應該就是因為對開頭3個字節的讀取導致的。開始不太相信這個是JDK的Bug,後來在多次試驗後,問題依然存在,就又狗狗了一下,果然找到一個如下的Bug: 
Bug ID:4508058 
不過在我關掉的一些頁面中記得有篇文件說這個bug只在jdk1.5及之前的版本才有,說是1.6已經解決了,從目前來看1.6只是解決了讀取帶有BOM文件失敗的問題,還是不能區別處理有BOM和無BOM的UTF-8編碼的文件,從Bug ID:4508058裡的描述可以看出,這個問題將作為一個不會修改的問題關閉,對於BOM編碼的識別將由應用程序自己來處理,原因可從另處一個bug處查看到,因為Unicode對於BOM的編碼的規定可能發生變化。也就是說對於一個UTF-8的文件,應用程序需要知道這個文件有沒有寫BOM,然後自己決定處理BOM的方式。 
紅色部分的"EF BB BF"剛好是UTF-8文件的BOM編碼,可以看出Java在讀文件時沒能正確處理UTF-8文件的BOM編碼,將前3個字節當作文本內容來處理了。 
使用鏈接中提供的代碼可以解決碰到的亂碼問題: 
http://koti.mbnet.fi/akini/java/unicodereader/ 

解決方案 : 
這裡採用的方法是撰寫一個 UnicodeReader 類別繼承 Reader 來確認前方的 BOM. 首先會讀四個 Bytes, 接著判斷該四個 Bytes 是否為已知的 BOM, 如果是的話則根據不同編碼的 BOM, 退回多的的 Byte, 並以 BOM 後的第一個 Byte 當作一般我們讀取 Stream 的開頭. 否則將之前讀入的四個 Bytes 原封不動的退回, 並從頭開始處理這個 Stream. 該類別實作如下 : 

- UnicodeReader.java : 判斷 BOM 表並作適當處理以避免 BOM 形成不可讀字元的產生
  1. package john.nlp.io;  
  2.   
  3. import java.io.IOException;  
  4. import java.io.InputStream;  
  5. import java.io.InputStreamReader;  
  6. import java.io.PushbackInputStream;  
  7. import java.io.Reader;  
  8.   
  9. public class UnicodeReader extends Reader {  
  10.     private static final int BOM_SIZE = 4;  
  11.     private final InputStreamReader reader;  
  12.   
  13.     /** 
  14.      * Construct UnicodeReader 
  15.      * @param in Input stream. 
  16.      * @param defaultEncoding Default encoding to be used if BOM is not found, 
  17.      * or null to use system default encoding. 
  18.      * @throws IOException If an I/O error occurs. 
  19.      */  
  20.     public UnicodeReader(InputStream in, String defaultEncoding) throws IOException {  
  21.         byte bom[] = new byte[BOM_SIZE];  
  22.         String encoding;  
  23.         int unread;  
  24.         PushbackInputStream pushbackStream = new PushbackInputStream(in, BOM_SIZE);  
  25.         int n = pushbackStream.read(bom, 0, bom.length);  
  26.   
  27.         // Read ahead four bytes and check for BOM marks.  
  28.         if ((bom[0] == (byte0xEF) && (bom[1] == (byte0xBB) && (bom[2] == (byte0xBF)) {  
  29.             encoding = "UTF-8";  
  30.             unread = n - 3;  
  31.         } else if ((bom[0] == (byte0xFE) && (bom[1] == (byte0xFF)) {  
  32.             encoding = "UTF-16BE";  
  33.             unread = n - 2;  
  34.         } else if ((bom[0] == (byte0xFF) && (bom[1] == (byte0xFE)) {  
  35.             encoding = "UTF-16LE";  
  36.             unread = n - 2;  
  37.         } else if ((bom[0] == (byte0x00) && (bom[1] == (byte0x00) && (bom[2] == (byte0xFE) && (bom[3] == (byte0xFF)) {  
  38.             encoding = "UTF-32BE";  
  39.             unread = n - 4;  
  40.         } else if ((bom[0] == (byte0xFF) && (bom[1] == (byte0xFE) && (bom[2] == (byte0x00) && (bom[3] == (byte0x00)) {  
  41.             encoding = "UTF-32LE";  
  42.             unread = n - 4;  
  43.         } else {  
  44.             encoding = defaultEncoding;  
  45.             unread = n;  
  46.         }  
  47.   
  48.         // Unread bytes if necessary and skip BOM marks.  
  49.         if (unread > 0) {  
  50.             pushbackStream.unread(bom, (n - unread), unread);  
  51.         } else if (unread < -1) {  
  52.             pushbackStream.unread(bom, 00);  
  53.         }  
  54.   
  55.         // Use given encoding.  
  56.         if (encoding == null) {  
  57.             reader = new InputStreamReader(pushbackStream);  
  58.         } else {  
  59.             reader = new InputStreamReader(pushbackStream, encoding);  
  60.         }  
  61.     }  
  62.   
  63.     public String getEncoding() {  
  64.         return reader.getEncoding();  
  65.     }  
  66.   
  67.     public int read(char[] cbuf, int off, int len) throws IOException {  
  68.         return reader.read(cbuf, off, len);  
  69.     }  
  70.   
  71.     public void close() throws IOException {  
  72.         reader.close();  
  73.     }  
  74. }  

透過剛剛介紹的 UnicodeReader 與修改測試代碼中的輸入流後 :   

* 類別 UTF8Reader2 代碼 : 

  1. package test;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.File;  
  5. import java.io.FileInputStream;  
  6. import java.io.IOException;  
  7. import java.io.InputStreamReader;  
  8.   
  9. public class UTF8Reader2 {  
  10.       
  11.     public static void main(String args[]) {  
  12.         String path = "E:\\EclipseProjects\\FlibTest\\Data\\utf8.txt";  
  13.         File f = new File(path);  
  14.         try{  
  15.             FileInputStream fis = new FileInputStream(f);  
  16.             BufferedReader br = new BufferedReader(new UnicodeReader(fis,"UTF-8"));  
  17.             String line;  
  18.             while((line=br.readLine())!=null) {  
  19.                 byte[] lineBinary = line.getBytes("UTF-8");  
  20.                 System.out.println(line);  
  21.                 for(int i=0;i
  22.                     int tmp = lineBinary[i];    
  23.                     String hexString = Integer.toHexString(tmp);    
  24.                     // 1個byte變成16進制的,只需要2位就可以表示了,取後面兩位,去掉前面的符號填充    
  25.                     hexString = hexString.substring(hexString.length() -2);    
  26.                     System.out.print(hexString.toUpperCase());    
  27.                                     System.out.print(" ");    
  28.                 }  
  29.                 System.out.println();  
  30.             }  
  31.             br.close();  
  32.             fis.close();  
  33.         }catch(IOException e){  
  34.             e.printStackTrace();  
  35.         }catch(Exception e){  
  36.             e.printStackTrace();  
  37.         }  
  38.     }  
  39.   
  40. }  


輸出結果 :
This is the first line.
54 68 69 73 20 69 73 20 74 68 65 20 66 69 72 73 74 20 6C 69 6E 65 2E 20
This is second line.
54 68 69 73 20 69 73 20 73 65 63 6F 6E 64 20 6C 69 6E 65 2E 20

執行,可以看到正確的結果。

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!