6

请原谅这个问题,但我有一个 Web 应用程序,我想将一个可能很大的文件发送到服务器并让它解析格式。我正在使用 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 是解决这个问题的正确方法吗?有没有更好的方法来解析这个文件?我主要关心的是我想让文件非常大,这样我就可以在某个时候刷新缓冲区,而不是将整个文件保存在内存中。

4

2 回答 2

10

如果您的 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
}
于 2012-06-15T23:16:18.920 回答
1

如果您不介意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]]or ,Vector[Vector[String]]具体取决于您是要处理可变数据还是不可变数据。

如果您正在处理真正大量的数据(或丢失中等大小数据的请求)并且您的处理可以增量完成,那么您可以考虑滚动您自己的正文解析器。该正文解析器不会生成 a List[List[String]],而是在行到来时对其进行解析,并将每一行折叠成增量结果。但这要复杂得多,特别是如果您的 CSV 使用双引号来支持带有逗号、换行符或双引号的字段。

于 2012-06-14T05:36:50.450 回答