web-dev-qa-db-fra.com

Comment lire un fichier texte avec des encodages mixtes en Scala ou Java?

J'essaie d'analyser un fichier CSV, en utilisant idéalement weka.core.converters.CSVLoader. Cependant, le fichier que j'ai n'est pas un fichier UTF-8 valide. Il s'agit principalement d'un fichier UTF-8 mais certaines des valeurs de champ sont dans des encodages différents, il n'y a donc pas d'encodage dans lequel tout le fichier est valide, mais je dois quand même l'analyser. Hormis l'utilisation de bibliothèques Java comme Weka, je travaille principalement dans Scala. Je ne suis même pas capable de lire le fichier usin scala.io.Source: Par exemple

Source.
  fromFile(filename)("UTF-8").
  foreach(print);

jette:

    Java.nio.charset.MalformedInputException: Input length = 1
at Java.nio.charset.CoderResult.throwException(CoderResult.Java:277)
at Sun.nio.cs.StreamDecoder.implRead(StreamDecoder.Java:337)
at Sun.nio.cs.StreamDecoder.read(StreamDecoder.Java:176)
at Java.io.InputStreamReader.read(InputStreamReader.Java:184)
at Java.io.BufferedReader.fill(BufferedReader.Java:153)
at Java.io.BufferedReader.read(BufferedReader.Java:174)
at scala.io.BufferedSource$$anonfun$iter$1$$anonfun$apply$mcI$sp$1.apply$mcI$sp(BufferedSource.scala:38)
at scala.io.Codec.wrap(Codec.scala:64)
at scala.io.BufferedSource$$anonfun$iter$1.apply(BufferedSource.scala:38)
at scala.io.BufferedSource$$anonfun$iter$1.apply(BufferedSource.scala:38)
at scala.collection.Iterator$$anon$14.next(Iterator.scala:150)
at scala.collection.Iterator$$anon$25.hasNext(Iterator.scala:562)
at scala.collection.Iterator$$anon$19.hasNext(Iterator.scala:400)
at scala.io.Source.hasNext(Source.scala:238)
at scala.collection.Iterator$class.foreach(Iterator.scala:772)
at scala.io.Source.foreach(Source.scala:181)

Je suis parfaitement heureux de jeter tous les caractères invalides ou de les remplacer par un mannequin. Je vais avoir beaucoup de texte comme celui-ci à traiter de différentes manières et je devrai peut-être transmettre les données à diverses bibliothèques tierces. Une solution idéale serait une sorte de paramètre global qui ferait en sorte que toutes les bibliothèques de bas niveau Java ignorent les octets invalides dans le texte, afin que je puisse appeler des bibliothèques tierces sur ces données sans modification.

SOLUTION:

import Java.nio.charset.CodingErrorAction
import scala.io.Codec

implicit val codec = Codec("UTF-8")
codec.onMalformedInput(CodingErrorAction.REPLACE)
codec.onUnmappableCharacter(CodingErrorAction.REPLACE)

val src = Source.
  fromFile(filename).
  foreach(print)

Merci à + Esailija de m'avoir pointé dans la bonne direction. Cela m'a amené à Comment détecter les séquences d'octets UTF-8 illégales pour les remplacer dans Java inputstream? qui fournit le noyau Java Dans Scala je peux en faire le comportement par défaut en rendant le codec implicite. Je pense que je peux en faire le comportement par défaut pour l'ensemble du package en lui mettant la définition implicite du codec dans l'objet package .

49
Daniel Mahler

