Java: Bir akışın doğru karakter kümesi kodlamasını belirleme


140

Aşağıdaki iş parçacığına referansla: Java Uygulaması: iso-8859-1 kodlanmış dosya doğru okunamıyor

Bir girdi / dosyanın doğru karakter kümesi kodlamasını programlı olarak belirlemenin en iyi yolu nedir?

Aşağıdaki kullanarak denedim:

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

Ancak ISO8859_1 ile kodlandığını bildiğim bir dosyada yukarıdaki kod, doğru olmayan ASCII'yi verir ve dosyanın içeriğini konsola doğru şekilde işlememe izin vermez.


11
Eduard haklı, "Rasgele bir bayt akışının kodlamasını belirleyemezsiniz". Diğer tüm öneriler size en iyi tahminde bulunmanız için yollar (ve kütüphaneler) sunar. Ama sonunda hala tahmin ediliyorlar.
Mihai Nita

9
Reader.getEncodingokuyucunun kullanmak üzere ayarlanmış olduğu kodlamayı döndürür. Bu, sizin durumunuzda varsayılan kodlamadır.
Karol S

Yanıtlar:


70

Java'da kodlamayı tespit etmek için jchardet'e benzer bu kütüphaneyi kullandım: http://code.google.com/p/juniversalchardet/


