Проблема буферизации 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

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

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

Ответ 2

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

  • Используйте InputStream, но не Reader вокруг него.
  • Чтение байтов, содержащих заголовок и храните их в ByteArrayOutputStream.
  • Создайте ByteArrayInputStream из ByteArrayOutputStream и декодировать заголовок, на этот раз wrap ByteArrayInputStream в Reader с кодировкой ASCII.
  • Вычислить длину не-ascii ввод и чтение этого количества байтов в другой ByteArrayOutputStream.
  • Создайте еще один ByteArrayInputStream со второго ByteArrayOutputStream и оберните его с Reader с кодировкой из заголовок.

Ответ 3

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

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

Ответ 4

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

Ответ 5

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

Как вы сказали, ваш заголовок всегда находится в ASCII. Поэтому читайте заголовок непосредственно из InputStream, и когда вы закончите с ним, создайте Reader с правильным кодированием и прочитайте с него

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

Ответ 6

Если вы завершаете InputStream и ограничиваете все чтения только одним байтом за раз, он, похоже, отключает буферизацию внутри 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));