2012-06-13 10 views
6

Przepraszam za n00bness tego pytania, ale mam aplikację internetową, w której chcę wysłać potencjalnie duży plik na serwer i zlecić parsowanie formatu. Używam struktury Play20 i jestem nowy w Scala.Parsowanie pliku z BodyParser w Scala Play20 z nowymi liniami

Na przykład, jeśli mam plik CSV, chciałbym podzielić każdy wiersz przez "," i ostatecznie utworzyć List[List[String]] z każdym polem.

Obecnie myślę, że najlepszym sposobem na zrobienie tego jest BodyParser (ale mogę się mylić). Mój kod wygląda mniej więcej tak:

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

Moje pierwsze pytanie brzmi: w jaki sposób poradzić sobie z sytuacją, jak poniżej, gdzie bryła została podzielona na środku linii:

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

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

My Drugie pytanie brzmi: czy napisanie własnego BodyParser jest właściwym sposobem postępowania w tej sprawie? Czy istnieją lepsze sposoby analizowania tego pliku? Moją główną obawą jest to, że chcę, aby pliki były bardzo duże, więc mogę w pewnym momencie opróżnić bufor i nie przechowywać całego pliku w pamięci.

Odpowiedz

10

Jeśli twój plik CSV nie zawiera unikniętych znaków nowej linii, łatwo jest przeprowadzić stopniowe analizowanie bez umieszczania całego pliku w pamięci. Biblioteka iteratee wyposażony w poszukiwaniu metody wewnątrz play.api.libs.iteratee.Parsing:

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

która partycja strumień do Matched[Array[Byte]] i Unmatched[Array[Byte]]

Następnie można połączyć pierwszy iteratee które ma nagłówek i innym, że spasuje do Bezkompromisowe wyniki. Powinno to wyglądać następująco:

// 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)) 

Oczywiście można poprawić przetwarzanie. Jeśli to zrobisz, będę wdzięczny, jeśli podzielisz się nią ze społecznością.

więc kontroler PLAY2 byłoby coś takiego:

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 
} 
+0

Ten kod wygląda obiecująco, ale zajmie mi trochę zrozumieć ... wszystkich operatorów Scala daje duży krzywej uczenia się. –

+0

Absolutnie nie, możesz przepisać poprzedni kod zastępując><> przez compose, & >> przez transformację, | >>> przez uruchomienie. Operatory te nie pochodzą z scala, ale są metodami odpowiednich obiektów. – Sadache

+0

Ach tak, ponownie przeczytałem dokumenty na temat Enumeratees i ma to sens. Dzięki! –

1

Jeśli nie przeszkadza gospodarstwa dwukrotnej wielkości List[List[String]] w pamięci a następnie można użyć parsera ciało jak 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") 
} 

Pamiętaj, że jeśli chcesz, aby zmniejszyć zużycie pamięci, polecam użyciu Array[Array[String]] lub Vector[Vector[String]] w zależności od tego, czy chcesz uporać się z danymi zmiennymi czy niezmiennymi.

Jeśli masz do czynienia z naprawdę dużą ilością danych (lub utraconych z żądań danych średniej wielkości), a twoje przetwarzanie może być wykonywane przyrostowo, możesz spojrzeć na toczenia swojego własnego parsera ciała. Ten analizator składni nie generowałby wartości List[List[String]], ale zamiast tego analizowałby linie w momencie ich pojawiania się i składała każdą linię w wyniku przyrostowym. Jest to jednak nieco bardziej skomplikowane, szczególnie jeśli Twój CSV używa podwójnego cudzysłowu do obsługi pól z przecinkami, znakami nowej linii lub podwójnymi cytatami.