在scala

时间:2015-10-25 03:34:42

标签: scala

我应该如何在scala中读取和编写大量文本文件,避免因巨大的内存需求而导致崩溃?

示例案例

输入文件有300万个字符串,以下代码显然崩溃

// The first line of the file contains input data type and total number of lines

  val src = Source.fromFile("in.txt").getLines
  val header = src.next.split(" ").toVector
  val lines = (if (header(0) == "i") src.map(_.toInt) else src).toArray

  process(lines) // no lines are removed during processing

  val writer = new PrintWriter("out.txt", "UTF-8")
  try writer.print(lines.mkString("\n"))
  finally writer.close

我应该如何

  • 将数据拆分成块(以及如何确定单个块大小?)
  • 处理块
  • 合并块
  • 写一个输出文件

1 个答案:

答案 0 :(得分:1)

直接回答您的问题

  • 块大小 - 块大小是速度/空间的函数。夹头越大,您可以更快地(通常)处理数据,但内存使用量将与卡盘尺寸成正比。
  • 处理卡盘 - 由于您无法将所有数据保存在内存中,因此您需要过滤掉您不需要的数据,并且只保留您需要的结果。这是相当抽象的,因为它非常直接取决于您的用例。例如,如果你想计算角色的数量' a'发生在一个文本文件中,你会在每个块中查找字母' a',增加一个整数,然后丢弃该块并继续。
  • 合并块 - 通常,您只需将先前计算的结果应用于下一个计算(即像折叠)。所以你真的不需要合并块。你是在谈论以分布式的方式同时做这件事(听起来你可能会这样)?
  • 编写输出文件 - 同样,您将希望以流/增量方式执行此操作。如果需要生成大输出语料库,则在获得部分结果后立即刷新输出。这样做的缺点是,如果发生错误,您需要清理部分构建的输出文件。

一个简单的解决方案

为此,您需要对数据进行流处理。这可以通过多种方式完成。使用专门处理流处理的库是非常的好方法。 scalaz-stream就是这种图书馆的一个很好的例子。

他们的github页面提供了有关如何完全按照您的要求执行操作的示例代码。读入一个大的(可能是无限的)文件,对数据执行一些转换,所有转换都是持续的内存使用。

这是他们的README.md

示例的副本
import scalaz.stream._
import scalaz.concurrent.Task

val converter: Task[Unit] =
  io.linesR("testdata/fahrenheit.txt")
    .filter(s => !s.trim.isEmpty && !s.startsWith("//"))
    .map(line => fahrenheitToCelsius(line.toDouble).toString)
    .intersperse("\n")
    .pipe(text.utf8Encode)
    .to(io.fileChunkW("testdata/celsius.txt"))
    .run

// at the end of the universe...
val u: Unit = converter.run

您当然可以从标准库构建此类流处理。在这种情况下,你可能想要根据行或字节来处理它(前者更容易,后者更安​​全,因为不能保证即使在非常大的文件中也会有任何换行)。我个人强烈建议scalaz-stream

纯Scala解决方案

纯scala解决方案是使用scala.io.Source之类的东西。例如,这将程序将计算角色的数量' a'发生在文件中,它将在常量内存中执行。

注意,您在示例中使用的是Source,但是您正在调用将其转换为内存数据结构(特别是Array 流式构造)。

import scala.io.Source

object Streaming extends App {

  args.headOption.foreach{(file: String) =>
    println(
      s"Number of 'a' is: ${Source.fromFile(file).count(_ == 'a')}"
    )
  }
}

Java / Scala解决方案

现在Source非常容易使用,但它实际上只是为字符数据而设计的。如果您需要更强大的东西,也许处理任意二进制数据,那么您将需要使用Java标准库。

请注意,您可能会在标准库中使用其他原语来执行此操作,这些只是我选择的原语。他们使用我读过的java.nio包更加高效(我自己没有做任何基准测试)。

import java.nio.ByteBuffer
import java.nio.file.FileSystems
import java.nio.channels.ReadableByteChannel
import java.nio.file.Files
import java.nio.file.Path

object Streaming extends App {


  @scala.annotation.tailrec
  def countStuff(
    buffer: ByteBuffer,
    byteChannel: ReadableByteChannel,
    count: BigInt
  ): BigInt = {
    val newCount = byteChannel.read(buffer)
    if (newCount == -1) {
      println("Done reading")
      count
    } else {
      println(s"Read ${newCount + count} bytes!")
      buffer.clear()
      countStuff(buffer, byteChannel, count + newCount)
    }
  }

  args.headOption.foreach{(file: String) =>
    val byteChannel =
      Files.newByteChannel(FileSystems.getDefault().getPath(file))
    countStuff(ByteBuffer.allocateDirect(1024), byteChannel, 0)
    byteChannel.close()
  }
}

计算它已读取的字节数,打印出当前计数,并在常量内存中执行此操作。显然这是一个非常无聊的用途,但你可以希望看到你如何根据自己的需要改变它。

为什么选择直播

你问为什么你不能在内存中对所有这些进行操作。您当然可以这样做,具体取决于您的可用内存(可通过JVM选项调整)和输入的大小。这里的答案假定您需要对任意大的数据进行操作。如果您不需要这样做,将整个语料库处理到内存中通常是更容易的解决方案。

同样,Scala / Java标准库示例仅在此处,因为您说不能使用类似scalaz-stream的内容,这是我在这种情况下通常会执行的操作。