2010-04-13 5 views
11

Я читаю данные из файла, который имеет, к сожалению, два типа кодировки символов.Ошибка буферизации InputStreamReader

Существует заголовок и корпус. Заголовок всегда находится в ASCII и определяет набор символов, в котором закодировано тело.

Заголовок не является фиксированной длиной и должен проходить через анализатор для определения его содержимого/длины.

Файл также может быть довольно большим, поэтому мне нужно избегать переноса всего содержимого в память.

Итак, я начал с одного InputStream. Я сначала переношу его с помощью InputStreamReader с ASCII и декодирует заголовок и извлекаю набор символов для тела. Все хорошо.

Затем я создаю новый InputStreamReader с правильным набором символов, бросаю его за тот же InputStream и начинаю пытаться читать тело.

К сожалению, похоже, что javadoc подтверждает это, что InputStreamReader может выбрать функцию чтения вперед для эффективного использования. Таким образом, чтение заголовка жевает некоторые/все тело.

Есть ли у кого-нибудь предложения по работе над этой проблемой? Будет ли создание CharsetDecoder вручную и подача в один байт за раз, но хорошая идея (возможно, завершена в пользовательскую реализацию Reader?)

Заранее спасибо.

EDIT: Моим окончательным решением было написать InputStreamReader, у которого нет буферизации, чтобы я мог разобрать заголовок без жевания части тела. Хотя это не очень эффективно, я обматываю исходный InputStream с помощью BufferedInputStream, поэтому это не будет проблемой.

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

Может быть, я ошибаюсь, но с того момента я думал, что файл может иметь только один тип кодировки одновременно. – Roman

+4

@Roman: вы можете делать все, что хотите, с файлами; это просто последовательности байтов. Таким образом, вы можете записать кучу байтов, которые должны интерпретироваться как ASCII, а затем выписать еще несколько байтов, предназначенных для интерпретации как UTF-16, и еще больше байтов, предназначенных для интерпретации как UTF-32. Я не говорю, что это хорошая идея, хотя пример использования OP, безусловно, является разумным (у вас должен быть * некоторый * способ указать, что кодирует файл, в конце концов). –

+0

@Mike Q - Хорошая идея InputStreamReaderUnbuffered. Я предлагаю отдельный ответ - он заслуживает внимания :) –

ответ

3

Почему бы вам не использовать 2 InputStream? Один для чтения заголовка, а другой для тела.

Второй InputStream должен skip заголовочные байты.

+0

Спасибо, я думаю, мне нужно это сделать. –

+0

Откуда вы знаете, что пропустить? Вам нужно прочитать заголовок, чтобы узнать, где он заканчивается. Когда вы начнете читать заголовок с помощью InputStreaReader, он может пережевывать байты из тела. –

1

Моя первая мысль - закрыть поток и снова открыть его, используя InputStream#skip, чтобы пропустить мимо заголовка, прежде чем дать потоку новому InputStreamReader.

Если вы действительно не хотите повторно открывать файл, вы можете использовать file descriptors, чтобы получить более одного потока в файл, хотя вам, возможно, придется использовать channels, чтобы иметь несколько позиций в файле (так как вы можете Предположим, вы можете сбросить позицию с помощью reset, она может не поддерживаться).

+0

Если вы создаете несколько 'FileInputStream' с тем же' FileDescriptor', то они будут вести себя так, как если бы они были одним и тем же потоком. –

+0

@Tom: Да, я предполагал, что он будет использовать их последовательно, а не параллельно, и что он сбросит позицию между использованием одного и другим. Но вы не можете предположить, что вы можете сбросить позицию ... (я не думаю, что они будут вести себя как * тот же поток *, я думаю, что это будет хуже, они просто разделяют фактическую позицию файла. кеширование в отдельных случаях могло бы теоретически сделать это действительно, действительно грязным, если вы попытались использовать их параллельно.) –

1

Предлагаю перечитать поток с самого начала с помощью нового InputStreamReader. Возможно, предположим, что поддерживается InputStream.mark.

3

Это псевдо-код.

  1. Использование InputStream, но не обернуть Reader вокруг него.
  2. Прочитать байты, содержащие заголовок, и сохранить их в ByteArrayOutputStream.
  3. Создание ByteArrayInputStream из ByteArrayOutputStream и декодировать заголовок, на этот раз обернуть ByteArrayInputStream в Reader с ASCII кодировкой.
  4. Вычислите длину не-ascii и введите это число байтов в другое ByteArrayOutputStream.
  5. Создайте еще один ByteArrayInputStream из второго ByteArrayOutputStream и завернуть его с Reader с кодировкой из заголовка .
+0

Спасибо за ваше предложение. К сожалению, заголовок не является фиксированной длиной, либо в двоичном, либо в символьном выражении, поэтому мне нужно разобрать его через декодер Charset, чтобы выяснить его структуру и, следовательно, ее длину. Мне также необходимо избегать считывания всего содержимого во внутренний буфер. –

1

Это еще проще:

Как вы сказали, ваш заголовок всегда в ASCII. Так что читайте заголовок непосредственно из InputStream, и когда вы закончите с этим, создать Ридер с правильной кодировкой и читать из него

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

Спасибо. В конце концов я пошел с другим решением, которое должно было написать InputStreamReaderUnbuffered, который делает то же самое, что и InputStreamReader, но не имеет внутреннего буфера, поэтому вы никогда не читаете слишком много. См. Мое редактирование. –

1

Если вы обернуть InputStream и ограничить все читает только 1 байт в время, кажется, отключает буферизацию внутри InputStreamReader.

Таким образом, нам не нужно переписывать логику 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); 
    } 
} 

Для построения:

new InputStreamReader(new OneByteReadInputStream(inputStream));