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

Извините за бестактность этого вопроса, но у меня есть веб-приложение, в котором я хочу отправить потенциально большой файл на сервер и заставить его проанализировать формат. Я использую платформу 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? Есть ли лучшие способы разбора этого файла? Меня больше всего беспокоит то, что я хочу, чтобы файлы были очень большими, чтобы в какой-то момент я мог очистить буфер и не хранить весь файл в памяти.


person Jeff Wu    schedule 13.06.2012    source источник


Ответы (2)


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

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

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

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

// 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
}
person Community    schedule 15.06.2012
comment
Этот код выглядит многообещающе, но мне потребуется немного времени, чтобы понять... все операторы, которые есть в Scala, дают ему большую кривую обучения. - person Jeff Wu; 16.06.2012
comment
Абсолютно нет, вы можете переписать предыдущий код, заменив ›‹› на compose, &›› на transform, |››› на run. Эти операторы не из scala, а являются методами соответствующих объектов. - person Sadache; 16.06.2012
comment
Ах да, я снова прочитал документацию по Enumeratees, и это имеет смысл. Спасибо! - person Jeff Wu; 17.06.2012
comment
Есть ли способ передать перечислитель строк в действие? Чтобы можно было что-то делать с элементами, а потом выводить чанки с помощью, например, Result.Ok.chunked(). Насколько я вижу здесь, анализатор тела использует весь файл в структуру List[List[String]] в памяти. - person valgog; 02.01.2016

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

person huynhjl    schedule 14.06.2012