Разбор файла с BodyParser в Scala Play20 с новыми строками

Извините n00bness этого вопроса, но у меня есть веб-приложение, где я хочу отправить потенциально большой файл на сервер и проанализировать его. Я использую платформу Play20, и я новичок в Scala.

Например, если у меня есть csv, я бы хотел разделить каждую строку на "," и в конечном итоге создать List[List[String]] с каждым полем.

В настоящее время я думаю, что лучший способ сделать это - с BodyParser (но я мог ошибаться). Мой код выглядит примерно так:

Iteratee.fold[String, List[List[String]]]() {
  (result, chunk) =>
    result = chunk.splitByNewLine.splitByDelimiter // Psuedocode
}

Мой первый вопрос: как я могу справиться с ситуацией, подобной той, которая находится ниже, где фрагмент разделен посередине строки:

Chunk 1:
1,2,3,4\n
5,6

Chunk 2:
7,8\n
9,10,11,12\n

Мой второй вопрос: писать собственный BodyParser правильный путь? Есть ли лучшие способы анализа этого файла? Моя основная проблема заключается в том, что я хочу, чтобы файлы были очень большими, поэтому я могу сбросить буфер в какой-то момент и не хранить весь файл в памяти.

Ответ 1

Если ваш csv не содержит экранированные символы новой строки, довольно просто сделать прогрессивный синтаксический анализ, не помещая весь файл в память. Библиотека iteratee поставляется с методом поиска внутри play.api.libs.iteratee.Parsing:

def search (needle: Array[Byte]): Enumeratee[Array[Byte], MatchInfo[Array[Byte]]]

который разбивает ваш поток на Matched[Array[Byte]] и Unmatched[Array[Byte]]

Затем вы можете комбинировать первую итерацию, которая принимает заголовок, а другую, которая будет складываться в результаты umatched. Это должно выглядеть следующим образом:

// break at each match and concat unmatches and drop the last received element (the match)
val concatLine: Iteratee[Parsing.MatchInfo[Array[Byte]],String] = 
  ( Enumeratee.breakE[Parsing.MatchInfo[Array[Byte]]](_.isMatch) ><>
    Enumeratee.collect{ case Parsing.Unmatched(bytes) => new String(bytes)} &>>
    Iteratee.consume() ).flatMap(r => Iteratee.head.map(_ => r))

// group chunks using the above iteratee and do simple csv parsing
val csvParser: Iteratee[Array[Byte], List[List[String]]] =
  Parsing.search("\n".getBytes) ><>
  Enumeratee.grouped( concatLine ) ><>
  Enumeratee.map(_.split(',').toList) &>>
  Iteratee.head.flatMap( header => Iteratee.getChunks.map(header.toList ++ _) )

// an example of a chunked simple csv file
val chunkedCsv: Enumerator[Array[Byte]] = Enumerator("""a,b,c
""","1,2,3","""
4,5,6
7,8,""","""9
""") &> Enumeratee.map(_.getBytes)

// get the result
val csvPromise: Promise[List[List[String]]] = chunkedCsv |>>> csvParser

// eventually returns List(List(a, b, c),List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))

Конечно, вы можете улучшить синтаксический анализ. Если вы это сделаете, я был бы признателен, если вы поделитесь им с сообществом.

Итак, ваш контроллер Play2 будет примерно таким:

val requestCsvBodyParser = BodyParser(rh => csvParser.map(Right(_)))

// progressively parse the big uploaded csv like file
def postCsv = Action(requestCsvBodyParser){ rq: Request[List[List[String]]] => 
  //do something with data
}

Ответ 2

Если вы не возражаете держать в два раза размер List[List[String]] в памяти, вы можете использовать синтаксический анализатор тела, например play.api.mvc.BodyParsers.parse.tolerantText:

def toCsv = Action(parse.tolerantText) { request =>
  val data = request.body
  val reader = new java.io.StringReader(data)
  // use a Java CSV parsing library like http://opencsv.sourceforge.net/
  // to transform the text into CSV data
  Ok("Done")
}

Обратите внимание, что если вы хотите уменьшить потребление памяти, я рекомендую использовать Array[Array[String]] или Vector[Vector[String]] в зависимости от того, хотите ли вы иметь дело с изменяемыми или неизменяемыми данными.

Если вы имеете дело с действительно большим объемом данных (или потерями запросов данных среднего размера), и ваша обработка может быть выполнена постепенно, вы можете посмотреть на сворачивание собственного анализатора тела. Этот анализатор тела не генерирует List[List[String]], а вместо этого анализирует строки по мере их поступления и складывает каждую строку в инкрементный результат. Но это довольно сложно сделать, особенно если ваш CSV использует двойную кавычку для поддержки полей запятыми, новыми символами или двойными кавычками.