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


140

次のスレッドを参照してください: Javaアプリ:iso-8859-1でエンコードされたファイルを正しく読み取ることができません

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

私は以下を使ってみました:

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

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


11
Eduard氏の言うとおり、「任意のバイトストリームのエンコーディングを特定することはできません」。他のすべての提案は、最良の推測を行う方法(およびライブラリ)を提供します。しかし、結局、彼らはまだ推測です。
Mihai Nita

9
Reader.getEncodingリーダーが使用するように設定されたエンコーディングを返します。この場合はデフォルトのエンコーディングです。
Karol S

回答:


70

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


6
私は、これは、より正確であったことが判明:jchardet.sourceforge.netを(私は、窓-1252、UTF-8 ISO 8859-1でエンコードされた西ヨーロッパ言語文書上でテストしていた)
ジョエル

1
このuniversalchardetは機能しません。ファイルが100%windows-1212でエンコードされている場合でも、ほとんどの場合UTF-8を提供します。
2016

1
juniversalchardetがGitHubに追加されました
デーモン

東ヨーロッパのwindows-1250は検出されません
BernhardDöbler'18年

cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt」からのファイルを検出するために次のコードスニペットを試しましたが、検出された文字セットとしてnullを取得しました。UniversalDetector ud = new UniversalDetector(null); byte [] bytes = FileUtils.readFileToByteArray(new File(file)); ud.handleData(bytes、0、bytes.length); ud.dataEnd(); detectedCharset = ud.getDetectedCharset();
Rohit Verma 2018

105

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

getEncoding()メソッドは、(読み取り設定されたエンコーディングが返されJavadocをストリームするため)。エンコーディングは推測されません。

一部のストリームは、それらを作成するためにどのエンコーディングが使用されたかを示します:XML、HTML。しかし、任意のバイトストリームではありません。

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

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


18
これは実際には質問の答えにはなりません。opはおそらくdocs.codehaus.org/display/GUESSENC/Homeまたはicu-project.org/apiref/icu4j/com/ibm/icu/text/…またはjchardet.sourceforge.net
ChristofferHammarströmDec

23
では、エディタ、notepad ++は、ファイルを開いて正しい文字を表示する方法をどのように知っていますか?
mmm

12
@ハミダムそれはあなたに適切なキャラクターを表示するのは幸運です。誤って推測する場合(そして多くの場合そうです)、エンコーディングを変更できるオプション(メニュー>>エンコーディング)があります。
パセリエ2012年

15
@エドゥアルド:「すべてのエンコーディングは正しいはずです。」不正解です。多くのテキストエンコーディングには無効なパターンがいくつかあります。これは、テキストがおそらくそのエンコーディングではないことを示すフラグです。実際、ファイルの最初の2バイトを考えると、組み合わせの38%だけが有効なUTF8です。最初の5つのコードポイントが偶然に有効なUTF8である確率は、.77%未満です。同様に、UTF16BEとLEは通常、多数のゼロバイトとその場所によって簡単に識別されます。
Mooing Duck

38

これをチェックしてください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()
}

2
試してみましたが、大失敗しました。Eclipseで2つのテキストファイルを作成し、どちらにも「öäüß」が含まれています。1つはisoエンコーディングに設定され、もう1つはutf8に設定されます-両方がutf8として検出されます!だから私は私のhd(windows)のどこかに安全なファイルを試しました-これは正しく検出されました( "windows-1252")。次に、hdで2つの新しいファイルを作成し、1つはエディターで編集し、もう1つはメモ帳++で編集しました。どちらの場合も「Big5」(中国語)が検出されました。
dermoritz、2009

2
編集:わかりましたcm.getConfidence()を確認する必要があります-私の短い "äöüß"では信頼度は10です。したがって、どの程度の信頼度で十分かを判断する必要があります-しかし、これはこの試み(文字セット検出)
dermoritz

1
サンプルコードへの直接リンク: userguide.icu-project.org/conversion/detection
james.garriss

27

これが私のお気に入りです:

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);
  }

