2010-04-13 13 views
11

Estoy leyendo datos de un archivo que tiene, desafortunadamente, dos tipos de codificación de caracteres.Problema de almacenamiento en búfer de InputStreamReader

Hay un encabezado y un cuerpo. El encabezado está siempre en ASCII y define el conjunto de caracteres en el que está codificado el cuerpo.

El encabezado no es de longitud fija y debe ejecutarse a través de un analizador para determinar su contenido/longitud.

El archivo también puede ser bastante grande, así que debo evitar traer todo el contenido a la memoria.

Así que comencé con un solo InputStream. Lo envuelvo inicialmente con un InputStreamReader con ASCII y decodifico el encabezado y extraigo el conjunto de caracteres para el cuerpo. Todo bien.

Luego creo un nuevo InputStreamReader con el juego de caracteres correcto, lo coloco sobre el mismo InputStream y comienzo a intentar leer el cuerpo.

Desafortunadamente, javadoc confirma esto, que InputStreamReader puede optar por la lectura anticipada para fines de efeciencia. Entonces la lectura del encabezado mastica todo/parte del cuerpo.

¿Alguien tiene alguna sugerencia para solucionar este problema? ¿Crearía un CharsetDecoder manualmente y se alimentaría en un byte a la vez pero sería una buena idea (posiblemente incluido en una implementación personalizada de Reader?)

Gracias de antemano.

EDITAR: Mi solución final fue escribir un InputStreamReader que no tiene buffering para asegurarme de que puedo analizar el encabezado sin masticar parte del cuerpo. Aunque esto no es demasiado eficiente, envuelvo el InputStream sin procesar con un BufferedInputStream para que no sea un problema.

// 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

Tal vez me equivoque, pero desde el momento en que pensé que el archivo puede tener un solo tipo de codificación al mismo tiempo. – Roman

+4

@Roman: puede hacer lo que quiera con los archivos; solo son secuencias de bytes. Así que puedes escribir un grupo de bytes que deben interpretarse como ASCII, y luego escribir un grupo de bytes para interpretarlos como UTF-16, e incluso más bytes para ser interpretados como UTF-32. No digo que sea una buena idea, aunque el caso de uso del OP es ciertamente razonable (hay que tener * alguna * forma de indicar qué codificación utiliza un archivo, después de todo). –

+0

@Mike Q - Buena idea, el InputStreamReaderUnbuffered. Sugiero una respuesta por separado: merece la atención :) –

Respuesta

3

¿Por qué no utiliza 2 InputStream s? Uno para leer el encabezado y otro para el cuerpo.

El segundo InputStream debe skip los bytes del encabezado.

+0

Gracias, creo que tendré que hacer esto. –

+0

¿Cómo sabes qué omitir? Debe leer el encabezado para saber dónde termina. Una vez que comienzas a leer el encabezado con un InputStreaReader, puede masticar bytes del cuerpo. –

1

Mi primer pensamiento es cerrar la secuencia y volver a abrirla, usando InputStream#skip para omitir el encabezado antes de dar la secuencia al nuevo InputStreamReader.

Si realmente, realmente no desea volver a abrir el archivo, se puede usar file descriptors para obtener más de una corriente al archivo, aunque puede que tenga que utilizar channels tener varias posiciones dentro del archivo (ya que se puede no asuma que puede restablecer la posición con reset, puede que no sea compatible).

+0

Si crea múltiples 'FileInputStream's con el mismo' FileDescriptor', entonces se comportarán como si fueran la misma secuencia. –

+0

@Tom: Sí, suponía que los usaría en serie, no en paralelo, y que restablecería la posición entre usar uno y usar el otro. Pero no puede asumir que puede restablecer la posición ... (No creo que se comporten como * la misma secuencia *, creo que sería peor que eso, simplemente compartirían la posición real del archivo. el almacenamiento en caché dentro de las instancias individuales podría, en teoría, ser realmente desordenado si intenta utilizarlos en paralelo.) –

1

Sugiero releer la secuencia desde el principio con un nuevo InputStreamReader. Tal vez suponga que es compatible con InputStream.mark.

3

Aquí está el pseudo código.

  1. Uso InputStream, pero no envolver un Reader alrededor de ella.
  2. Lea los bytes que contienen el encabezado y guárdelos en ByteArrayOutputStream.
  3. Crear ByteArrayInputStream de ByteArrayOutputStream y decodificar cabecera, esta vez envolver ByteArrayInputStream en Reader con juego de caracteres ASCII.
  4. Calcule la longitud de la entrada que no es ascii , y lea esa cantidad de bytes en otro ByteArrayOutputStream.
  5. crear otro ByteArrayInputStream de la segunda ByteArrayOutputStream y se envuelve con Reader con juego de caracteres de la cabecera .
+0

Gracias por su sugerencia. Lamentablemente, el encabezado no es de longitud fija, ni en términos binarios ni de caracteres, por lo que es necesario analizarlo a través de un decodificador Charset para determinar su estructura y, por lo tanto, su longitud. También necesito evitar leer todo el contenido en un búfer interno. –

1

Es aún más fácil:

Como usted ha dicho, su cabecera es siempre en ASCII. Así que leer el encabezado directamente desde el InputStream, y cuando haya terminado con él, cree el lector con la codificación correcta y leer de él

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

Gracias. Eventualmente fui con otra solución que era escribir un InputStreamReaderUnbuffered que hace exactamente lo mismo que InputStreamReader pero no tiene un búfer interno así que nunca lees demasiado. Ver mi edición –

1

Si envuelve el InputStream y limitar todas las lecturas a sólo 1 byte cada una vez, parece desactivar el almacenamiento en búfer dentro de InputStreamReader.

De esta forma no es necesario volver a escribir la lógica de 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); 
    } 
} 

Para construir:

new InputStreamReader(new OneByteReadInputStream(inputStream)); 
Cuestiones relacionadas