问题 InputStreamReader缓冲问题


我正在从一个文件中读取数据,遗憾的是,这种文件有两种类型的字符编码。

有标题和正文。标头始终为ASCII,并定义正文编码的字符集。

标头不是固定长度,必须通过解析器运行以确定其内容/长度。

该文件也可能非常大,所以我需要避免将整个内容带入内存。

所以我开始使用单个InputStream。我最初使用带有ASCII的InputStreamReader包装它并解码标头并提取主体的字符集。都好。

然后我创建一个具有正确字符集的新InputStreamReader,将其放在同一个InputStream上并开始尝试读取正文。

不幸的是,javadoc证实了这一点,即InputStreamReader可能会选择提前读取以达到效率目的。因此,标题的阅读会咀嚼身体的一部分/全部。

有没有人有解决这个问题的建议?会手动创建一个CharsetDecoder并一次输入一个字节但是一个好主意(可能包含在一个自定义的Reader实现中吗?)

提前致谢。

编辑:我的最终解决方案是编写一个没有缓冲的InputStreamReader,以确保我可以解析标题而不会咀嚼身体的一部分。虽然这不是非常有效,但我使用BufferedInputStream包装原始InputStream,因此它不会成为问题。

// 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();
    }
}

7224
2018-04-13 16:53


起源

也许我错了,但从那时起我认为该文件只能同时拥有一种编码类型。 - Roman
@Roman:你可以用文件做任何你想做的事情;它们只是字节序列。因此,你可以写出一堆意味着被解释为ASCII的字节,然后写出一堆更多的字节意味着被解释为UTF-16,甚至更多的字节意味着被解释为UTF-32。我不是说这是一个好主意,虽然OP的用例肯定是合理的(你必须拥有 一些 毕竟,指示文件编码使用的方式。 - T.J. Crowder
@Mike Q - 好主意是InputStreamReaderUnbuffered。我建议一个单独的答案 - 值得关注:) - AlikElzin-kilaka
关于InputStreamReaderUnbuffered解决方案:如果字节缓冲区的大小为1,那么如何使用属于单个字符的2个字节? - AlikElzin-kilaka


答案:


你为什么不用2 InputStreamS'一个用于读取标题,另一个用于读取身体。

第二 InputStream 应该 skip 标头字节。


3
2018-04-13 17:02



谢谢,我想我必须这样做。 - Mike Q
你怎么知道要跳过什么?您需要阅读标题才能知道它的结束位置。一旦你开始用InputStreaReader读取头文件,它就可以从正文中咀嚼字节。 - AlikElzin-kilaka


这是伪代码。

  1. 使用 InputStream,但不要包装 Reader 周围。
  2. 读取包含标题和的字节 将它们存入 ByteArrayOutputStream
  3. 创建 ByteArrayInputStream 从 ByteArrayOutputStream 和解码 标题,这次换行 ByteArrayInputStreamReader 使用ASCII字符集。
  4. 计算非ascii的长度 输入,并读取该字节数 进入另一个 ByteArrayOutputStream
  5. 创建另一个 ByteArrayInputStream 从第二个 ByteArrayOutputStream 并包装它 同 Reader 来自的charset 头。

3
2018-04-13 17:06



谢谢你的建议。不幸的是,标题不是固定长度,无论是二进制还是字符术语,所以我需要通过Charset解码器解析它以确定其结构,从而确定其长度。我还需要避免将整个内容读入内部缓冲区。 - Mike Q


我建议从一开始就用新的重新读取流 InputStreamReader。或许假设 InputStream.mark 得到支持。


1
2018-04-13 17:06





我的第一个想法是关闭流并重新打开它,使用 InputStream#skip 在将流提供给新流之前跳过标题 InputStreamReader

如果你真的,真的不想重新打开文件,你可以使用 文件描述符 尽管你可能不得不使用,但要获取多个流到文件 渠道 在文件中有多个位置(因为你不能假设你可以重置位置 reset,它可能不受支持)。


1
2018-04-13 17:03



如果你创建多个 FileInputStream同样的 FileDescriptor,然后他们会表现得好像他们是同一个流。 - Tom Hawtin - tackline
@Tom:是的,我假设他会将它们串联使用,而不是并行使用,并且他会在使用一个和使用另一个之间重置位置。但你不能假设你可以重置位置...(我不认为他们的行为会像 相同的流,我认为会比这更糟糕;他们只是分享实际档案位置。理论上,如果你试图并行使用它们,那么在各个实例中的数据缓存可能真的非常麻烦。) - T.J. Crowder


它更容易:

正如您所说,您的标题始终为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
2018-06-29 08:43



谢谢。最后,我选择了另一个解决方案,即编写一个与InputStreamReader完全相同但没有内部缓冲区的InputStreamReaderUnbuffered,因此你永远不会读太多。看我的编辑。 - Mike Q


如果你将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
2018-02-25 18:23