Voici comment j'ai réussi à le faire avec Java:

    FileInputStream input;
    String result = null;
    try {
        input = new FileInputStream(new File("invalid.txt"));
        CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
        decoder.onMalformedInput(CodingErrorAction.IGNORE);
        InputStreamReader reader = new InputStreamReader(input, decoder);
        BufferedReader bufferedReader = new BufferedReader( reader );
        StringBuilder sb = new StringBuilder();
        String line = bufferedReader.readLine();
        while( line != null ) {
            sb.append( line );
            line = bufferedReader.readLine();
        }
        bufferedReader.close();
        result = sb.toString();

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch( IOException e ) {
        e.printStackTrace();
    }

    System.out.println(result);

Le fichier invalide est créé avec des octets:

0x68, 0x80, 0x65, 0x6C, 0x6C, 0xC3, 0xB6, 0xFE, 0x20, 0x77, 0xC3, 0xB6, 0x9C, 0x72, 0x6C, 0x64, 0x94

Lequel est hellö wörld en UTF-8 avec 4 octets invalides mélangés.

Avec .REPLACE vous voyez le caractère de remplacement unicode standard utilisé:

//"h�ellö� wö�rld�"

Avec .IGNORE, vous voyez les octets invalides ignorés:

//"hellö wörld"

Sans préciser .onMalformedInput, vous obtenez

Java.nio.charset.MalformedInputException: Input length = 1
    at Java.nio.charset.CoderResult.throwException(Unknown Source)
    at Sun.nio.cs.StreamDecoder.implRead(Unknown Source)
    at Sun.nio.cs.StreamDecoder.read(Unknown Source)
    at Java.io.InputStreamReader.read(Unknown Source)
    at Java.io.BufferedReader.fill(Unknown Source)
    at Java.io.BufferedReader.readLine(Unknown Source)
    at Java.io.BufferedReader.readLine(Unknown Source)
24
Esailija

La solution pour la source de scala (basée sur la réponse @Esailija):

def toSource(inputStream:InputStream): scala.io.BufferedSource = {
    import Java.nio.charset.Charset
    import Java.nio.charset.CodingErrorAction
    val decoder = Charset.forName("UTF-8").newDecoder()
    decoder.onMalformedInput(CodingErrorAction.IGNORE)
    scala.io.Source.fromInputStream(inputStream)(decoder)
}
14
raisercostin

Le codec de Scala a un champ décodeur qui renvoie un Java.nio.charset.CharsetDecoder:

val decoder = Codec.UTF8.decoder.onMalformedInput(CodingErrorAction.IGNORE)
Source.fromFile(filename)(decoder).getLines().toList
13
maxmc

Je passe à un codec différent en cas de défaillance.

Afin de mettre en œuvre le modèle, je me suis inspiré de cette autre question de stackoverflow .

J'utilise une liste de codecs par défaut et je les parcours récursivement. S'ils échouent tous, j'imprime les bits effrayants:

private val defaultCodecs = List(
  io.Codec("UTF-8"),
  io.Codec("ISO-8859-1")
)

def listLines(file: Java.io.File, codecs:Iterable[io.Codec] = defaultCodecs): Iterable[String] = {
  val codec = codecs.head
  val fileHandle = scala.io.Source.fromFile(file)(codec)
  try {
    val txtArray = fileHandle.getLines().toList
    txtArray
  } catch {
    case ex: Exception => {
      if (codecs.tail.isEmpty) {
        println("Exception:  " + ex)
        println("Skipping file:  " + file.getPath)
        List()
      } else {
        listLines(file, codecs.tail)
      }
    }
  } finally {
    fileHandle.close()
  }
}

J'apprends juste Scala, donc le code n'est peut-être pas optimal.

2
Harry Pehkonen

Le problème avec l'ignorance des octets invalides est alors de décider quand ils sont à nouveau valides. Notez que UTF-8 autorise les codages d'octets de longueur variable pour les caractères, donc si un octet n'est pas valide, vous devez comprendre à partir de quel octet commencer la lecture pour obtenir à nouveau un flux de caractères valide.

En bref, je ne pense pas que vous trouverez une bibliothèque qui puisse `` corriger '' en lisant. Je pense qu'une approche beaucoup plus productive consiste à essayer de nettoyer ces données en premier.

2
Brian Agnew

Une solution simple serait d'interpréter votre flux de données en ASCII, d'ignorer tous les caractères non textuels. Cependant, vous perdriez même des caractères UTF8 encodés valides. Je ne sais pas si cela vous convient.

EDIT: Si vous savez à l'avance quelles colonnes sont UTF-8 valides, vous pouvez écrire votre propre analyseur CSV qui peut être configuré quelle stratégie utiliser sur quelle colonne.

0
mbelow

Utilisation ISO-8859-1 comme codeur; cela vous donnera simplement des valeurs d'octets regroupées dans une chaîne. Cela suffit pour analyser CSV pour la plupart des encodages. (Si vous avez mélangé des blocs 8 bits et 16 bits, vous avez des problèmes; vous pouvez toujours lire les lignes dans ISO-8859-1, mais vous ne pourrez peut-être pas analyser la ligne en tant que bloc.)

Une fois que vous avez les champs individuels sous forme de chaînes distinctes, vous pouvez essayer

new String(oldstring.getBytes("ISO-8859-1"), "UTF-8")

pour générer la chaîne avec l'encodage approprié (utilisez le nom d'encodage approprié par champ, si vous le connaissez).

Edit: vous devrez utiliser Java.nio.charset.Charset.CharsetDecoder si vous souhaitez détecter des erreurs. Le mappage vers UTF-8 de cette façon vous donnera simplement 0xFFFF dans votre chaîne en cas d'erreur.

val decoder = Java.nio.charset.Charset.forName("UTF-8").newDecoder

// By default will throw a MalformedInputException if encoding fails
decoder.decode( Java.nio.ByteBuffer.wrap(oldstring.getBytes("ISO-8859-1")) ).toString
0
Rex Kerr