2
注意: TikaEncodingDetector 1.1は、実際にはICU4J 3.4 CharsetDectectorクラスの薄いラッパーです。
Stephan

残念ながら、どちらのライブラリも機能しません。1つの例では、ドイツ語のウムラウトを含むUTF-8ファイルをISO-8859-1およびUS-ASCIIとして識別します。
脳は

1
@Brain:テストしたファイルは実際にはUTF-8形式であり、BOM(en.wikipedia.org/wiki/Byte_order_mark)が含まれていますか?
Benny Neugebauer

@BennyNeugebauerファイルはBOMなしのUTF-8です。エンコードを変更し、「ウムラウト」がまだ表示されていることをアサートすることによって、Notepad ++で確認しました。

13

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


12

どのライブラリを使用しますか?

これを書いている時点で、これらは出現する3つのライブラリです。

内部ではICU4j 3.4を使用しているため、Apache Any23は含めません。

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

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

返された応答の採点方法は?

各応答には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メソッドは入力ストリームを完全に読み取ります。大きな入力ストリームの場合、これは問題になる可能性があります。これらのライブラリはすべて、入力ストリーム全体を読み取ります。これは、文字セットを検出するために多くの時間を消費することを意味します。

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


8

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


18
先端にありますが、このサイトには「上」はありません-参照しているライブラリを述べることを検討してください。
McDowell

6

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

IBM ICU4jとMozilla JCharDetを組み込みコンポーネントとして使用して、HTML Webページの文字セットエンコーディングを検出するためのメタJavaツールを作成しました。ここで私のツールを見つけることができます。何よりもまずREADMEセクションを読んでください。また、この問題のいくつかの基本的な概念は、私の論文とその参考文献に記載されています。

怒鳴る私は私の仕事で経験したいくつかの役立つコメントを提供しました:

  • 文字セット検出は、本質的に統計データに基づいており、実際に何が検出されていないのかを推測しているため、間違いのないプロセスではありません
  • icu4jは、IBM、imhoによるこのコンテキストのメインツールです。
  • TikaEncodingDetectorとLucene-ICU4jはどちらもicu4jを使用しており、それらの精度には、テストでのicu4jとの大きな違いはありませんでした(覚えている限り、最大で%1)。
  • icu4jはjchardetよりもはるかに一般的です。icur4jは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セクションの下の最後の段落を参照してください)


5

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が必要とすることを忘れないでください。

これがうまくいくことを願っています。


IMO、この答えは完璧です。ICU4jを使用する場合は、代わりにstackoverflow.com/a/4013565/363573を使用してください。
Stephan


2

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


15
すべてのUTF-8またはUTF-16ファイルにBOMが必要なわけではないため、UTF-8 BOMはお勧めしません。
ChristofferHammarström11年

1

TikaEncodingDetectorの代わりに、Tika AutoDetectReaderを使用することができます。

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

Tike AutoDetectReaderは、ServiceLoaderでロードされたEncodingDetectorを使用します。どのEncodingDetector実装を使用していますか?
Stephan

-1

プレーン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であるため、これらの項目のみが含まれています)


しかし、ISO-8859-1(リストに記載されていない他の多くのものの中でも)は常に成功します。そしてもちろん、これは推測に過ぎず、テキストファイル通信に不可欠な失われたメタデータを回復することはできません。
トムブロジェット

こんにちは@TomBlodget、エンコーディングの順序を変える必要があるとお考えですか?
アンドレス

3
多くの人が「働く」と言いますが、「正しい」のは1人だけです。また、ISO-8859-1は常に「機能する」ため、テストする必要はありません。
トムブロジェット2018

-12

あなたが適切な文字セット選択できるコンストラクタを

new InputStreamReader(new FileInputStream(in), "ISO8859_1");

8
ここでのポイントは、文字セットをプログラムで決定できるかどうかを確認することでした。
ジョエル

1
いいえ、それはあなたのためにそれを推測することはありません。あなたはそれを供給しなければなりません。
ケビン

1
ここでの回答のいくつかで示唆されているように、発見的方法があるかもしれません。stackoverflow.com/ questions / 457655 / java-charset-and-windows /…
Joel
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.