Изменение большого файла в Scala - PullRequest
6 голосов
/ 16 февраля 2012

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

Мне нужно:

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

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

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

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

Спасибо!

Ответы [ 3 ]

2 голосов
/ 17 февраля 2012

Возможно, вы могли бы реализовать это с помощью класса 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()
1 голос
/ 17 февраля 2012

Если вы не стремитесь к функциональному просветлению скалы, я бы порекомендовал более императивный стиль, используя 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()
0 голосов
/ 17 февраля 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
...