引子
我們知道從一個文件流中讀取內容時是要指定具體的編碼格式的,否則讀出來的內容會是亂碼。比如我們的代碼寫成下面這個樣子:
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過去幾十年收集的字符集數據,理論上也是基於統計學的。這種方式統計的結果准確性也較高推薦使用。
下面就來具體介紹下怎么使用cpdector
和ICU4J
推測文件編碼。
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的。所以能看出,這兩個工具都是以能正確解碼文件為原則來推測編碼的,不一定要推測出原始編碼。
參考
公眾號推薦
歡迎大家關注我的微信公眾號「程序員自由之路」