沒提供編碼格式,讀文件時要怎么推測文件具體的編碼


引子

我們知道從一個文件流中讀取內容時是要指定具體的編碼格式的,否則讀出來的內容會是亂碼。比如我們的代碼寫成下面這個樣子:

private static void m1(){
    try(FileInputStream fileInputStream = new FileInputStream("D:\\每日摘錄.txt")) {
        byte[] bytes = FileCopyUtils.copyToByteArray(fileInputStream);
        System.out.println(new String(bytes));
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

執行上面的代碼,有時我們能“僥幸”得到正確的執行結果。因為new String(byte[])這個方法會指定默認的編碼格式,所以如果我們讀取的文件的編碼格式正好是UTF8的話,那上面的代碼就一點問題沒有。但是如果我們讀取的是一個編碼格式是GBK的文件,那么得到的內容將是一坨亂碼。

上面的問題解決起來很簡單,只要指定下字符編碼就可以了。

new String(bytes,"GBK");

在告知文件編碼格式的條件下,解決上面的問題是很簡單。假如現在沒告知文件具體的編碼格式,我們需要怎么正確的讀取文件呢?一個可行的辦法是推測文件編碼方式。

推測文件編碼的方式

網上有多種方式可以“推測”出一個文件的可用編碼,但是需要注意的是:所有的方法都不能保證推測出來的結果是絕對准確的,有的方法推測的准確率較高,而有的方法推測出來的准確率較低。主要的推測方法有以下幾種:

  • 通過文件的前三個字節來判斷:因為有些編碼格式會存在文件的前面3個字節中,比如UTF-8編碼格式的文本文件,其前3個字節的值就是-17、-69、-65。但是很明顯,這種方式的局限性比較大,推測出來的准確率也比較低,因此不推薦這種方式。
  • 通過特殊字符來判斷:通過某些編碼格式編碼的文件中會出現一些特殊的字節值,因此可以通過判斷文件中是否有這些特殊值來推測文件編碼格式。此方准確率也不高,不推薦使用。
  • 通過工具庫cpdetector來判斷:cpdector 是一款開源的文檔編碼檢測工具,可以檢測 xml,html文檔編碼類型。是基於統計學原理來推測文件編碼的,但是也不保證推測結果的准確性。
  • 通過ICU4J庫來判斷:ICU的推測邏輯基於IBM過去幾十年收集的字符集數據,理論上也是基於統計學的。這種方式統計的結果准確性也較高推薦使用。

下面就來具體介紹下怎么使用cpdectorICU4J推測文件編碼。

cpdector

使用Cpdetector jar包,提供兩種方式檢測文件編碼,至於選擇哪種 需要根據個人需求,文檔有注釋。依賴antlr-2.7.4.jar,chardet-1.0.jar,jargs-1.0.jar三個jar包。 可以再官網下載 http://cpdetector.sourceforge.net/。

import info.monitorenter.cpdetector.io.ASCIIDetector;
import info.monitorenter.cpdetector.io.ByteOrderMarkDetector;
import info.monitorenter.cpdetector.io.CodepageDetectorProxy;
import info.monitorenter.cpdetector.io.JChardetFacade;
import info.monitorenter.cpdetector.io.ParsingDetector;
import info.monitorenter.cpdetector.io.UnicodeDetector;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

import org.apache.log4j.Logger;

/**
 * <p>
 * 	獲取流編碼,不保證完全正確,設置檢測策略 isFast為true為快速檢測策略,false為正常檢測
 * 	InputStream 支持mark,則會在檢測后調用reset,外部可重新使用。
 * 	InputStream 流沒有關閉。
 * </p>
 * 
 * <p>
 * 	如果采用快速檢測編碼方式,最多會掃描8個字節,依次采用的{@link UnicodeDetector},{@link byteOrderMarkDetector},
 * 	{@link JChardetFacade}, {@link ASCIIDetector}檢測。對於一些標准的unicode編碼,適合這個方式或者對耗時敏感的。
 * </p>
 * 
 * <p>
 * 	采用正常檢測,讀取指定字節數,如果沒有指定,默認讀取全部字節檢測,依次采用的{@link byteOrderMarkDetector},{@link parsingDetector},{@link JChardetFacade}, {@link ASCIIDetector}檢測。
 * 	字節越多檢測時間越長,正確率較高。
 * </p>
 * @author WuKong
 *
 */
public class CpdetectorEncoding {
	
	private static final Logger logger = Logger.getLogger(CpdetectorEncoding.class);
	
	/**
	 * <p>
	 * 獲取流編碼,不保證完全正確,設置檢測策略 isFast為true為快速檢測策略,false為正常檢測
	 * InputStream 支持mark,則會在檢測后調用reset,外部可重新使用。
	 * InputStream 流沒有關閉。
	 * </p>
	 * 
	 * <p>
	 * 如果采用快速檢測編碼方式,最多會掃描8個字節,依次采用的{@link UnicodeDetector},{@link byteOrderMarkDetector},
	 * {@link JChardetFacade}, {@link ASCIIDetector}檢測。對於一些標准的unicode編碼,適合這個方式或者對耗時敏感的。
	 * </p>
	 * 
	 * <p>
	 *  采用正常檢測,讀取指定字節數,如果沒有指定,默認讀取全部字節檢測,依次采用的{@link byteOrderMarkDetector},{@link parsingDetector},{@link JChardetFacade}, {@link ASCIIDetector}檢測。
	 *  字節越多檢測時間越長,正確率較高。
	 * </p>
	 *
	 * @param in 輸入流  isFast 是否采用快速檢測編碼方式
	 * @return Charset The character are now - hopefully - correct。如果為null,沒有檢測出來。
	 * @throws IOException 
	 */
	public Charset getEncoding(InputStream buffIn,boolean isFast) throws IOException{
		
		return getEncoding(buffIn,buffIn.available(),isFast);
	}
	
	public Charset getFastEncoding(InputStream buffIn) throws IOException{
		return getEncoding(buffIn,MAX_READBYTE_FAST,DEFALUT_DETECT_STRATEGY);
	}
	
	
	
	public Charset getEncoding(InputStream in, int size, boolean isFast) throws IOException {
		
		try {
			
			java.nio.charset.Charset charset = null;
			
			int tmpSize = in.available();
			size = size >tmpSize?tmpSize:size;
			//if in support mark method, 
			if(in.markSupported()){
				
				if(isFast){
					
					size = size>MAX_READBYTE_FAST?MAX_READBYTE_FAST:size;
					in.mark(size++);
					charset = getFastDetector().detectCodepage(in, size);
				}else{
					
					in.mark(size++);
					charset = getDetector().detectCodepage(in, size);
				}
				in.reset();
				
			}else{
				
				if(isFast){
					
					size = size>MAX_READBYTE_FAST?MAX_READBYTE_FAST:size;
					charset = getFastDetector().detectCodepage(in, size);
				}else{
					charset = getDetector().detectCodepage(in, size);
				}
			}
			
			
		    return charset;
		}catch(IllegalArgumentException e){
			
			logger.error(e.getMessage(),e);
			throw e;
		} catch (IOException e) {
			
			logger.error(e.getMessage(),e);
			throw e;
		}
		
	}
	
	
	public Charset getEncoding(byte[] byteArr,boolean isFast) throws IOException{
		
		return getEncoding(byteArr, byteArr.length, isFast);
	}
	
	
	public Charset getFastEncoding(byte[] byteArr) throws IOException{
		
		return getEncoding(byteArr, MAX_READBYTE_FAST, DEFALUT_DETECT_STRATEGY);
	}
	
	
	public Charset getEncoding(byte[] byteArr, int size,boolean isFast) throws IOException {
		
		size = byteArr.length>size?size:byteArr.length;
		if(isFast){
			size = size>MAX_READBYTE_FAST?MAX_READBYTE_FAST:size;
		}
		
		ByteArrayInputStream byteArrIn = new ByteArrayInputStream(byteArr,0,size);
		BufferedInputStream in = new BufferedInputStream(byteArrIn);
		
		try {
			
			Charset charset = null;
			if(isFast){
				
				charset = getFastDetector().detectCodepage(in, size);
			}else{
				
				charset = getDetector().detectCodepage(in, size);
			}
			
			return charset;
		} catch (IllegalArgumentException e) {
			
			logger.error(e.getMessage(),e);
			throw e;
		} catch (IOException e) {
			
			logger.error(e.getMessage(),e);
			throw e;
		}
	   
	}
	
	private static CodepageDetectorProxy detector =null;
	private static CodepageDetectorProxy fastDtector =null;
	private static ParsingDetector parsingDetector =  new ParsingDetector(false);
	private static ByteOrderMarkDetector byteOrderMarkDetector = new ByteOrderMarkDetector();
	
	//default strategy use fastDtector
	private static final boolean DEFALUT_DETECT_STRATEGY = true;
	
	private static final int MAX_READBYTE_FAST = 8; 
	
	private static CodepageDetectorProxy getDetector(){
		
		if(detector==null){
			
			detector = CodepageDetectorProxy.getInstance();
			 // Add the implementations of info.monitorenter.cpdetector.io.ICodepageDetector: 
		    // This one is quick if we deal with unicode codepages:
			detector.add(byteOrderMarkDetector);
			// The first instance delegated to tries to detect the meta charset attribut in html pages.
		    detector.add(parsingDetector);
		    // This one does the tricks of exclusion and frequency detection, if first implementation is 
		    // unsuccessful:
			detector.add(JChardetFacade.getInstance());
		    detector.add(ASCIIDetector.getInstance());
		}
		
		return detector;
	}
	
	
	private static CodepageDetectorProxy getFastDetector(){
		
		if(fastDtector==null){
			
			fastDtector = CodepageDetectorProxy.getInstance();
			fastDtector.add(UnicodeDetector.getInstance());
			fastDtector.add(byteOrderMarkDetector); 
			fastDtector.add(JChardetFacade.getInstance());
			fastDtector.add(ASCIIDetector.getInstance());
		}
		
		return fastDtector;
	}
	
}

ICU4J

ICU (International Components for Unicode)是為軟件應用提供Unicode和全球化支持的一套成熟、廣泛使用的C/C++和Java類庫集,可在所有平台的C/C++和Java軟件上獲得一致的結果。

ICU首先是由Taligent公司開發的,Taligent公司被合並為IBM公司全球化認證中心的Unicode研究組后,ICU由IBM和開源組織合作繼續開發。開始ICU只有Java平台的版本,后來這個平台下的ICU類被吸納入SUN公司開發的JDK1.1,並在JDK以后的版本中不斷改進。C++和C平台下的ICU是由JAVA平台下的ICU移植過來的,移植過的版本被稱為ICU4C,來支持這C/C++兩個平台下的國際化應用。ICU4J和ICU4C區別不大,但由於ICU4C是開源的,並且緊密跟進Unicode標准,ICU4C支持的Unicode標准總是最新的;同時,因為JAVA平台的ICU4J的發布需要和JDK綁定,ICU4C支持Unicode標准改變的速度要比ICU4J快的多。

ICU的功能主要有:

  • 代碼頁轉換: 對文本數據進行Unicode、幾乎任何其他字符集或編碼的相互轉換。ICU的轉化表基於IBM過去幾十年收集的字符集數據,在世界各地都是最完整的。
  • 排序規則(Collation): 根據特定語言、區域或國家的管理和標准比較字數串。ICU的排序規則基於Unicode排序規則算法加上來自公共區域性數據倉庫(Common locale data repository)的區域特定比較規則。
  • 格式化: 根據所選區域設置的慣例,實現對數字、貨幣、時間、日期、和利率的格式化。包括將月和日名稱轉換成所選語言、選擇適當縮寫、正確對字段進行排序等。這些數據也取自公共區域性數據倉庫。
  • 時間計算: 在傳統格里歷基礎上提供多種歷法。提供一整套時區計算API。
  • Unicode支持: ICU緊密跟進Unicode標准,通過它可以很容易地訪問Unicode標准制定的很多Unicode字符屬性、Unicode規范化、大小寫轉換和其他基礎操作。
  • 正則表達式: ICU的正則表達式全面支持Unicode並且性能極具競爭力。
  • Bidi: 支持不同文字書寫順序混合文字(例如從左到右書寫的英語,或者從右到左書寫的阿拉伯文和希伯來文)的處理。
  • 文本邊界: 在一段文本內定位詞、句或段落位置、或標識最適合顯示文本的自動換行位置。

代碼示例:

public class FileEncodingDetector {

    public static void main(String[] args) {
        File file = new File("D:\\xx1.log");
        System.out.println(getFileCharsetByICU4J(file));
    }

    public static String getFileCharsetByICU4J(File file) {
        String encoding = null;

        try {
            Path path = Paths.get(file.getPath());
            byte[] data = Files.readAllBytes(path);
            CharsetDetector detector = new CharsetDetector();
            detector.setText(data);
            //這個方法推測首選的文件編碼格式
            CharsetMatch match = detector.detect();
            //這個方法可以推測出所有可能的編碼方式
            CharsetMatch[] charsetMatches = detector.detectAll();
            if (match == null) {
                return encoding;
            }
            encoding = match.getName();
        } catch (IOException var6) {
            System.out.println(var6.getStackTrace());
        }
        return encoding;
    }
}

注意點

  • ICU4J和cpdector推測出來的文件編碼都不能保證百分百准確,只能保證大概率准確;
  • ICU4J和cpdector推測出來的編碼不一定是文件原始的編碼。比如我的一個文本文件中只有簡單的英文字符,然后我將這個文件存為GBK編碼格式。這時你使用這兩個工具推測出來的文件編碼可能是ASCII編碼。但是使用ASCII編碼也能正確打開這個文件,因為GBK是兼容ASCII的。所以能看出,這兩個工具都是以能正確解碼文件為原則來推測編碼的,不一定要推測出原始編碼。

參考

公眾號推薦

歡迎大家關注我的微信公眾號「程序員自由之路」


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM