11

不幸的是,我正在从具有两种字符编码的文件中读取数据。

有一个标题和一个正文。标头始终采用 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();
    }
}
4

6 回答 6

3

为什么不使用 2InputStream秒?一个用于读取标题,另一个用于正文。

第二个InputStream应该skip是头字节。

于 2010-04-13T17:02:55.880 回答
3

这是伪代码。

  1. 使用InputStream,但不要 Reader环绕它。
  2. 读取包含标头的字节并将它们存储到 ByteArrayOutputStream.
  3. 创建ByteArrayInputStreamByteArrayOutputStream解码标头,这次ByteArrayInputStreamReaderASCII 字符集包装。
  4. 计算非 ascii 输入的长度,并将该字节数读入另一个ByteArrayOutputStream.
  5. ByteArrayInputStream 从第二个 创建另一个并用标题中的字符集ByteArrayOutputStream包装它。Reader
于 2010-04-13T17:06:31.813 回答
1

我的第一个想法是关闭流并重新打开它,InputStream#skip在将流提供给新的InputStreamReader.

如果您真的,真的不想重新打开文件,您可以使用文件描述符来获取多个文件流,尽管您可能必须使用通道在文件中拥有多个位置(因为您不能假设您可以使用 重置位置reset,可能不支持)。

于 2010-04-13T17:03:20.553 回答
1

我建议使用新的InputStreamReader. 也许假设这InputStream.mark是支持的。

于 2010-04-13T17:06:02.423 回答
1

它更容易:

正如您所说,您的标题始终为 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
    }
}
于 2010-06-29T08:43:27.330 回答
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));
于 2015-02-25T18:23:52.617 回答