Изменение большого файла в Scala

Я пытаюсь изменить большой файл PostScript в Scala (некоторые из них имеют размер до 1 ГБ). Файл представляет собой группу пакетов, каждый из которых содержит код, представляющий номер пакета, количество страниц и т. д.

Мне необходимо:

  1. Найдите в файле пакетные коды (которые всегда начинаются с одной и той же строки в файле).
  2. Подсчитайте количество страниц до следующего батч-кода
  3. Измените пакетный код, указав количество страниц в каждом пакете.
  4. Сохраните новый файл в другом месте.

В моем текущем решении используются два итератора (iterA и iterB), созданные из Source.fromFile("file.ps").getLines. Первый итератор (iterA) проходит в цикле while к началу пакетного кода (при этом каждый раз также вызывается iterB.next). iterB затем продолжает поиск до следующего пакетного кода (или конца файла), подсчитывая количество пройденных страниц. Затем он обновляет пакетный код в позиции iterA, и процесс повторяется.

Это кажется очень не похожим на Scala, и я до сих пор не придумал хороший способ сохранить эти изменения в новый файл.

Каков хороший подход к этой проблеме? Должен ли я полностью отказаться от итераторов? Я бы предпочел сделать это без необходимости сразу вводить или выводить весь ввод или вывод в память.

Спасибо!


person Andrew Conner    schedule 16.02.2012    source источник


Ответы (3)


Вероятно, вы могли бы реализовать это с помощью класса Stream Scala. Я предполагаю, что вы не возражаете держать в памяти одну «партию» за раз.

import scala.annotation.tailrec
import scala.io._

def isBatchLine(line:String):Boolean = ...

def batchLine(size: Int):String = ...

val it = Source.fromFile("in.ps").getLines
// cannot use it.toStream here because of SI-4835
def inLines = Stream.continually(i).takeWhile(_.hasNext).map(_.next)

// Note: using `def` instead of `val` here means we don't hold
// the entire stream in memory
def batchedLinesFrom(stream: Stream[String]):Stream[String] = {
  val (batch, remainder) = stream span { !isBatchLine(_) }
  if (batch.isEmpty && remainder.isEmpty) { 
    Stream.empty
  } else {
    batchLine(batch.size) #:: batch #::: batchedLinesFrom(remainder.drop(1))
  }
}

def newLines = batchedLinesFrom(inLines dropWhile isBatchLine)

val ps = new java.io.PrintStream(new java.io.File("out.ps"))

newLines foreach ps.println

ps.close()
person stephenjudkins    schedule 17.02.2012
comment
Я предполагаю, что это решение будет хранить весь файл в памяти, потому что в 2.9.x этот шаблон Source.fromFile("in.ps").getLines.toStream удерживает заголовок потока. См. stackoverflow.com/a/8640680/257449 и issues.scala-lang.org/browse/SI-4835. - person huynhjl; 17.02.2012
comment
huynhjl, я обновил пример кода, чтобы исправить (раздражающую) ошибку, которую вы нашли. Спасибо. - person stephenjudkins; 18.02.2012

Если вы не стремитесь к функциональному просветлению Scala, я бы порекомендовал более императивный стиль, используя java.util.Scanner#findWithinHorizon. Мой пример довольно наивен, дважды повторяя ввод.

val scanner = new Scanner(inFile)

val writer = new BufferedWriter(...)

def loop() = {
  // you might want to limit the horizon to prevent OutOfMemoryError
  Option(scanner.findWithinHorizon(".*YOUR-BATCH-MARKER", 0)) match {
    case Some(batch) =>
      val pageCount = countPages(batch)
      writePageCount(writer, pageCount)
      writer.write(batch)        
      loop()

    case None =>
  }
}

loop()
scanner.close()
writer.close()
person MxFr    schedule 17.02.2012

Может быть, вы сможете эффективно использовать span и duplicate. Предполагая, что итератор расположен в начале пакета, вы берете диапазон перед следующим пакетом, дублируете его, чтобы можно было подсчитать страницы, пишете измененную строку пакета, а затем записываете страницы, используя дублированный итератор. Затем обработайте следующую партию рекурсивно...

def batch(i: Iterator[String]) {
  if (i.hasNext) {
    assert(i.next() == "batch")
    val (current, next) = i.span(_ != "batch")
    val (forCounting, forWriting) = current.duplicate
    val count = forCounting.filter(_ == "p").size
    println("batch " + count)
    forWriting.foreach(println)
    batch(next)
  }
}

Предполагая следующий ввод:

val src = Source.fromString("head\nbatch\np\np\nbatch\np\nbatch\np\np\np\n")

Вы позиционируете итератор в начале пакета, а затем обрабатываете пакеты:

val (head, next) = src.getLines.span(_ != "batch")
head.foreach(println)
batch(next)

Это печатает:

head
batch 2
p
p
batch 1
p
batch 3
p
p
p
person huynhjl    schedule 17.02.2012