读取字符行并获取文件位置

Posted

技术标签:

【中文标题】读取字符行并获取文件位置【英文标题】:Read lines of characters and get file position 【发布时间】:2015-08-18 02:33:18 【问题描述】:

我正在从文本文件中读取连续的 字符 行。文件中字符的编码可能不是单字节的。

在某些时候,我想获取下一行开始的文件位置,以便稍后我可以重新打开文件并快速返回到该位置

问题

有没有一种简单的方法可以做到这两点,最好是使用标准 Java 库?

如果不是,什么是合理的解决方法?

理想解决方案的属性

理想的解决方案是处理多种字符编码。这包括 UTF-8,其中不同的字符可以用不同的字节数表示。一个理想的解决方案主要依赖于一个值得信赖的、得到良好支持的库。最理想的是标准 Java 库。其次是 Apache 或 Google 库。解决方案必须是可扩展的。将整个文件读入内存不是解决方案。返回一个位置不需要在线性时间内读取所有先前的字符。

详情

对于第一个要求,BufferedReader.readLine() 很有吸引力。但是缓冲显然会干扰获得有意义的文件位置。

不太明显,InputStreamReader 也可以提前读取,干扰获取文件位置。来自InputStreamReader documentation:

为了实现字节到字符的高效转换,可能会从底层流中提前读取比满足当前读取操作所需的更多的字节。

方法RandomAccessFile.readLine()reads a single byte per character。

通过获取字符低八位的字节值并将字符的高八位设置为零,将每个字节转换为字符。因此,此方法不支持完整的 Unicode 字符集。

【问题讨论】:

您需要完整的 unicode 支持吗? 作为一个 FYI,我认为大多数具有 readLine() 的 java 类也会修剪尾随空格/换行符,所以即使你只支持 ASCII,你的偏移量仍然会关闭 @dkatzel - 仅是 Java 支持的 - 16 位 unicode 字符,IIUC 被称为基本多语言平面。是的,我从类文档中看到BufferedReader.getLine()RandomAccessFile.readLine() 都从返回值中读取和剥离行终止符。但是,我认为剥离只会影响返回值,而不是文件位置。 @Andy_Thomas 正确,但是,如果您尝试通过 line.length() 计算文件位置,则由于剥离了终止符,计算将被关闭 @AndyThomas Charset 类本身甚至必须支持遗留错误这一事实表明,这里没有适用于所有可能的字符编码的简单解决方案。阅读文本文件是一个令人惊讶的难题。 【参考方案1】:

