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

时间:2015-06-03 18:11:26

标签: java nio java-io

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

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

问题

是否有一种简单的方法可以同时使用标准Java库?

如果没有,那么什么是合理的解决方法?

理想解决方案的属性

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

详细

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

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

  

为了有效地将字节转换为字符,可以从底层流中读取比满足当前读取操作所需的更多字节。

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

  

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

7 个答案:

答案 0 :(得分:8)

如果您从BufferedReader构建FileReader并保持代码可以访问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;
}

无论基础标记或缓冲如何,这都可以使字节数精确到您已处理的内容。你必须考虑你的理货中的行结尾,因为它们被剥离了。

答案 1 :(得分: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 );

答案 2 :(得分:3)

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

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

VTDGen.java和VTDGenHuge.java中提供了各种caracter编码的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

答案 3 :(得分:1)

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

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

答案 4 :(得分:1)

解决方案A

  1. 循环使用RandomAccessFile.readChar()RandomAccessFile.readByte()
  2. 检查您的EOL字符,然后处理该行。
  3. 其他任何问题都是你必须绝对确保你从未读过EOL角色。

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

      

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

    [...]

      

    此方法将一直阻塞,直到读取两个字节,检测到流的末尾,或者抛出异常。

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

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

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

    E.g。 https://stackoverflow.com/a/4305478/16549

答案 5 :(得分:1)

RandomAccessFile has a function: seek(long pos) Sets the file-pointer offset, measured from the beginning of this file, at which the next read or write occurs.

答案 6 :(得分:1)

最初,我发现安迪·托马斯(https://stackoverflow.com/a/30850145/556460)建议的方法最合适。

但遗憾的是,在文件行包含非拉丁字符的情况下,我无法成功将字节数组(取自RandomAccessFile.readLine)转换为正确的字符串。

所以我通过编写类似于RandomAccessFile.readLine本身的函数来重写该方法,该函数从行收集数据而不是字符串,而是直接收集字节数组,然后从字节数组构造所需的String。 所以下面的代码完全满足了我的需求(在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
}