在Scala中高效地将一系列Long存储到文件中

3
所以我有一个关联,将一对IntVector[Long]相关联,该向量最多可以达到10000个大小,并且我有几十万到一百万个这样的数据。 我想将其存储在单个文件中以供以后在Scala中处理。
显然,以纯文本格式存储这些数据将占据大量空间,因此我一直在尝试通过编写Byte流来解决它。 但是,我不太确定这是否有效,因为对我来说,LongbyteValue()返回的是仍为4字节长的Byte表示形式,因此我不会节省任何空间? 我没有使用二进制格式的太多经验。
似乎Scala标准库有一个BytePickle可能是我正在寻找的东西,但已被弃用?

我认为你可能需要定义一下你所说的“高效”。一百万个原始长整型数据是8兆字节(M的某个值)。这对你来说太大了吗,所以你想要进行一些压缩? - Duncan McGregor
长整型的字节表示仅为一个字节。但是,如果您将其用于范围在-128到127之外的值,则会丢失信息。 - Duncan McGregor
@DuncanMcGregor:一个长整型的字节表示只有一个字节?什么?一个字节由一个字节表示,一个短整型是2个字节,一个整型是4个字节,而一个长整型是8个字节长,不是吗?如果你将它存储为文本,则取决于值的大小,但是一个均匀分布的长整型的平均长度要比8个字节长得多。 - user unknown
更准确地说,Long.byteValue()返回一个字节,它只有一个字节长,而不是问题所述的4个字节。 - Duncan McGregor
3个回答

7
一个任意的Long大约有19.5个ASCII数字长,但只有8个字节长,因此如果您以二进制形式编写它,则可以节省约2倍的空间。现在,可能大多数值实际上并没有占用所有8个字节,因此您可以自己定义一些压缩方案。
无论如何,最好使用java.nio.ByteBuffer和相关工具编写块数据。二进制数据最有效地以块读取,并且您可能希望文件可以随机访问,在这种情况下,您的数据应该看起来像这样:
<some unique binary header that lets you check the file type>
<int saying how many records you have>
<offset of the first record>
<offset of the second record>
...
<offset of the last record>
<int><int><length of vector><long><long>...<long>
<int><int><length of vector><long><long>...<long>
...
<int><int><length of vector><long><long>...<long>

这是一种特别方便的格式,使用ByteBuffer进行读写,因为你事先知道所有内容的大小。 所以你可以

val fos = new FileOutputStream(myFileName)
val fc = fos.getChannel // java.nio.channel.FileChannel
val header = ByteBuffer.allocate(28)
header.put("This is my cool header!!".getBytes)
header.putInt(data.length)
fc.write(header)
val offsets = ByteBuffer.allocate(8*data.length)
data.foldLeft(28L+8*data.length){ (n,d) =>
  offsets.putLong(n)
  n = n + 12 + d.vector.length*8
}
fc.write(offsets)
...

在返回的路上

val fis = new FileInputStream(myFileName)
val fc = fis.getChannel
val header = ByteBuffer.allocate(28)
fc.read(header)
val hbytes = new Array[Byte](24)
header.get(hbytes)
if (new String(hbytes) != "This is my cool header!!") ???
val nrec = header.getInt
val offsets = ByteBuffer.allocate(8*nrec)
fc.read(offsets)
val offsetArray = offsets.getLongs(nrec)  // See below!
...

ByteBuffer上有一些方便的方法缺失,但是你可以通过隐式转换(这里是针对Scala 2.10的;对于2.9,请将其变成普通类,删除extends AnyVal,并提供一个从ByteBufferRichByteBuffer的隐式转换)来添加它们:

implicit class RichByteBuffer(val b: java.nio.ByteBuffer) extends AnyVal {
  def getBytes(n: Int) = { val a = new Array[Byte](n); b.get(a); a }
  def getShorts(n: Int) = { val a = new Array[Short](n); var i=0; while (i<n) { a(i)=b.getShort(); i+=1 } ; a }
  def getInts(n: Int) = { val a = new Array[Int](n); var i=0; while (i<n) { a(i)=b.getInt(); i+=1 } ; a }
  def getLongs(n: Int) = { val a = new Array[Long](n); var i=0; while (i<n) { a(i)=b.getLong(); i+=1 } ; a }
  def getFloats(n: Int) = { val a = new Array[Float](n); var i=0; while (i<n) { a(i)=b.getFloat(); i+=1 } ; a }
  def getDoubles(n: Int) = { val a = new Array[Double](n); var i=0; while (i<n) { a(i)=b.getDouble(); i+=1 } ; a }
}

