Разбор файла с 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 использует двойную кавычку для поддержки полей запятыми, новыми символами или двойными кавычками.