web-dev-qa-db-ja.com

Java:ストリームの正しい文字セットエンコーディングを決定する方法

次のスレッドを参照: Java App:iso-8859-1エンコードファイルを正しく読み込めません

入力ストリーム/ファイルの正しい文字セットエンコーディングをプログラムで決定する最良の方法は何ですか?

私は次を使用してみました:

File in =  new File(args[0]);
InputStreamReader r = new InputStreamReader(new FileInputStream(in));
System.out.println(r.getEncoding());

しかし、ISO8859_1でエンコードされていることがわかっているファイルでは、上記のコードはASCIIを生成しますが、これは正しくなく、ファイルの内容をコンソールに正しくレンダリングできません。

125
Joel

Javaでのエンコードを検出するためにjchardetに似たこのライブラリを使用しました: http://code.google.com/p/juniversalchardet/

65

任意のバイトストリームのエンコーディングを決定することはできません。これがエンコーディングの性質です。エンコードとは、バイト値とその表現の間のマッピングを意味します。そのため、すべてのエンコーディングが正しい可能性があります。

getEncoding() メソッドは、ストリームに設定された( JavaDoc を読んで)エンコーディングを返します。エンコーディングは推測されません。

一部のストリームでは、作成に使用されたエンコードがXML、HTMLで示されます。しかし、任意のバイトストリームではありません。

とにかく、必要に応じて、自分でエンコーディングを推測してみることができます。すべての言語には、すべての文字に共通の頻度があります。英語ではchar eは非常に頻繁に表示されますが、êはほとんど表示されません。 ISO-8859-1ストリームでは、通常0x00文字はありません。しかし、UTF-16ストリームには多くのものがあります。

または:ユーザーに尋ねることができます。さまざまなエンコーディングでファイルのスニペットを表示し、「正しい」ものを選択するように要求するアプリケーションを見てきました。

98
Eduard Wirch

これを確認してください: http://site.icu-project.org/ (icu4j)彼らはIOStreamから文字セットを検出するためのライブラリを持っています。

BufferedInputStream bis = new BufferedInputStream(input);
CharsetDetector cd = new CharsetDetector();
cd.setText(bis);
CharsetMatch cm = cd.detect();

if (cm != null) {
   reader = cm.getReader();
   charset = cm.getName();
}else {
   throw new UnsupportedCharsetException()
}
33
user345883

私のお気に入りは次のとおりです。

TikaEncodingDetector

依存:

<dependency>
  <groupId>org.Apache.any23</groupId>
  <artifactId>Apache-any23-encoding</artifactId>
  <version>1.1</version>
</dependency>

サンプル:

public static Charset guessCharset(InputStream is) throws IOException {
  return Charset.forName(new TikaEncodingDetector().guessEncoding(is));    
}

GuessEncoding

依存:

<dependency>
  <groupId>org.codehaus.guessencoding</groupId>
  <artifactId>guessencoding</artifactId>
  <version>1.4</version>
  <type>jar</type>
</dependency>

サンプル:

  public static Charset guessCharset2(File file) throws IOException {
    return CharsetToolkit.guessEncoding(file, 4096, StandardCharsets.UTF_8);
  }
24

確かにvalidate特定の文字セットのファイルを decodingCharsetDecoder で監視することができます「不正な入力」または「マップ不可の文字」エラーの場合は出力されます。もちろん、これは文字セットが間違っている場合にのみ通知します。正しいかどうかはわかりません。そのためには、デコード結果を評価するための比較の基礎が必要です。文字が一部のサブセットに制限されているかどうか、またはテキストが何らかの厳密な形式に準拠しているかどうかを事前に知っていますか?一番下の行は、文字セットの検出は、保証なしの当て推量です。

13
Zach Scrivena

使用するライブラリ

この記事を書いている時点で、3つのライブラリが出現しています。

Apache Any2 は含めません。これは、内部でICU4j 3.4を使用するためです。

どれがright文字セット(または可能な限り近い)を検出したかを知る方法は?

上記の各ライブラリによって検出された文字セットを認証することは不可能です。ただし、順番に質問して、返された応答をスコアリングすることは可能です。

返された応答をスコアリングする方法は?

各応答には1ポイントを割り当てることができます。応答のポイントが多いほど、検出された文字セットの信頼度が高くなります。これは単純なスコアリング方法です。他の人を詳しく説明できます。

サンプルコードはありますか?

以下は、前の行で説明した戦略を実装した完全なスニペットです。