无论如何,采用这种方法的原因是您将获得良好的性能,当您有数十GB的数据(根据您提供的长度为一万的数百万个向量),这也是需要考虑的。
如果您的问题实际上要小得多,那么不要太担心-将其打包成XML或使用JSON或一些自定义文本解决方案(或使用DataOutputStream和DataInputStream,它们的性能不佳,并且不会给您提供随机访问)。
如果您的问题实际上更大,则可以定义两个longs列表;首先是适合于Int的那些,然后是实际上需要完整Long的那些(具有索引,以便知道它们在哪里)。数据压缩是一个非常特定于情况的任务-假设您不只想使用java.util.zip-因此,如果没有更多关于数据外观的知识,很难知道除了按照我上面描述的方式将其存储为弱层次结构二进制文件之外还有什么建议。

文件开头的索引意味着追加速度慢。我建议为整数使用单独的文件。对于 ByteBuffer,我建议使用 ByteBuffer.allocate(8*buffer.length) - 重复使用比数据小的缓冲区。在 while 循环中实现。 - idonnie
@idonnie - 在开始时这样索引确实意味着降低了追加速度,同意;在这种情况下,您将有一个额外的偏移量,它将是0L,表示我们已经完成了块的块,或者是下一个头/记录块的偏移量。(受TIFF格式的微弱启发。) - Rex Kerr
我主要是指与通道和缓冲区一起工作的代码,例如用于读取的以下代码(生产代码中执行大量数据传输的部分):do { add -= fromCh.read(bbuf); } while (bbuf.hasRemaining() && (add > 0));。无论如何,感谢您提供TIFF的想法,非常值得记忆! - idonnie

5
请参阅Java的 DataOutputStream。它允许将原始类型和字符串轻松高效地写入字节流。特别是,您需要类似以下内容的东西:
val stream = new DataOutputStream(new FileOutputStream("your_file.bin"))

您可以使用相应的DataInputStream方法从该文件再次读取到变量中。

2
我使用了scala-ioscala-arm来写一个Long类型的二进制流。这些库本身应该是Scala的一种方式来做事情,但它们不在Scala的master分支中 - 也许有人知道为什么?我偶尔会使用它们。
1)克隆scala-iogit clone https://github.com/scala-incubator/scala-io.git 进入scala-io/package并在Build.scala中更改val scalaVersion为您自己的版本 sbt package 2)克隆scala-armgit clone https://github.com/jsuereth/scala-arm.git 进入scala-arm/package并在build.scala中更改scalaVersion :=为您自己的版本 sbt package 3)将文件复制到某个地方: scala-io/core/target/scala-xxx/scala-io-core_xxx-0.5.0-SNAPSHOT.jar scala-io/file/target/scala-xxx/scala-io-file_xxx-0.5.0-SNAPSHOT.jar scala-arm/target/scala-xxx/scala-arm_xxx-1.3-SNAPSHOT.jar 4)启动REPL:scala -classpath "/opt/scala-io/scala-io-core_2.10-0.5.0-SNAPSHOT.jar: /opt/scala-io/scala-io-file_2.10-0.5.0-SNAPSHOT.jar: /opt/scala-arm/scala-arm_2.10-1.3-SNAPSHOT.jar" 5):paste实际代码:
import scalax.io._

// create data stream
val EOData = Vector(0xffffffffffffffffL)
val data = List(
  (0, Vector(0L,1L,2L,3L))
  ,(1, Vector(4L,5L))
  ,(2, Vector(6L,7L,8L))
  ,(3, Vector(9L))  
)
var it = Iterator[Long]()
for (rec <- data) {
  it = it ++ Vector(rec._1).iterator.map(_.toLong)
  it = it ++ rec._2.iterator
  it = it ++ EOData.iterator
}

// write data at once
val out: Output = Resource.fromFile("/tmp/data")
out.write(it)(OutputConverter.TraversableLongConverter)  

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接