Java: Cara menentukan pengkodean charset yang benar dari suatu aliran


140

Dengan mengacu pada utas berikut: Aplikasi Java: Tidak dapat membaca file yang disandikan iso-8859-1 dengan benar

Apa cara terbaik untuk menentukan secara sistematis pengkodean charset dari inputstream / file?

Saya sudah mencoba menggunakan yang berikut ini:

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

Tetapi pada file yang saya tahu akan dikodekan dengan ISO8859_1 kode di atas menghasilkan ASCII, yang tidak benar, dan tidak memungkinkan saya untuk dengan benar membuat konten file kembali ke konsol.


11
Eduard benar, "Anda tidak dapat menentukan pengodean byte stream sewenang-wenang". Semua proposal lain memberi Anda cara (dan perpustakaan) untuk melakukan tebakan terbaik. Namun pada akhirnya mereka masih menebak.
Mihai Nita

9
Reader.getEncodingmengembalikan penyandian yang diatur pembaca untuk digunakan, yang dalam kasus Anda adalah penyandian default.
Karol S

Jawaban:


70

Saya telah menggunakan perpustakaan ini, mirip dengan jchardet untuk mendeteksi pengodean di Java: http://code.google.com/p/juniversalchardet/


6
Saya menemukan bahwa ini lebih akurat: jchardet.sourceforge.net (Saya sedang menguji dokumen bahasa Eropa Barat yang disandikan dalam ISO 8859-1, windows-1252, utf-8)
Joel

1
Juniversalchardet ini tidak berfungsi. Ini memberikan UTF-8 sebagian besar waktu, bahkan jika file tersebut 100% windows-1212 dikodekan.
Brain

1
juniversalchardet sekarang ada di GitHub .
deamon

Itu tidak mendeteksi windows Eropa Timur-1250
Bernhard Döbler

Saya mencoba mengikuti cuplikan kode untuk deteksi pada file dari " cl.cam.ac.uk/ ~ mgk25/ucs/examples/UTF-8-test.txt " tetapi mendapat null sebagai set karakter yang terdeteksi. UniversalDetector ud = new UniversalDetector (null); byte [] bytes = FileUtils.readFileToByteArray (File baru (file)); ud.handleData (bytes, 0, bytes.length); ud.dataEnd (); terdeteksiCharset = ud.getDetectedCharset ();
Rohit Verma

105

Anda tidak dapat menentukan pengodean byte stream yang sewenang-wenang. Ini adalah sifat dari pengkodean. Pengkodean berarti pemetaan antara nilai byte dan perwakilannya. Jadi, setiap penyandian "bisa" benar.

Metode getEncoding () akan mengembalikan pengkodean yang telah diatur (baca JavaDoc ) untuk stream. Tidak akan menebak pengodean untuk Anda.

Beberapa aliran memberi tahu Anda penyandian mana yang digunakan untuk membuatnya: XML, HTML. Tapi bukan aliran byte yang sewenang-wenang.

Bagaimanapun, Anda bisa mencoba menebak penyandian sendiri jika perlu. Setiap bahasa memiliki frekuensi yang sama untuk setiap karakter. Dalam bahasa Inggris char e muncul sangat sering tetapi ê akan muncul sangat sangat jarang. Dalam aliran ISO-8859-1 biasanya tidak ada karakter 0x00. Tetapi stream UTF-16 memiliki banyak dari mereka.

Atau: Anda bisa bertanya kepada pengguna. Saya telah melihat aplikasi yang memberikan Anda potongan file dalam penyandian yang berbeda dan meminta Anda untuk memilih yang "benar".



23
Jadi bagaimana editor saya, notepad ++ tahu cara membuka file dan menunjukkan kepada saya karakter yang tepat?
mmm

12
@Hamidam, untungnya itu menunjukkan Anda karakter yang tepat. Ketika tebakan salah (dan memang sering terjadi), ada opsi (Menu >> Pengkodean) yang memungkinkan Anda mengubah pengodean.
Pacerier

15
@Eduard: "Jadi setiap penyandian" bisa "menjadi yang benar." tidak benar. Banyak penyandian teks memiliki beberapa pola yang tidak valid, yang merupakan tanda bahwa teks tersebut mungkin bukan penyandian. Bahkan, mengingat dua byte pertama file, hanya 38% dari kombinasi UTF8 yang valid. Peluang dari 5 codepoint pertama yang valid UTF8 kurang dari 0,77%. Demikian juga, UTF16BE dan LE biasanya mudah diidentifikasi oleh sejumlah besar nol byte dan di mana mereka berada.
Mooing Duck

38

