Java: come determinare la codifica del set di caratteri corretta di uno stream


140

Con riferimento al seguente thread: App Java: Impossibile leggere correttamente il file codificato iso-8859-1

Qual è il modo migliore per determinare programmaticamente la corretta codifica del set di caratteri di un inputstream / file?

Ho provato a utilizzare quanto segue:

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

Ma su un file che so essere codificato con ISO8859_1 il codice sopra riportato produce ASCII, che non è corretto, e non mi consente di restituire correttamente il contenuto del file alla console.


11
Eduard ha ragione, "Non è possibile determinare la codifica di un flusso di byte arbitrario". Tutte le altre proposte ti offrono modi (e librerie) per fare le migliori ipotesi. Ma alla fine sono ancora ipotesi.
Mihai Nita,

9
Reader.getEncodingrestituisce la codifica utilizzata dal lettore, che nel tuo caso è la codifica predefinita.
Karol S,

Risposte:


70

Ho usato questa libreria, simile a jchardet per rilevare la codifica in Java: http://code.google.com/p/juniversalchardet/


6
Ho scoperto che questo era più preciso: jchardet.sourceforge.net (stavo testando documenti in lingua dell'Europa occidentale codificati in ISO 8859-1, windows-1252, utf-8)
Joel

1
Questo juniversalchardet non funziona. Fornisce UTF-8 la maggior parte delle volte, anche se il file è codificato al 100% da windows-1212.
Cervello,

1
juniversalchardet è ora su GitHub .
Deamon

Non rileva windows-1250 dell'Europa orientale
Bernhard Döbler il

Ho provato a seguire lo snippet di codice per il rilevamento su file da " cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt " ma ho ottenuto null come set di caratteri rilevato. UniversalDetector ud = new UniversalDetector (null); byte [] bytes = FileUtils.readFileToByteArray (nuovo file (file)); ud.handleData (byte, 0, bytes.length); ud.dataEnd (); detectCharset = ud.getDetectedCharset ();
Rohit Verma,

105

Non è possibile determinare la codifica di un flusso di byte arbitrario. Questa è la natura delle codifiche. Una codifica indica una mappatura tra un valore di byte e la sua rappresentazione. Quindi ogni codifica "potrebbe" essere la giusta.

Il metodo getEncoding () restituirà la codifica che è stata impostata (leggi JavaDoc ) per lo stream. Non indovinerà la codifica per te.

Alcuni flussi indicano quale codifica è stata utilizzata per crearli: XML, HTML. Ma non un flusso di byte arbitrario.

Ad ogni modo, potresti provare a indovinare una codifica da solo, se necessario. Ogni lingua ha una frequenza comune per ogni carattere. In inglese il carattere appare molto spesso ma ê apparirà molto raramente. In uno stream ISO-8859-1 di solito non ci sono caratteri 0x00. Ma un flusso UTF-16 ne ha molti.

Oppure: potresti chiedere all'utente. Ho già visto applicazioni che presentano uno snippet del file in diverse codifiche e ti chiedono di selezionare quella "corretta".


18
Questo non risponde davvero alla domanda. L'operazione dovrebbe probabilmente usare docs.codehaus.org/display/GUESSENC/Home o icu-project.org/apiref/icu4j/com/ibm/icu/text/… o jchardet.sourceforge.net
Christoffer Hammarström

23
Quindi come fa il mio editor, notepad ++, a sapere come aprire il file e mostrarmi i caratteri giusti?
mmm

12
@Hamidam è per fortuna che ti mostra i personaggi giusti. Quando indovina in modo errato (e spesso lo fa), c'è un'opzione (Menu >> Codifica) che ti consente di cambiare la codifica.
Pacerier,

15
@Eduard: "Quindi ogni codifica" potrebbe "essere la giusta". non del tutto giusto. Molte codifiche di testo hanno diversi modelli non validi, che indicano che il testo probabilmente non è tale codifica. Infatti, dati i primi due byte di un file, solo il 38% delle combinazioni sono UTF8 valide. Le probabilità che i primi 5 punti di codice siano UTF8 validi per caso sono inferiori a .77%. Allo stesso modo, UTF16BE e LE sono di solito facilmente identificati dal gran numero di zero byte e dove si trovano.
Mooing Duck il

38

Dai un'occhiata: http://site.icu-project.org/ (icu4j) hanno librerie per rilevare charset da IOStream potrebbero essere semplici come questo:

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
ho provato ma fallisce moltissimo: ho creato 2 file di testo in eclipse entrambi contenenti "öäüß". Uno impostato su iso encoding e uno su utf8 - entrambi vengono rilevati come utf8! Quindi ho provato un file protetto da qualche parte sul mio hd (Windows) - questo è stato rilevato correttamente ("windows-1252"). Poi ho creato due nuovi file su hd uno modificato con l'editor l'altro con notepad ++. in entrambi i casi è stato rilevato "Big5" (cinese)!
Dermoritz,

2
EDIT: Ok dovrei controllare cm.getConfidence () - con il mio breve "äöüß" la fiducia è 10. Quindi devo decidere quale confidenza è abbastanza buona - ma questo è assolutamente ok per questo sforzo (rilevazione charset)
dermoritz

1
Link diretto al codice di esempio: userguide.icu-project.org/conversion/detection
james.garriss

27

Ecco i miei preferiti:

TikaEncodingDetector

Dipendenza:

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

Campione:

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

GuessEncoding

Dipendenza:

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

Campione:

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

2
Nota: TikaEncodingDetector 1.1 è in realtà un involucro sottile in giro alla CharsetDectector classe ICU4J 3.4 .
Stephan,

Sfortunatamente entrambe le librerie non funzionano. In un caso identifica un file UTF-8 con Umlaute tedesca come ISO-8859-1 e US-ASCII.
Cervello,

1
@Brain: il tuo file testato è effettivamente in un formato UTF-8 e include una DBA ( en.wikipedia.org/wiki/Byte_order_mark )?
Benny Neugebauer,

@BennyNeugebauer il file è un UTF-8 senza BOM. L'ho controllato con Notepad ++, anche modificando la codifica e affermando che "Umlaute" è ancora visibile.
Brain,

13

Puoi certamente convalidare il file per un determinato set di caratteri decodificandolo con un CharsetDecodere facendo attenzione agli errori "input non valido" o "carattere non modificabile". Naturalmente, questo ti dice solo se un set di caratteri è sbagliato; non ti dice se è corretto. Per questo, hai bisogno di una base di confronto per valutare i risultati decodificati, ad esempio sai in anticipo se i caratteri sono limitati a un sottoinsieme o se il testo aderisce a un formato rigoroso? La linea di fondo è che il rilevamento dei set di caratteri è una congettura senza garanzie.


12

Quale libreria usare?

Al momento della stesura di questo documento, sono tre le librerie che emergono:

Non includo Apache Any23 perché utilizza ICU4j 3.4 sotto il cofano.

Come dire quale ha rilevato il set di caratteri giusto (o il più vicino possibile)?

È impossibile certificare il set di caratteri rilevato da ciascuna delle librerie precedenti. Tuttavia, è possibile chiedere loro a turno e segnare la risposta restituita.

Come segnare la risposta restituita?

A ogni risposta può essere assegnato un punto. Più punti ha una risposta, maggiore è la sicurezza del set di caratteri rilevato. Questo è un metodo di punteggio semplice. Puoi elaborare altri.

C'è qualche codice di esempio?

Ecco uno snippet completo che implementa la strategia descritta nelle righe precedenti.

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

Miglioramenti: ilguessEncoding metodo legge interamente l'inputstream. Per flussi di input di grandi dimensioni questo può essere un problema. Tutte queste librerie leggerebbero l'intero inputstream. Ciò implicherebbe un grande dispendio di tempo per il rilevamento del set di caratteri.

È possibile limitare il caricamento iniziale dei dati a pochi byte ed eseguire il rilevamento dei set di caratteri solo su quei pochi byte.


8

Le librerie sopra sono semplici rilevatori di distinte materiali che ovviamente funzionano solo se c'è una distinta all'inizio del file. Dai un'occhiata a http://jchardet.sourceforge.net/ che scansiona il testo


18
solo un suggerimento, ma non c'è "sopra" su questo sito - considera di indicare le librerie a cui ti riferisci.
McDowell,

6

Per quanto ne so, non esiste una biblioteca generale in questo contesto adatta a tutti i tipi di problemi. Quindi, per ogni problema dovresti testare le librerie esistenti e selezionare quella migliore che soddisfi i vincoli del tuo problema, ma spesso nessuna di esse è appropriata. In questi casi puoi scrivere il tuo rilevatore di codifica! Come ho scritto ...

Ho scritto uno strumento meta java per rilevare la codifica del set di caratteri delle pagine Web HTML, utilizzando IBM ICU4j e Mozilla JCharDet come componenti integrati. Qui puoi trovare il mio strumento, leggi la sezione README prima di ogni altra cosa. Inoltre, puoi trovare alcuni concetti di base di questo problema nel mio documento e nei suoi riferimenti.

Di seguito ho fornito alcuni commenti utili che ho riscontrato nel mio lavoro:

  • Il rilevamento dei set di caratteri non è un processo infallibile, perché si basa essenzialmente su dati statistici e ciò che effettivamente accade è indovinare non rilevare
  • icu4j è lo strumento principale in questo contesto di IBM, imho
  • Sia TikaEncodingDetector che Lucene-ICU4j stanno usando icu4j e la loro accuratezza non ha avuto una differenza significativa rispetto alla icu4j nei miei test (al massimo% 1, come ricordo)
  • icu4j è molto più generale di jchardet, icu4j è solo leggermente distorto dalle codifiche della famiglia IBM mentre jchardet è fortemente distorto da utf-8
  • A causa dell'uso diffuso di UTF-8 nel mondo HTML; jchardet è una scelta migliore di icu4j in generale, ma non è la scelta migliore!
  • icu4j è ottimo per le codifiche specifiche dell'Asia orientale come EUC-KR, EUC-JP, SHIFT_JIS, BIG5 e le codifiche della famiglia GB
  • Sia icu4j che jchardet sono debacle nel trattare pagine HTML con codifiche Windows-1251 e Windows-1256. Windows-1251 aka cp1251 è ampiamente usato per le lingue basate sul cirillico come il russo e Windows-1256 aka cp1256 è ampiamente usato per l'arabo
  • Quasi tutti gli strumenti di rilevamento della codifica utilizzano metodi statistici, quindi l'accuratezza dell'output dipende fortemente dalle dimensioni e dal contenuto dell'input
  • Alcune codifiche sono essenzialmente le stesse solo con differenze parziali, quindi in alcuni casi la codifica indovinata o rilevata può essere falsa ma allo stesso tempo vera! Come per Windows-1252 e ISO-8859-1. (fare riferimento all'ultimo paragrafo nella sezione 5.2 del mio documento)


5

Se usi ICU4J ( http://icu-project.org/apiref/icu4j/ )

Ecco il mio codice:

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

Ricorda di mettere tutto il necessario per prenderlo.

Spero che questo funzioni per te.


IMO, questa risposta è perfetta. Se si desidera utilizzare ICU4j, provare invece questo: stackoverflow.com/a/4013565/363573 .
Stephan,


2

Per i file ISO8859_1, non esiste un modo semplice per distinguerli da ASCII. Per i file Unicode, tuttavia, è generalmente possibile rilevarlo in base ai primi byte del file.

I file UTF-8 e UTF-16 includono un Byte Order Mark (BOM) all'inizio del file. La distinta componenti è uno spazio non interruttivo di larghezza zero.

Sfortunatamente, per motivi storici, Java non lo rileva automaticamente. Programmi come Blocco note controlleranno la DBA e useranno la codifica appropriata. Utilizzando unix o Cygwin, è possibile controllare la distinta base con il comando file. Per esempio:

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

Per Java, ti suggerisco di dare un'occhiata a questo codice, che rileverà i formati di file comuni e selezionerà la codifica corretta: Come leggere un file e specificare automaticamente la codifica corretta


15
Non tutti i file UTF-8 o UTF-16 hanno una DBA, in quanto non è richiesta e la DBA UTF-8 è sconsigliata.
Christoffer Hammarström,

1

Un'alternativa a TikaEncodingDetector è utilizzare Tika AutoDetectReader .

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

Tike AutoDetectReader utilizza EncodingDetector caricato con ServiceLoader. Quali implementazioni EncodingDetector usi?
Stephan,

-1

In Java semplice:

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

Questo approccio proverà le codifiche una alla volta fino a quando non funzionerà o non ne saremo a corto. (A proposito il mio elenco di codifiche ha solo quegli elementi perché sono le implementazioni di set di caratteri richieste su ogni piattaforma Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )


Ma ISO-8859-1 (tra molti altri che non hai elencato) avrà sempre successo. E, naturalmente, è solo un'ipotesi, che non può recuperare i metadati persi che sono essenziali per la comunicazione di file di testo.
Tom Blodget,

Ciao @TomBlodget, stai suggerendo che l'ordine delle codifiche dovrebbe essere diverso?
Andres,

3
Sto dicendo che molti "funzioneranno", ma solo uno è "giusto". E non è necessario testare ISO-8859-1 perché "funzionerà" sempre.
Tom Blodget,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.