2010-04-13 3 views
11

Je lis des données à partir d'un fichier qui comporte, malheureusement, deux types de codage de caractères.Problème de mise en mémoire tampon InputStreamReader

Il y a un en-tête et un corps. L'en-tête est toujours en ASCII et définit le jeu de caractères dans lequel le corps est codé.

L'en-tête n'est pas de longueur fixe et doit être parcouru par un analyseur pour déterminer son contenu/sa longueur.

Le fichier peut également être assez volumineux, je dois donc éviter de mettre tout le contenu en mémoire. J'ai donc commencé avec un seul InputStream. Je l'emballe d'abord avec un InputStreamReader avec ASCII et décode l'en-tête et extrait le jeu de caractères pour le corps. Tout bon.

Ensuite, je crée un nouveau InputStreamReader avec le jeu de caractères correct, le dépose sur le même InputStream et commence à essayer de lire le corps.

Malheureusement, il semble que javadoc confirme que InputStreamReader peut choisir de lire en avance pour des raisons d'efficacité. Donc, la lecture de l'en-tête mâche tout/tout le corps.

Est-ce que quelqu'un a des suggestions pour contourner ce problème? Est-ce que créer un CharsetDecoder manuellement et nourrir dans un octet à la fois mais une bonne idée (peut-être enveloppé dans une implémentation de lecteur personnalisé?)

Merci d'avance.

EDIT: Ma dernière solution consistait à écrire un InputStreamReader sans tampon pour m'assurer que je puisse analyser l'en-tête sans mâcher une partie du corps. Bien que ce ne soit pas terriblement efficace, j'enveloppe le InputStream brut avec un BufferedInputStream donc ce ne sera pas un problème.

// An InputStreamReader that only consumes as many bytes as is necessary 
// It does not do any read-ahead. 
public class InputStreamReaderUnbuffered extends Reader 
{ 
    private final CharsetDecoder charsetDecoder; 
    private final InputStream inputStream; 
    private final ByteBuffer byteBuffer = ByteBuffer.allocate(1); 

    public InputStreamReaderUnbuffered(InputStream inputStream, Charset charset) 
    { 
     this.inputStream = inputStream; 
     charsetDecoder = charset.newDecoder(); 
    } 

    @Override 
    public int read() throws IOException 
    { 
     boolean middleOfReading = false; 

     while (true) 
     { 
      int b = inputStream.read(); 

      if (b == -1) 
      { 
       if (middleOfReading) 
        throw new IOException("Unexpected end of stream, byte truncated"); 

       return -1; 
      } 

      byteBuffer.clear(); 
      byteBuffer.put((byte)b); 
      byteBuffer.flip(); 

      CharBuffer charBuffer = charsetDecoder.decode(byteBuffer); 

      // although this is theoretically possible this would violate the unbuffered nature 
      // of this class so we throw an exception 
      if (charBuffer.length() > 1) 
       throw new IOException("Decoded multiple characters from one byte!"); 

      if (charBuffer.length() == 1) 
       return charBuffer.get(); 

      middleOfReading = true; 
     } 
    } 

    public int read(char[] cbuf, int off, int len) throws IOException 
    { 
     for (int i = 0; i < len; i++) 
     { 
      int ch = read(); 

      if (ch == -1) 
       return i == 0 ? -1 : i; 

      cbuf[ i ] = (char)ch; 
     } 

     return len; 
    } 

    public void close() throws IOException 
    { 
     inputStream.close(); 
    } 
} 
+1

Peut-être que je me trompe, mais depuis le moment où je pensais que ce fichier peut avoir qu'un seul type de codage en même temps. – Roman

+4

@Roman: Vous pouvez faire tout ce que vous voulez avec des fichiers; ce ne sont que des séquences d'octets. Vous pouvez donc écrire un tas d'octets qui sont censés être interprétés comme ASCII, puis écrire un paquet plus d'octets censés être interprétés comme UTF-16, et encore plus d'octets censés être interprétés comme UTF-32. Je ne dis pas que c'est une bonne idée, bien que le cas d'utilisation de l'OP soit certainement raisonnable (vous devez avoir une * certaine * façon d'indiquer ce que l'encodage d'un fichier utilise, après tout). –

+0

@Mike Q - Bonne idée de InputStreamReaderUnbuffered. Je suggère une réponse séparée - elle mérite l'attention :) –

Répondre

3

Pourquoi n'utilisez-vous pas 2 InputStream s? Un pour lire l'en-tête et un autre pour le corps.

