【问题标题】: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 类本身甚至必须支持遗留错误这一事实表明,这里没有适用于所有可能的字符编码的简单解决方案。阅读文本文件是一个令人惊讶的难题。

标签: java nio java-io


【解决方案1】:

最初,我发现 Andy Thomas (https://stackoverflow.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

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

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

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

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

      [...]

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

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

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

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

      例如https://stackoverflow.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。

      【讨论】:

      • 我正在寻找一种快速返回位置的方法,因为我正在处理大文件。设置文件位置是一个常数时间的操作。跳线的成本是线性的。
      猜你喜欢
      • 2011-03-23
      • 2015-08-28
      • 1970-01-01
      • 2011-08-10
      • 1970-01-01
      • 1970-01-01
      • 2011-07-20
      • 1970-01-01
      • 2020-12-19
      相关资源
      最近更新 更多