读取文件中一行的最快方法

Fastest way to read a line in file

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

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

因为我必须在这个文件中做很多访问,性能是一个需要注意的重要问题,然后我正在寻找其他选项来读取文件到特定行并获取整行。

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

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

有没有人有好的解决办法?

编辑:

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

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

假设我想转到带有键 "foo" 的行,那么我必须查找值位置,如下所示:

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

如果我使用 raf.readLine(),return 将是带有键 "foo".

的整行

但我不想使用 RandomAccessFile 来完成这项工作,因为它太慢了。

这就是我现在在 Scala 中的做法:

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

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

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 即可找到您的密钥所在的 "page":position = keyIndex / pageSize * pageSize,然后从该索引调用 buffer.positionkeyIndex - position

如果您真的没有办法将对一个 "page" 的访问分组在一起,那么您就不需要 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堆上创建的,实际上是OS级别的内存映射文件。 (从 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")))