lihat ini: http://site.icu-project.org/ (icu4j) mereka memiliki perpustakaan untuk mendeteksi charset dari IOStream bisa sederhana seperti ini:

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
saya mencoba tetapi sangat gagal: saya membuat 2 file teks dalam gerhana keduanya mengandung "öäüß". Satu set ke iso encoding dan satu ke utf8 - keduanya terdeteksi sebagai utf8! Jadi saya mencoba file yang disimpan di suatu tempat di hd saya (windows) - yang ini terdeteksi dengan benar ("windows-1252"). Kemudian saya membuat dua file baru di hd satu diedit dengan editor yang lain dengan notepad ++. dalam kedua kasus "Big5" (Cina) terdeteksi!
dermoritz

2
EDIT: Ok saya harus memeriksa cm.getConfidence () - dengan "äöüß" singkat saya, keyakinannya adalah 10. Jadi saya harus memutuskan kepercayaan apa yang cukup baik - tetapi thats absolutly ok untuk usaha ini (deteksi charset)
dermoritz


27

Inilah favorit saya:

TikaEncodingDetector

Ketergantungan:

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

Sampel:

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

GuessEncoding

Ketergantungan:

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

Sampel:

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

2
Catatan: TikaEncodingDetector 1.1 sebenarnya adalah pembungkus tipis di sekitar kelas ICU4J 3.4 CharsetDectector .
Stephan

Sayangnya kedua lib tidak bekerja. Dalam satu kasus itu mengidentifikasi file UTF-8 dengan jerman Umlaute sebagai ISO-8859-1 dan US-ASCII.
Brain

1
@ Otak: Apakah file yang Anda uji sebenarnya dalam format UTF-8 dan apakah itu termasuk BOM ( en.wikipedia.org/wiki/Byte_order_mark )?
Benny Neugebauer

@ BennyNeugebauer file tersebut adalah UTF-8 tanpa BOM. Saya memeriksanya dengan Notepad ++, juga dengan mengubah encoding dan menyatakan bahwa "Umlaute" masih terlihat.
Brain

13

Anda tentu dapat memvalidasi file untuk charset tertentu dengan mendekodekannya dengan CharsetDecoderdan mengawasi kesalahan "input salah bentuk" atau "karakter tidak dapat dipetakan". Tentu saja, ini hanya memberi tahu Anda jika charset salah; itu tidak memberi tahu Anda apakah itu benar. Untuk itu, Anda memerlukan dasar perbandingan untuk mengevaluasi hasil yang diterjemahkan, mis. Anda tahu sebelumnya jika karakter dibatasi untuk beberapa bagian, atau apakah teks mematuhi beberapa format yang ketat? Intinya adalah bahwa deteksi charset adalah dugaan tanpa jaminan.


12

Perpustakaan mana yang digunakan?

Pada tulisan ini, mereka adalah tiga perpustakaan yang muncul:

Saya tidak memasukkan Apache Any23 karena menggunakan ICU4j 3.4 di bawah tenda.

Bagaimana cara mengetahui charset yang tepat (atau sedekat mungkin) yang terdeteksi ?

Tidak mungkin mengesahkan charset yang terdeteksi oleh masing-masing pustaka di atas. Namun, dimungkinkan untuk meminta mereka secara bergantian dan menilai respons yang dikembalikan.

Bagaimana cara mencetak respons yang dikembalikan?

Setiap respons dapat diberikan satu poin. Semakin banyak poin yang dimiliki respons, semakin percaya diri charset yang terdeteksi. Ini adalah metode penilaian sederhana. Anda bisa menguraikan orang lain.

Apakah ada kode sampel?

Berikut ini cuplikan lengkap yang mengimplementasikan strategi yang dijelaskan dalam baris sebelumnya.

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() + " }";
}

Perbaikan: TheguessEncoding Metode membaca inputstream sepenuhnya. Untuk aliran input yang besar ini dapat menjadi perhatian. Semua perpustakaan ini akan membaca seluruh inputstream. Ini akan menyiratkan konsumsi waktu yang besar untuk mendeteksi rangkaian karakter.

Dimungkinkan untuk membatasi pemuatan data awal hingga beberapa byte dan melakukan deteksi charset hanya pada beberapa byte tersebut.


8

Lib di atas adalah detektor BOM sederhana yang tentu saja hanya berfungsi jika ada BOM di awal file. Lihatlah http://jchardet.sourceforge.net/ yang memindai teks


18
tepat di ujung, tetapi tidak ada "di atas" di situs ini - pertimbangkan untuk menyatakan perpustakaan yang Anda maksud.
McDowell

6

Sejauh yang saya tahu, tidak ada perpustakaan umum dalam konteks ini yang cocok untuk semua jenis masalah. Jadi, untuk setiap masalah Anda harus menguji perpustakaan yang ada dan memilih yang terbaik yang memenuhi kendala masalah Anda, tetapi seringkali tidak ada satupun yang sesuai. Dalam kasus ini Anda dapat menulis Encoding Detector Anda sendiri! Seperti yang saya tulis ...

Saya telah menulis alat java meta untuk mendeteksi pengkodean charset dari halaman Web HTML, menggunakan IBM ICU4j dan Mozilla JCharDet sebagai komponen bawaan. Di sini Anda dapat menemukan alat saya, silakan baca bagian README sebelum yang lain. Selain itu, Anda dapat menemukan beberapa konsep dasar masalah ini di makalah saya dan dalam rujukannya.