public static String guessEncoding(InputStream input) throws IOException {
    // Load input data
    long count = 0;
    int n = 0, EOF = -1;
    byte[] buffer = new byte[4096];
    ByteArrayOutputStream output = new ByteArrayOutputStream();

    while ((EOF != (n = input.read(buffer))) && (count <= Integer.MAX_VALUE)) {
        output.write(buffer, 0, n);
        count += n;
    }

    if (count > Integer.MAX_VALUE) {
        throw new RuntimeException("Inputstream too large.");
    }

    byte[] data = output.toByteArray();

    // Detect encoding
    Map<String, int[]> encodingsScores = new HashMap<>();

    // * GuessEncoding
    updateEncodingsScores(encodingsScores, new CharsetToolkit(data).guessEncoding().displayName());

    // * ICU4j
    CharsetDetector charsetDetector = new CharsetDetector();
    charsetDetector.setText(data);
    charsetDetector.enableInputFilter(true);
    CharsetMatch cm = charsetDetector.detect();
    if (cm != null) {
        updateEncodingsScores(encodingsScores, cm.getName());
    }

    // * juniversalchardset
    UniversalDetector universalDetector = new UniversalDetector(null);
    universalDetector.handleData(data, 0, data.length);
    universalDetector.dataEnd();
    String encodingName = universalDetector.getDetectedCharset();
    if (encodingName != null) {
        updateEncodingsScores(encodingsScores, encodingName);
    }

    // Find winning encoding
    Map.Entry<String, int[]> maxEntry = null;
    for (Map.Entry<String, int[]> e : encodingsScores.entrySet()) {
        if (maxEntry == null || (e.getValue()[0] > maxEntry.getValue()[0])) {
            maxEntry = e;
        }
    }

    String winningEncoding = maxEntry.getKey();
    //dumpEncodingsScores(encodingsScores);
    return winningEncoding;
}

private static void updateEncodingsScores(Map<String, int[]> encodingsScores, String encoding) {
    String encodingName = encoding.toLowerCase();
    int[] encodingScore = encodingsScores.get(encodingName);

    if (encodingScore == null) {
        encodingsScores.put(encodingName, new int[] { 1 });
    } else {
        encodingScore[0]++;
    }
}    

private static void dumpEncodingsScores(Map<String, int[]> encodingsScores) {
    System.out.println(toString(encodingsScores));
}

private static String toString(Map<String, int[]> encodingsScores) {
    String GLUE = ", ";
    StringBuilder sb = new StringBuilder();

    for (Map.Entry<String, int[]> e : encodingsScores.entrySet()) {
        sb.append(e.getKey() + ":" + e.getValue()[0] + GLUE);
    }
    int len = sb.length();
    sb.delete(len - GLUE.length(), len);

    return "{ " + sb.toString() + " }";
}

改善:guessEncodingメソッドは、入力ストリームを完全に読み取ります。大きな入力ストリームの場合、これは懸念事項です。これらのライブラリはすべて、入力ストリーム全体を読み取ります。これは、文字セットの検出に多大な時間を消費することを意味します。

初期データのロードを数バイトに制限し、それらの数バイトのみで文字セット検出を実行することができます。

10
Stephan

上記のライブラリは単純なBOMディテクタであり、ファイルの先頭にBOMがある場合にのみ機能します。テキストをスキャンする http://jchardet.sourceforge.net/ を見てください

7
Lorrat

