【问题标题】:Fastest way to read a line in file读取文件中一行的最快方法
【发布时间】:2017-02-21 19:15:44
【问题描述】:

我正在使用RandomAccessFile 从一个大文件中读取一些信息。 RandomAccessFile 有一个方法 seek 将光标指向我想要读取整行的文件的特定部分。要阅读这一行,我使用readLine() 方法。

我之前阅读了整个文件,然后创建了一个索引,允许我使用seek 方法访问任何行的开头。该索引工作正常。 我根据这个答案创建了这个索引:https://stackoverflow.com/a/42077860/763368

由于我必须在此文件中进行大量访问,因此性能是一个需要注意的重要问题,因此我正在寻找其他选项来读取文件到特定行并获取整行。

我读到FileChannelMappedByteBuffer 是快速读取文件的好选择,但我没有看到任何符合我要求的解决方案。

P.S.:线条有不同的长度,我不知道这个长度。

有人有好的解决方案吗?

编辑

我要读取的文件格式如下:key\tvalue

索引是一个哈希图,该文件的所有键都是键,值是字节位置(Long)。

假设我想用 "foo" 键去一行,那么我必须寻找到值的位置,像这样:

raf.seek(index.get("foo"))

如果我使用raf.readLine(),则返回将是带有 "foo" 键的整行。

但我不想在这项工作中使用RandomAccessFile,因为它太慢了。

这就是我现在在 Scala 中所做的方式:

val raf = new RandomAccessFile(file,"r")  
raf.seek(position.get(key))
println(raf.readLine)
raf.close

【问题讨论】:

  • 您在访问不同的文件吗?如果不是,为什么要关闭文件访问?如果您保持文件访问打开,则不必等待操作系统授予您读取权限。
  • @Tschallacka 我只是在所有阅读结束时结束,这只是一个例子。但我这里的问题是读取文件的方式。
  • 您能否提供索引读取的代码以及如何将其转换为查找位置。因为您已经走上了一条好的道路,您的索引搜索可能会从一些优化中受益,但如果没有完整的代码和示例数据,就很难提供帮助。
  • @Tschallacka 我编辑了我的问题,请看一下。

标签: java nio randomaccessfile filechannel


【解决方案1】:

如果您已经必须通读文件一次才能找到键的索引,那么绝对最快的解决方案是读取这些行并将它们保存在内存中。如果由于某种原因(例如内存限制)不起作用,则使用缓冲区确实是一个不错的选择。这是代码的大纲:

FileChannel channel = new RandomAccessFile("/some/file", "r").getChannel();

long pageSize = ...; // e.g. "3 GB or file size": max(channel.size(), THREE_GB); 
long position = 0;
ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, position, pageSize);

ByteBuffer slice;
int maxLineLength = 30;
byte[] lineBuffer = new byte[maxLineLength];

// Read line at indices 20 - 25
buffer.position(20);
slice = buffer.slice();
slice.get(lineBuffer, 0, 6);
System.out.println("Starting at 20:" + new String(lineBuffer, Charset.forName("UTF8")));

// Read line at indices 0 - 10
buffer.position(0);
slice = buffer.slice();
slice.get(lineBuffer, 0, 11);
System.out.println("Starting at 0:" + new String(lineBuffer, Charset.forName("UTF8")));

此代码也可用于非常大的文件。只需调用channel.map 即可找到您的密钥所在的“页面”:position = keyIndex / pageSize * pageSize,然后从该索引调用buffer.positionkeyIndex - position

如果您真的无法将访问一个“页面”的权限分组在一起,那么您不需要slice。性能不会那么好,但这可以让您进一步简化代码:

byte[] lineBuffer = new byte[maxLineLength];
// ...
ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, keyIndex, lineLength);
buffer .get(lineBuffer, 0, lineLength);
System.out.println(new String(lineBuffer, Charset.forName("UTF8")));

注意ByteBuffer不是在JVM堆上创建的,实际上是操作系统级别的内存映射文件。 (从 Java 8 开始,您可以通过查看源代码并在实现中搜索 sun.nio.ch.DirectBuffer 来验证这一点。

行大小: 获取行大小的最佳方法是在扫描文件时存储它,即使用Map[String, (Long, Int)] 而不是您现在使用的index。如果这对您不起作用,您应该运行一些测试以找出更快的方法:

  • 只需存储最大行大小,然后在此最大长度的字符串中搜索换行符。在这种情况下,请注意在单元测试中访问文件末尾。
  • 使用ByteBuffer.get 向前扫描,直到找到\n。如果您有真正的 Unicode 文件,这可能不是一个选项,因为换行符 (0x0A) 的 Ascii 代码可能出现在其他地方,例如在字符代码为 0xAC0A 的 UTF-16 编码的韩语音节中。

这将是第二种方法的 Scala 代码:

// this happens once
val maxLineLength: Long = 2000 // find this in your initial sequential scan
val lineBuffer = new Array[Byte](maxLineLength.asInstanceOf[Int])

// this is how you read a key
val bufferLength = maxLineLength min (channel.size() - index("key"))
val buffer = channel.map(FileChannel.MapMode.READ_ONLY, index("key"), bufferLength)
var lineLength = 0 // or minLineLength
while (buffer.get(lineLength) != '\n') {
  lineLength += 1
}
buffer.get(lineBuffer, 0, lineLength - 1)
println(new String(lineBuffer, Charset.forName("UTF8")))

【讨论】:

  • 我有一个索引,所以我可以访问一行的开头。我访问此索引,然后搜索到那里。使用与 RandomAccessFile 不同的其他选项,我也想搜索到这个位置,索引也将被使用。
  • 我之前已经阅读了所有文件,然后创建了一个索引。我把这个索引放在内存中,这样我就可以访问它并使用搜索方法转到一行的开头。对于与 RandomAccessFile 不同的其他选项,我也想寻找到这个位置,索引也将被使用
  • 不,我不能把那个文件放到内存中,它超过了 100GB。我的解决方案有效,但速度很慢,这是我的问题。
  • 我没有行大小,我想转到行首(通过搜索),然后得到以 \n 结尾的整行。
  • @Marcelo Machado 我希望我编辑的答案可以更清楚地说明如何将这种方法用于大文件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-08-06
  • 1970-01-01
  • 2012-08-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多