最初,我发现 Andy Thomas (https://***.com/a/30850145/556460) 建议的方法最合适。

但不幸的是,当文件行包含非拉丁字符时,我无法成功地将字节数组(取自 RandomAccessFile.readLine)转换为正确的字符串。

所以我重写了这个方法,编写了一个类似于RandomAccessFile.readLine 本身的函数,它从行中收集数据,而不是直接收集到字符串,而是直接收集到字节数组,然后从字节数组构造所需的字符串。 所以下面的代码完全满足了我的需求(在 Kotlin 中)。

调用函数后,file.channel.position() 会返回下一行的准确位置(如果有的话):

fun RandomAccessFile.readEncodedLine(charset: Charset = Charsets.UTF_8): String? 
    val lineBytes = ByteArrayOutputStream()
    var c = -1
    var eol = false

    while (!eol) 
        c = read()
        when (c) 
            -1, 10 -> eol = true // \n
            13     ->  // \r
                eol = true
                val cur = filePointer
                if (read() != '\n'.toInt()) 
                    seek(cur)
                
            
            else   -> lineBytes.write(c)
        
    

    return if (c == -1 && lineBytes.size() == 0)
        null
    else
        java.lang.String(lineBytes.toByteArray(), charset) as String

【讨论】:

【参考方案2】:

这个案例似乎被 VTD-XML 解决了,这是一个能够快速解析大型 XML 文件的库:

最后一个 java VTD-XML ximpleware 实现,当前是 2.13 http://sourceforge.net/projects/vtd-xml/files/vtd-xml/ 提供了一些代码,在每次调用其 IReader 实现的 getChar() 方法后维护一个字节偏移量。

在 VTDGen.java 和 VTDGenHuge.java 中提供了各种字符编码的 IReader 实现

为以下编码提供了 IReader 实现

ASCII; ISO_8859_1 ISO_8859_10 ISO_8859_11 ISO_8859_12 ISO_8859_13 ISO_8859_14 ISO_8859_15 ISO_8859_16 ISO_8859_2 ISO_8859_3 ISO_8859_4 ISO_8859_5 ISO_8859_6 ISO_8859_7 ISO_8859_8 ISO_8859_9 UTF_16BE UTF_16LE UTF8; WIN_1250 WIN_1251 WIN_1252 WIN_1253 WIN_1254 WIN_1255 WIN_1256 WIN_1257 WIN_1258

【讨论】:

使用 getCharOffset() 方法更新 IReader 并通过将 charCount 成员添加到 VTDGen 和 VTDGenHuge 类的偏移成员并在每次调用 getChar() 和 skipChar() 时递增它来实现它每个 IReader 实现都可能为您提供解决方案。【参考方案3】:

此部分解决方法仅适用于使用 7 位 ASCII 或 UTF-8 编码的文件。具有通用解决方案的答案仍然是可取的(对这种解决方法的批评也是如此)。

在 UTF-8 中:

所有单字节字符都可以与多字节字符中的所有字节区分开来。多字节字符中的所有字节在高位都有一个“1”。特别是,表示 LF 和 CR 的字节不能是多字节字符的一部分。 所有单字节字符都是 7 位 ASCII。因此,我们可以使用 UTF-8 解码器解码仅包含 7 位 ASCII 字符的文件。

综合起来,这两点意味着我们可以用读取字节而不是字符的东西来读取一行,然后对该行进行解码。

为了避免缓冲问题,我们可以使用RandomAccessFile。该类提供读取行和获取/设置文件位置的方法。

这是使用 RandomAccessFile 将下一行读取为 UTF-8 的代码草图。

protected static String 
readNextLineAsUTF8( RandomAccessFile in ) throws IOException 
    String rv = null;
    String lineBytes = in.readLine();
    if ( null != lineBytes ) 
        rv = new String( lineBytes.getBytes(),
            StandardCharsets.UTF_8 );
    
    return rv;
  

然后可以在调用该方法之前立即从 RandomAccessFile 获取文件位置。给定 in 引用的 RandomAccessFile:

    long startPos = in.getFilePointer();
    String line = readNextLineAsUTF8( in );

【讨论】:

【参考方案4】:

RandomAccessFile 有一个功能: 寻找(长位置) 设置文件指针偏移量,从该文件的开头开始测量,下一次读取或写入发生的位置。

【讨论】:

是的......但问题是我如何获取文件位置......当读取任何可能的字符编码的字符行时。跨度> 【参考方案5】:

解决方案 A

    在循环中使用RandomAccessFile.readChar() 或RandomAccessFile.readByte()。 检查您的 EOL 字符,然后处理该行。

其他任何事情的问题在于,您必须绝对确保您从未读过 EOL 字符。

readChar() 返回一个 char 而不是一个字节。所以你不必担心字符宽度。

从此文件中读取一个字符。此方法从文件中读取两个字节,从当前文件指针开始。

[...]

此方法一直阻塞,直到读取了两个字节、检测到流结束或抛出异常。

通过使用 RandomAccessFile 而不是 Reader,您将放弃 Java 为您解码文件中字符集的能力。 BufferedReader 会自动执行此操作。

有几种方法可以解决这个问题。一种是自己检测编码,然后使用正确的 read*() 方法。另一种方法是使用 BoundedInput 流。

这个问题有一个Java: reading strings from a random access file with buffered input

例如https://***.com/a/4305478/16549

【讨论】:

这是否等同于简单地调用RandomAccessFile.getLine()?如上所述,字符的编码不一定是单字节。 哪一部分?读字符()?不,readChar() 将始终返回一个字符,而不是一个字节。使用第二种解决方案,您可以倒带,因此唯一的限制是您的读取限制。 抱歉,我错过了readChar()。不幸的是,readChar() 的文档和源代码都显示它恰好读取两个字节,大概是 UTF-16。如果文件使用单字节编码或 UTF-8,这将不起作用。 Java 字符总是 2 个字节宽。 UTF-8、UTF-16 或其他。 UTF-8 是一个可变宽度字符集,长度可以是 1 到 4 个字节。如果达到EOF,就可以处理异常,将单字节作为待处理的char。 Java 字符是两个字节。但文件中的字符不一定和内存中的Java字符编码相同。【参考方案6】:

如果您从FileReader 构造BufferedReader 并保持FileReader 的实例可供您的代码访问,您应该能够通过调用获得下一行的位置:

fileReader.getChannel().position();

在致电bufferedReader.readLine()之后。

BufferedReader 可以使用大小为 1 的输入缓冲区构建,如果您愿意以性能提升换取位置精度。

替代解决方案 自己跟踪字节会有什么问题:

long startingPoint = 0; // or starting position if this file has been previously processed

while (readingLines) 
    String line = bufferedReader.readLine();
    startingPoint += line.getBytes().length;

无论底层标记或缓冲如何,这都会为您提供与您已处理的内容准确的字节数。您必须在计数中考虑行尾,因为它们已被剥离。

【讨论】:

一个 BufferedReader 缓冲它的输入。对BufferedReader.readLine() 的调用可以从底层 FileReader 将更多内容读入缓冲区,而不仅仅是下一行——使该位置超过下一行的位置。 缓冲区大小可以为 0。它违背了我认为拥有 BufferedReader 的目的,除了它提供了不必自己进行行解析/字符编码逻辑的便利。正如您所指出的,即使 InputStreamReader 也可能正在预读。 缓冲区大小是否为 1 提供了足够合理的位置?您始终可以从文件通道报告的位置中减去 1。最糟糕的事情是重新处理行终止符。不过现在开始感觉很老套了…… 这是一个有趣的想法。我喜欢你关于接近而不是完美的洞察力。但是,底层的 FileReader 扩展了 InputStreamReader,它有自己的缓冲。不精确可以为字符提供字节中间的位置。 (此外,如果空行存在且重要,则不精确可能会很麻烦。它们不在我当前的用例中。) @Jeff FileReader 没有 getChannel。此外,FileInputStream 不能转换为 BufferedReader 并且使用 FileInputStream.getChannel().position() 根本不会推进文件指针(这意味着每次调用都会获得相同的位置值)【参考方案7】:

我建议java.io.LineNumberReader。您可以设置和获取行号,从而在某个行索引处继续。

由于它是BufferedReader,它也能够处理 UTF-8。

【讨论】:

我正在寻找一种快速返回位置的方法,因为我正在处理大文件。设置文件位置是一个常数时间的操作。跳线的成本是线性的。

以上是关于读取字符行并获取文件位置的主要内容,如果未能解决你的问题,请参考以下文章

如何获取文件在`BufReader`中的当前位置?[重复]

读取文件并获取字符串数组

获取文件的最后n行,类似于tail

java 读取读取指定字符的前一字符

JAVA获取文件本身所在的磁盘位置

用java如何读取一个文件的指定字节位置的数据?