ICU4Jを使用する場合( http://icu-project.org/apiref/icu4j/

ここに私のコードがあります:

            String charset = "ISO-8859-1"; //Default chartset, put whatever you want

            byte[] fileContent = null;
            FileInputStream fin = null;

            //create FileInputStream object
            fin = new FileInputStream(file.getPath());

            /*
             * Create byte array large enough to hold the content of the file.
             * Use File.length to determine size of the file in bytes.
             */
            fileContent = new byte[(int) file.length()];

            /*
             * To read content of the file in byte array, use
             * int read(byte[] byteArray) method of Java FileInputStream class.
             *
             */
            fin.read(fileContent);

            byte[] data =  fileContent;

            CharsetDetector detector = new CharsetDetector();
            detector.setText(data);

            CharsetMatch cm = detector.detect();

            if (cm != null) {
                int confidence = cm.getConfidence();
                System.out.println("Encoding: " + cm.getName() + " - Confidence: " + confidence + "%");
                //Here you have the encode name and the confidence
                //In my case if the confidence is > 50 I return the encode, else I return the default value
                if (confidence > 50) {
                    charset = cm.getName();
                }
            }

すべてのtry catchがそれを必要とすることを忘れないでください。

これがお役に立てば幸いです。

5
ssamuel68

実際のエンコードを検出できるニースのサードパーティライブラリを見つけました: http://glaforge.free.fr/wiki/index.php?wiki=GuessEncoding

私はそれを広範囲にテストしませんでしたが、うまくいくようです。

5
falcon

私の知る限り、このコンテキストではすべてのタイプの問題に適した一般的なライブラリはありません。そのため、問題ごとに既存のライブラリをテストし、問題の制約を満たす最適なライブラリを選択する必要がありますが、多くの場合、適切ではありません。これらの場合、独自のエンコーディング検出器を作成できます!私が書いたように...

IBM ICU4jとMozilla JCharDetを組み込みコンポーネントとして使用して、HTML Webページの文字セットエンコーディングを検出するメタJavaツールを作成しました。 ここ あなたは私のツールを見つけることができます。他の何かの前にREADMEセクションを読んでください。また、この問題のいくつかの基本的な概念は、私の paper およびそのリファレンスで見つけることができます。

私は私の仕事で経験したいくつかの有用なコメントを提供しました:

  • 文字セットの検出は、本質的に統計データに基づいており、実際に起こるのはguessingnotであるため、確実なプロセスではありません検出中
  • icu4jは、IBMによるこのコンテキストのメインツールです。
  • TikaEncodingDetectorとLucene-ICU4jは両方ともicu4jを使用しており、それらの精度は私のテストのicu4jとは有意な差はありませんでした(覚えているように、多くても%1)
  • icu4jはjchardetよりもはるかに一般的です。icu4jはIBMファミリーのエンコーディングに少し偏っていますが、jchardetはutf-8に強く偏っています。
  • HTMLの世界ではUTF-8が広く使用されているため、 jchardetは全体としてicu4jよりも良い選択ですが、最良の選択ではありません!
  • icu4jは、EUC-KR、EUC-JP、SHIFT_JIS、BIG5、GBファミリーエンコーディングなどの東アジア固有のエンコーディングに最適です。
  • Icu4jとjchardetはどちらも、Windows-1251およびWindows-1256エンコーディングのHTMLページの処理に問題があります。 Windows-1251別名cp1251はロシア語などのキリル文字ベースの言語に広く使用され、Windows-1256別名cp1256はアラビア語に広く使用されています
  • ほとんどすべてのエンコーディング検出ツールは統計的手法を使用しているため、出力の精度は入力のサイズと内容に大きく依存します
  • 一部のエンコーディングは、本質的には部分的に異なるだけで同じであるため、場合によっては、推測または検出されたエンコーディングは偽であるかもしれませんが、同時に真になります! Windows-1252およびISO-8859-1について。 (私の論文の5.2セクションの下の最後の段落を参照)
4
faghani

データのエンコードがわからない場合、それを決定するのはそれほど簡単ではありませんが、 推測するライブラリ を使用することができます。また、 同様の質問 があります。

4
Fabian Steeg

ISO8859_1ファイルの場合、ASCIIと区別する簡単な方法はありません。ただし、Unicodeファイルの場合、通常はファイルの最初の数バイトに基づいてこれを検出できます。

UTF-8およびUTF-16ファイルには、ファイルの先頭に バイトオーダーマーク (BOM)が含まれています。 BOMは、幅がゼロの改行なしスペースです。

残念ながら、歴史的な理由により、Javaはこれを自動的に検出しません。メモ帳などのプログラムは、BOMをチェックし、適切なエンコードを使用します。 UNIXまたはCygwinを使用すると、fileコマンドでBOMを確認できます。例えば:

$ file sample2.sql 
sample2.sql: Unicode text, UTF-16, big-endian

Javaの場合、一般的なファイル形式を検出し、正しいエンコーディングを選択するこのコードをチェックアウトすることをお勧めします。 ファイルを読み取り、正しいエンコーディングを自動的に指定する方法

2
brianegge

TikaEncodingDetectorの代替手段は、 Tika AutoDetectReader を使用することです。

Charset charset = new AutoDetectReader(new FileInputStream(file)).getCharset();
1
Nolf

プレーンJavaの場合:

final String[] encodings = { "US-ASCII", "ISO-8859-1", "UTF-8", "UTF-16BE", "UTF-16LE", "UTF-16" };

List<String> lines;

for (String encoding : encodings) {
    try {
        lines = Files.readAllLines(path, Charset.forName(encoding));
        for (String line : lines) {
            // do something...
        }
        break;
    } catch (IOException ioe) {
        System.out.println(encoding + " failed, trying next.");
    }
}

このアプローチでは、エンコーディングが1つ機能するか、使い果たされるまで1つずつ試行します。 (ところで、私のエンコーディングリストには、すべてのJavaプラットフォームで必要な文字セット実装であるため、これらの項目のみがあります。 https://docs.Oracle.com/javase/9​​/docs/api/Java /nio/charset/Charset.html

0
Andres