Разбор на файл с 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