Di bawah ini saya memberikan beberapa komentar bermanfaat yang saya alami dalam pekerjaan saya:

  • Deteksi charset bukanlah proses yang mudah, karena pada dasarnya didasarkan pada data statistik dan apa yang sebenarnya terjadi adalah menebak tidak mendeteksi
  • icu4j adalah alat utama dalam konteks ini oleh IBM, imho
  • Baik TikaEncodingDetector dan Lucene-ICU4j menggunakan icu4j dan akurasinya tidak memiliki perbedaan yang berarti dari yang icu4j dalam tes saya (paling banyak% 1, seingat saya)
  • icu4j jauh lebih umum daripada jchardet, icu4j hanya sedikit bias terhadap pengkodean keluarga IBM sementara jchardet sangat bias untuk utf-8
  • Karena meluasnya penggunaan UTF-8 di dunia HTML; jchardet adalah pilihan yang lebih baik daripada icu4j secara keseluruhan, tetapi bukan pilihan terbaik!
  • icu4j sangat bagus untuk penyandian khusus Asia Timur seperti EUC-KR, EUC-JP, SHIFT_JIS, BIG5 dan penyandian keluarga GB
  • Kedua icu4j dan jchardet adalah bencana dalam berurusan dengan halaman HTML dengan Windows-1251 dan Windows-1256 encodings. Windows-1251 alias cp1251 banyak digunakan untuk bahasa berbasis Cyrillic seperti Rusia dan Windows-1256 alias cp1256 banyak digunakan untuk bahasa Arab
  • Hampir semua alat deteksi enkode menggunakan metode statistik, sehingga akurasi output sangat tergantung pada ukuran dan isi input
  • Beberapa pengkodean pada dasarnya sama hanya dengan perbedaan parsial, sehingga dalam beberapa kasus pengkodean yang diduga atau terdeteksi mungkin salah tetapi pada saat yang sama benar! Seperti tentang Windows-1252 dan ISO-8859-1. (lihat paragraf terakhir di bawah bagian 5.2 makalah saya)


5

Jika Anda menggunakan ICU4J ( http://icu-project.org/apiref/icu4j/ )

Ini kode saya:

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

Ingatlah untuk meletakkan semua try-catch membutuhkannya.

Saya harap ini berhasil untuk Anda.


IMO, jawaban ini sempurna. Jika Anda ingin menggunakan ICU4j, coba yang ini sebagai gantinya: stackoverflow.com/a/4013565/363573 .
Stephan


2

Untuk file ISO8859_1, tidak ada cara mudah untuk membedakannya dari ASCII. Namun demikian, untuk file Unicode orang dapat mendeteksi ini berdasarkan beberapa byte pertama dari file tersebut.

File UTF-8 dan UTF-16 termasuk Byte Order Mark (BOM) di bagian paling awal file. BOM adalah ruang tanpa putus lebar nol.

Sayangnya, karena alasan historis, Java tidak mendeteksi ini secara otomatis. Program seperti Notepad akan memeriksa BOM dan menggunakan penyandian yang sesuai. Menggunakan unix atau Cygwin, Anda dapat memeriksa BOM dengan perintah file. Sebagai contoh:

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

Untuk Java, saya sarankan Anda memeriksa kode ini, yang akan mendeteksi format file umum dan memilih pengkodean yang benar: Cara membaca file dan secara otomatis menentukan pengkodean yang benar


15
Tidak semua file UTF-8 atau UTF-16 memiliki BOM, karena tidak diperlukan, dan UTF-8 BOM tidak disarankan.
Christoffer Hammarström

1

Alternatif untuk TikaEncodingDetector adalah dengan menggunakan Tika AutoDetectReader .

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

Tike AutoDetectReader menggunakan EncodingDetector yang dimuat dengan ServiceLoader. Implementasi EncodingDetector apa yang Anda gunakan?
Stephan

-1

Di Jawa polos:

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

Pendekatan ini akan mencoba pengkodean satu per satu sampai satu berhasil atau kita kehabisan. (BTW daftar penyandian saya hanya memiliki item-item itu karena itu adalah implementasi rangkaian karakter yang diperlukan pada setiap platform Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )


Tetapi ISO-8859-1 (di antara banyak lainnya yang belum Anda daftarkan) akan selalu berhasil. Dan, tentu saja, ini hanya menebak, yang tidak dapat memulihkan metadata yang hilang yang penting untuk komunikasi file teks.
Tom Blodget

Hai @ TomBlodget, apakah Anda menyarankan agar urutan penyandian harus berbeda?
Andres

3
Saya mengatakan bahwa banyak yang akan "bekerja" tetapi hanya satu yang "benar". Dan Anda tidak perlu menguji untuk ISO-8859-1 karena akan selalu "berfungsi".
Tom Blodget

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.