La seconde InputStream doit skip octets d'en-tête.

+0

Merci, je pense que je vais devoir le faire. –

+0

Comment savez-vous quoi sauter? Vous devez lire l'en-tête afin de savoir où cela se termine. Une fois que vous commencez à lire l'en-tête avec un InputStreaReader, il peut mâcher des octets du corps. –

1

Ma première pensée est de fermer le flux et le rouvrir, en utilisant InputStream#skip pour ignorer l'en-tête avant de donner le courant à la nouvelle InputStreamReader. Si vous ne voulez vraiment pas vraiment rouvrir le fichier, vous pouvez utiliser file descriptors pour obtenir plus d'un flux dans le fichier, bien que vous deviez utiliser channels pour avoir plusieurs positions dans le fichier (puisque vous pouvez Supposons que vous pouvez réinitialiser la position avec reset, il peut ne pas être pris en charge).

+0

Si vous créez plusieurs 'FileInputStream' avec le même' FileDescriptor', alors ils se comporteront comme s'il s'agissait du même flux. –

+0

@Tom: Oui, je supposais qu'il les utiliserait en série, pas en parallèle, et qu'il allait réinitialiser la position entre l'utilisation de l'un et l'utilisation de l'autre. Mais vous ne pouvez pas supposer que vous pouvez réinitialiser la position ... (Je ne pense pas qu'ils se comporteront comme le * même flux *, je pense que ce serait pire que cela, ils partageraient simplement la position réelle du fichier. la mise en cache au sein des instances individuelles pourrait en théorie rendre vraiment, vraiment désordonné si vous avez essayé de les utiliser en parallèle.) –

1

Je suggère de relire le flux dès le début avec un nouveau InputStreamReader. Supposons peut-être que InputStream.mark est supporté.

3

Voici le pseudo code.

  1. Utilisez InputStream, mais ne pas envelopper un Reader autour d'elle.
  2. Lisez les octets contenant l'en-tête et stockez-les dans ByteArrayOutputStream.
  3. Créer ByteArrayInputStream de ByteArrayOutputStream et décoder en-tête , cette fois envelopper ByteArrayInputStream en Reader avec charset ASCII.
  4. Calculez la longueur de l'entrée non-ascii et lisez ce nombre d'octets dans un autre ByteArrayOutputStream.
  5. Créer un autre ByteArrayInputStream de la deuxième ByteArrayOutputStream et l'envelopper avec Reader avec charset de l'en-tête .
+0

Merci pour votre suggestion. Malheureusement, l'en-tête n'a pas de longueur fixe, que ce soit en termes binaires ou de caractères, j'ai donc besoin de l'analyser à travers un décodeur Charset pour comprendre sa structure et donc sa longueur. Je dois également éviter de lire tout le contenu dans un tampon interne. –

1

Il est encore plus facile:

Comme vous l'avez dit, votre tête est toujours en ASCII. Alors, lisez l'en-tête directement à partir du InputStream, et quand vous avez terminé, créez le lecteur avec l'encodage correct et lire ce

private Reader reader; 
private InputStream stream; 

public void read() { 
    int c = 0; 
    while ((c = stream.read()) != -1) { 
     // Read encoding 
     if (headerFullyRead) { 
      reader = new InputStreamReader(stream, encoding); 
      break; 
     } 
    } 
    while ((c = reader.read()) != -1) { 
     // Handle rest of file 
    } 
} 
+0

Merci. Finalement, je suis allé avec une autre solution qui était d'écrire un InputStreamReaderUnbuffered qui fait exactement la même chose que InputStreamReader, mais n'a pas de tampon interne de sorte que vous ne lisiez jamais trop. Voir ma modification. –

1

Si vous enroulez le InputStream et limiter toutes les lectures à seulement 1 octet à une fois, il semble désactiver la mise en mémoire tampon à l'intérieur de InputStreamReader. De cette façon, nous n'avons pas besoin de réécrire la logique InputStreamReader.

public class OneByteReadInputStream extends InputStream 
{ 
    private final InputStream inputStream; 

    public OneByteReadInputStream(InputStream inputStream) 
    { 
     this.inputStream = inputStream; 
    } 

    @Override 
    public int read() throws IOException 
    { 
     return inputStream.read(); 
    } 

    @Override 
    public int read(byte[] b, int off, int len) throws IOException 
    { 
     return super.read(b, off, 1); 
    } 
} 

Pour construire:

new InputStreamReader(new OneByteReadInputStream(inputStream)); 
Questions connexes