Ошибка буферизации InputStreamReader - PullRequest
11 голосов
/ 13 апреля 2010

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

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

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

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

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

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

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

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

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

РЕДАКТИРОВАТЬ: мое окончательное решение было написать 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();
    }
}

Ответы [ 6 ]

3 голосов
/ 13 апреля 2010

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

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

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

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

1 голос
/ 25 февраля 2015

Если вы оберните 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));
1 голос
/ 29 июня 2010

Это даже проще:

Как вы сказали, ваш заголовок всегда в 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
    }
}
1 голос
/ 13 апреля 2010

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

1 голос
/ 13 апреля 2010

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

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

...