2012-06-13 9 views
6

Disculpe el n00bness de esta pregunta, pero tengo una aplicación web en la que deseo enviar un archivo potencialmente grande al servidor y hacer que analice el formato. Estoy usando el framework Play20 y soy nuevo en Scala.Analizando un archivo con BodyParser en Scala Play20 con nuevas líneas

Por ejemplo, si tengo un csv, me gustaría dividir cada fila por "," y finalmente crear un List[List[String]] con cada campo.

Actualmente, estoy pensando que la mejor manera de hacerlo es con un BodyParser (pero podría estar equivocado). Mi código es algo como:

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

Mi primera pregunta es, ¿cómo manejar una situación como la de abajo, donde una parte se ha dividido en el medio de una línea:

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

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

mi La segunda pregunta es, ¿escribir mi propio BodyParser es la forma correcta de hacerlo? ¿Hay mejores formas de analizar este archivo? Mi principal preocupación es que quiero permitir que los archivos sean muy grandes para poder descargar un búfer en algún momento y no guardar todo el archivo en la memoria.

Respuesta

10

Si su csv no contiene saltos de líneas nuevas, entonces es bastante fácil hacer un análisis progresivo sin poner todo el archivo en la memoria. La biblioteca iteratee viene con un Método de búsqueda dentro play.api.libs.iteratee.Parsing:

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

que particionar el flujo en Matched[Array[Byte]] y Unmatched[Array[Byte]]

A continuación, puede combinar un primer iteratee que tiene una cabecera y otro que va a doblar en la umatched resultados. Esto debería verse como el siguiente código:

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

Por supuesto que puede mejorar el análisis sintáctico. Si lo haces, te agradecería si lo compartes con la comunidad.

Así que su controlador Play2 sería algo así como:

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

Este código parece prometedor, pero me va a tomar un poco de entender ... todos los operadores Scala tiene le da una gran curva de aprendizaje. –

+0

Absolutamente no, puede volver a escribir el código anterior reemplazando><> con componer, & >> por transformación, | >>> ejecutando. Estos operadores no son de scala sino que son métodos de los objetos correspondientes. – Sadache

+0

Ah sí, leí nuevamente los documentos en Enumeratees nuevamente y esto tiene sentido. ¡Gracias! –

1

Si no te importa la celebración de dos veces el tamaño de List[List[String]] en la memoria a continuación, se puede utilizar un analizador de cuerpo como 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") 
} 

Tenga en cuenta que si desea reducir el consumo de memoria, le recomiendo usar o Array[Array[String]]Vector[Vector[String]] dependiendo de si desea tratar con datos mutables o inmutables.

Si está tratando con una cantidad realmente grande de datos (o la pérdida de solicitudes de datos de tamaño medio) y su procesamiento puede hacerse de forma incremental, entonces puede ver cómo se procesa su propio analizador corporal. Ese analizador corporal no generaría un List[List[String]], sino que analizaría las líneas a medida que aparecen y doblaría cada línea en el resultado incremental. Pero esto es bastante más complejo de hacer, en particular, si su CSV usa comillas dobles para admitir campos con comas, líneas nuevas o comillas dobles.

Cuestiones relacionadas