读取字符行并获取文件位置
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。
【讨论】:
我正在寻找一种快速返回位置的方法,因为我正在处理大文件。设置文件位置是一个常数时间的操作。跳线的成本是线性的。以上是关于读取字符行并获取文件位置的主要内容,如果未能解决你的问题,请参考以下文章