6
Bunun daha doğru olduğunu gördüm : jchardet.sourceforge.net (ISO 8859-1, windows-1252, utf-8'de kodlanmış Batı Avrupa dil belgelerini test ediyordum )
Joel

1
Bu juniversalchardet çalışmıyor. Dosya% 100 windows-1212 kodlanmış olsa bile çoğu zaman UTF-8 sunar.
Beyin

1
juniversalchardet artık GitHub'da .
deamon

Doğu Avrupa pencerelerini tespit etmiyor-1250
Bernhard Döbler

Kod parçacığını " cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt " dosyasından algılama için aşağıdaki denedim ama karakter kümesi tespit olarak null var. UniversalDetector ud = yeni UniversalDetector (null); byte [] bytes = FileUtils.readFileToByteArray (yeni Dosya (dosya)); ud.handleData (bayt, 0, bayt.length); ud.dataEnd (); tespitCharset = ud.getDetectedCharset ();
Rohit Verma

105

Rasgele bir bayt akışının kodlamasını belirleyemezsiniz. Bu kodlamaların doğasıdır. Kodlama, bir bayt değeri ve temsili arasında bir eşleme anlamına gelir. Yani her kodlama "olabilir" doğru olabilir.

GetEncoding () metodu (okuma kurulmuş kodlama döner JavaDoc'u akışı). Kodlamayı sizin için tahmin etmeyecektir.

Bazı akışlar bunları oluşturmak için hangi kodlamanın kullanıldığını söyler: XML, HTML. Ancak keyfi bir bayt akışı değil.

Her neyse, gerekirse bir kodlamayı kendi başınıza tahmin etmeye çalışabilirsiniz. Her dilin her karakter için ortak bir frekansı vardır. İngilizcede char e çok sık görülür, ancak ê çok nadiren görünecektir. Bir ISO-8859-1 akışında genellikle 0x00 karakter yoktur. Ancak UTF-16 akışında çok fazla var.

Veya: kullanıcıya sorabilirsiniz. Zaten farklı kodlamalarda dosyanın bir parçacığını sunan ve "doğru" olanı seçmenizi isteyen uygulamaları gördüm.



23
Peki editörüm, notepad ++ dosyayı nasıl açacağımı ve bana doğru karakterleri nasıl göstereceğini biliyor?
mmm

12
@Hamidam size doğru karakterleri göstermesi şans eseri. Yanlış tahmin ederse (ve sıklıkla yapar), kodlamayı değiştirmenizi sağlayan bir seçenek (Menü >> Kodlama) vardır.
Pacerier

15
@Eduard: "Yani her kodlama" "doğru olabilir." tam olarak doğru değil. Birçok metin kodlamasında, metnin büyük olasılıkla kodlama olmadığını belirten bir bayrak olan geçersiz kalıplar bulunur . Aslında, bir dosyanın ilk iki baytı göz önüne alındığında, kombinasyonların yalnızca% 38'i geçerli UTF8'dir. İlk 5 kod noktasının tesadüfen geçerli UTF8 olma olasılığı% 0,77'den düşüktür. Benzer şekilde, UTF16BE ve LE genellikle çok sayıda sıfır bayt ve nerede oldukları ile kolayca tanımlanır.
Mooing Duck

38

şuna bir bak : http://site.icu-project.org/ (icu4j) IOStream'den karakter setini tespit etmek için kütüphaneleri var, bu kadar basit olabilir:

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
denedim ama büyük başarısız: i hem "öäüß" içeren tutulması 2 metin dosyaları yaptı. Biri iso kodlamaya ve diğeri utf8'e ayarlandı - her ikisi de utf8 olarak algılanıyor! Bu yüzden bir yerde benim hd (windows) güvenli bir dosya denedim - bu doğru tespit edildi ("windows-1252"). Sonra bir tane editör ile düzenlenen diğeri notepad ++ ile düzenlenmiş iki yeni dosya oluşturdum. her iki durumda da "Big5" (Çince) tespit edildi!
dermoritz

2
DÜZENLEME: Tamam cm.getConfidence () - benim kısa "äöüß" güven 10 kontrol etmeliyiz. Bu yüzden güven ne kadar iyi olduğuna karar vermeliyim - ama bu çaba için
kesin olarak

1
Örnek koda doğrudan bağlantı: userguide.icu-project.org/conversion/detection
james.garriss

27

İşte benim favorilerim:

TikaEncodingDetector

Bağımlılık:

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

Örneklem:

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

GuessEncoding

Bağımlılık:

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

Örneklem:

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

2
Not: TikaEncodingDetector 1.1 aslında ICU4J 3.4 CharsetDectector sınıfı etrafında ince bir pakettir .
Stephan

Ne yazık ki her iki lib de çalışmıyor. Bir durumda Alman Umlaute'lu bir UTF-8 dosyasını ISO-8859-1 ve US-ASCII olarak tanımlar.
Beyin

1
@Brain: Test edilen dosyanız gerçekten UTF-8 biçiminde mi ve bir Malzeme Listesi içeriyor mu ( en.wikipedia.org/wiki/Byte_order_mark )?
Benny Neugebauer

@BennyNeugebauer dosya BOM'siz bir UTF-8'dir. Ben de kodlama değiştirerek ve "Umlaute" hala görünür olduğunu iddia ederek, Notepad ++ ile kontrol ettim.
Beyin

13

Belirli bir karakter kümesi için dosyayı a ile çözerek ve "hatalı biçimlendirilmiş giriş" veya "eşlenemez karakter" hatalarını izleyerek kesinlikle doğrulayabilirsiniz . Tabii ki, bu sadece bir karakter kümesinin yanlış olup olmadığını gösterir; doğru olup olmadığını söylemez. Bunun için, kodu çözülmüş sonuçları değerlendirmek için bir karşılaştırma tabanına ihtiyacınız vardır, örneğin, karakterlerin bazı alt kümelerle sınırlı olup olmadığını veya metnin bazı katı biçimlere uyup uymadığını önceden biliyor musunuz? Sonuç olarak, karakter seti tespiti herhangi bir garanti olmaksızın tahmin çalışmasıdır.CharsetDecoder


12

Hangi kütüphane kullanılacak?

Bu yazı itibariyle, ortaya çıkan üç kütüphane:

Apache Any23'ü dahil etmiyorum çünkü kaputun altında ICU4j 3.4 kullanıyor.

Hangisinin doğru karakter kümesini algıladığını (veya mümkün olduğunca yakın) nasıl söyleyebilirim ?

Yukarıdaki kütüphanelerin tespit ettiği karakter kümesini onaylamak imkansızdır. Bununla birlikte, onlara sırayla sormak ve iade edilen cevabı puanlamak mümkündür.

Geri verilen cevap nasıl puanlanır?

Her yanıta bir nokta atanabilir. Bir yanıt ne kadar fazla puana sahip olursa, algılanan karakter kümesine o kadar güven duyulur. Bu basit bir puanlama yöntemidir. Diğerlerini detaylandırabilirsiniz.

Örnek kod var mı?

Önceki satırlarda açıklanan stratejiyi uygulayan tam bir pasaj.

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

Gelişmeler:guessEncoding yöntem InputStream tamamen okur. Büyük girdi akışları için bu bir endişe kaynağı olabilir. Bütün bu kütüphaneler girdi akışının tamamını okuyacaktır. Bu, karakter setini tespit etmek için büyük bir zaman tüketimi anlamına gelir.

İlk veri yüklemesini birkaç bayt ile sınırlamak ve karakter kümesi algılamasını yalnızca bu bayt üzerinde yapmak mümkündür.


8

Yukarıdaki kütüphaneler basit BOM dedektörleridir, ki bu sadece dosyanın başında bir BOM varsa çalışır. Metni tarayan http://jchardet.sourceforge.net/ adresine bir göz atın


18
sadece bahşişte, ama bu sitede "yukarıda" yok - bahsettiğiniz kütüphaneleri belirtmeyi düşünün.
McDowell

6

Bildiğim kadarıyla, bu bağlamda her türlü soruna uygun genel bir kütüphane yoktur. Bu nedenle, her sorun için mevcut kütüphaneleri test etmeli ve probleminizin kısıtlamalarını karşılayan en iyisini seçmelisiniz, ancak çoğu zaman uygun değildir. Bu durumlarda kendi Kodlama Dedektörünüzü yazabilirsiniz! Yazdığım gibi ...

Yerleşik bileşenler olarak IBM ICU4j ve Mozilla JCharDet kullanarak HTML Web sayfalarının karakter kümesi kodlamasını tespit etmek için bir meta java aracı yazdım. Aracımı burada bulabilirsiniz, lütfen her şeyden önce README bölümünü okuyun. Ayrıca, bu sorunun bazı temel kavramlarını makalemde ve referanslarında bulabilirsiniz.

Körük Çalışmamda yaşadığım bazı yararlı yorumlar sağladım:

  • Aslında istatistiki verilere dayanmaktadır ve aslında ne olur çünkü charset algılama, kusursuz bir süreç değildir tahmin değil tespit
  • icu4j IBM, imho tarafından bu bağlamda ana araçtır
  • Hem TikaEncodingDetector hem de Lucene-ICU4j, icu4j kullanıyor ve doğrulukları, testlerimde icu4j'nin (en çok% 1, hatırladığım kadarıyla) anlamlı bir farkı yoktu.
  • icu4j, jchardet'ten çok daha geneldir, icu4j IBM ailesi kodlamalarına biraz eğilimliyken, jchardet utf-8'e şiddetle önyargılıdır
  • UTF-8'in HTML dünyasında yaygın kullanımı nedeniyle; jchardet genel olarak icu4j daha iyi bir seçimdir, ama en iyi seçim değil!
  • icu4j, EUC-KR, EUC-JP, SHIFT_JIS, BIG5 ve GB ailesi kodlamaları gibi Doğu Asya'ya özgü kodlamalar için mükemmeldir
  • Hem icu4j hem de jchardet, Windows-1251 ve Windows-1256 kodlamalı HTML sayfaları ile uğraşırken çöküyor. Windows-1251 aka cp1251 Rusça gibi Kiril tabanlı diller için yaygın olarak kullanılır ve Windows-1256 aka cp1256 Arapça için yaygın olarak kullanılır
  • Hemen hemen tüm kodlama algılama araçları istatistiksel yöntemler kullanır, bu nedenle çıktının doğruluğu büyük ölçüde girdinin boyutuna ve içeriğine bağlıdır
  • Bazı kodlamalar temelde sadece kısmi farklılıklarla aynıdır, bu nedenle bazı durumlarda tahmin edilen veya algılanan kodlama yanlış olabilir, ancak aynı zamanda doğru olabilir! Windows-1252 ve ISO-8859-1 hakkında olduğu gibi. (makalemin 5.2 bölümü altındaki son paragrafa bakınız)


5

ICU4J kullanıyorsanız ( http://icu-project.org/apiref/icu4j/ )

İşte benim kod:

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

Tüm try-catch ihtiyacını koymayı unutmayın.

Umarım bu senin için işe yarar.


IMO, bu cevap mükemmel. ICU4j kullanmak istiyorsanız, bunu deneyin: stackoverflow.com/a/4013565/363573 .
Stephan


2

ISO8859_1 dosyaları için, dosyaları ASCII'den ayırmanın kolay bir yolu yoktur. Bununla birlikte, Unicode dosyaları için genellikle dosyanın ilk birkaç baytına göre algılanabilir.

UTF-8 ve UTF-16 dosyaları , dosyanın en başında bir Bayt Sırası İşareti (BOM) içerir. Malzeme Listesi sıfır genişlikli, kırılmaz bir alandır.

Ne yazık ki, tarihsel nedenlerden dolayı Java bunu otomatik olarak algılamaz. Not Defteri gibi programlar Malzeme Listesini kontrol eder ve uygun kodlamayı kullanır. Unix veya Cygwin kullanarak, Malzeme Listesini dosya komutuyla kontrol edebilirsiniz. Örneğin:

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

Java için, ortak dosya biçimlerini algılayacak ve doğru kodlamayı seçecek olan bu kodu kontrol etmenizi öneririm: Dosya nasıl okunur ve doğru kodlamayı otomatik olarak belirtir


15
UTF-8 veya UTF-16 dosyalarının tümü gerekli olmadığından BOM'ye sahip değildir ve UTF-8 BOM önerilmez.
Christoffer Hammarström

1

TikaEncodingDetector'a bir alternatif de Tika AutoDetectReader kullanmaktır .

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

Tike AutoDetectReader, ServiceLoader yüklü EncodingDetector öğesini kullanır. Hangi EncodingDetector uygulamalarını kullanıyorsunuz?
Stephan

-1

Düz Java'da:

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

Bu yaklaşım, kodlamalar tek tek çalışana kadar veya biz bitene kadar tek tek deneyecek. (BTW kodlama listemde yalnızca şu öğeler var, çünkü bunlar her Java platformunda gereken karakter kümesi uygulamaları, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )


Ancak ISO-8859-1 (listelemediğiniz diğerlerinin yanı sıra) her zaman başarılı olacaktır. Ve tabii ki, bu sadece tahmin, metin dosyası iletişimi için gerekli olan kayıp meta verileri kurtaramaz.
Tom Blodget

Merhaba @ TomBlodget, kodlama sırasının farklı olması gerektiğini mi öneriyorsunuz?
Andres

3
Birçoğunun "çalışacağını" söylüyorum ama sadece bir tanesi "doğru". Ve ISO-8859-1 için test yapmanız gerekmez, çünkü her zaman "çalışır".
Tom Blodget

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.