Utiliser le combinateur d’parsingur Scala pour parsingr les fichiers CSV

J’essaie d’écrire un parsingur CSV en utilisant des combinateurs d’parsingurs Scala. La grammaire est basée sur la RFC4180 . Je suis venu avec le code suivant. Cela fonctionne presque, mais je ne peux pas le faire pour séparer correctement les différents enregistrements. Qu’est-ce que j’ai raté?

object CSV extends RegexParsers { def COMMA = "," def DQUOTE = "\"" def DQUOTE2 = "\"\"" ^^ { case _ => "\"" } def CR = "\r" def LF = "\n" def CRLF = "\r\n" def TXT = "[^\",\r\n]".r def file: Parser[List[List[Ssortingng]]] = ((record~((CRLF~>record)*)) r::rs } def record: Parser[List[Ssortingng]] = (field~((COMMA~>field)*)) ^^ { case f~fs => f::fs } def field: Parser[Ssortingng] = escaped|nonescaped def escaped: Parser[Ssortingng] = (DQUOTE~>((TXT|COMMA|CR|LF|DQUOTE2)*) ls.mkSsortingng("")} def nonescaped: Parser[Ssortingng] = (TXT*) ^^ { case ls => ls.mkSsortingng("") } def parse(s: Ssortingng) = parseAll(file, s) match { case Success(res, _) => res case _ => List[List[Ssortingng]]() } } println(CSV.parse(""" "foo", "bar", 123""" + "\r\n" + "hello, world, 456" + "\r\n" + """ spam, 789, egg""")) // Output: List(List(foo, bar, 123hello, world, 456spam, 789, egg)) // Expected: List(List(foo, bar, 123), List(hello, world, 456), List(spam, 789, egg)) 

Mise à jour: problème résolu

Le RegexParsers par défaut ignore les espaces blancs, y compris les espaces, les tabulations, les retours chariot et les sauts de ligne en utilisant l’expression régulière [\s]+ . Le problème de l’parsingur ci-dessus incapable de séparer les enregistrements est dû à cela. Nous devons désactiver le mode skipWhitespace. Remplacer la définition de whiteSpace par [ \t]} ne résout pas le problème car elle ignorera tous les espaces dans les champs (par conséquent, “foo bar” dans le fichier CSV devient “foobar”), ce qui est indésirable. La source mise à jour de l’parsingur est donc

 import scala.util.parsing.combinator._ // A CSV parser based on RFC4180 // http://tools.ietf.org/html/rfc4180 object CSV extends RegexParsers { override val skipWhitespace = false // meaningful spaces in CSV def COMMA = "," def DQUOTE = "\"" def DQUOTE2 = "\"\"" ^^ { case _ => "\"" } // combine 2 dquotes into 1 def CRLF = "\r\n" | "\n" def TXT = "[^\",\r\n]".r def SPACES = "[ \t]+".r def file: Parser[List[List[Ssortingng]]] = repsep(record, CRLF) DQUOTE~>((TXT|COMMA|CRLF|DQUOTE2)*)<~DQUOTE ls.mkSsortingng("") } } def nonescaped: Parser[Ssortingng] = (TXT*) ^^ { case ls => ls.mkSsortingng("") } def parse(s: Ssortingng) = parseAll(file, s) match { case Success(res, _) => res case e => throw new Exception(e.toSsortingng) } } 

Ce que vous avez manqué, ce sont des espaces. J’ai apporté quelques améliorations de bonus.

 import scala.util.parsing.combinator._ object CSV extends RegexParsers { override protected val whiteSpace = """[ \t]""".r def COMMA = "," def DQUOTE = "\"" def DQUOTE2 = "\"\"" ^^ { case _ => "\"" } def CR = "\r" def LF = "\n" def CRLF = "\r\n" def TXT = "[^\",\r\n]".r def file: Parser[List[List[Ssortingng]]] = repsep(record, CRLF) <~ opt(CRLF) def record: Parser[List[String]] = rep1sep(field, COMMA) def field: Parser[String] = (escaped|nonescaped) def escaped: Parser[String] = (DQUOTE~>((TXT|COMMA|CR|LF|DQUOTE2)*)<~DQUOTE) ^^ { case ls => ls.mkSsortingng("")} def nonescaped: Parser[Ssortingng] = (TXT*) ^^ { case ls => ls.mkSsortingng("") } def parse(s: Ssortingng) = parseAll(file, s) match { case Success(res, _) => res case _ => List[List[Ssortingng]]() } } 

Avec la bibliothèque Scala Parser Combinators hors de la bibliothèque standard Scala à partir de la version 2.11, il n’y a aucune raison de ne pas utiliser la bibliothèque beaucoup plus performante de Parboiled2. Voici une version de l’parsingur CSV dans DSL de Parboiled2:

 /* based on comments in https://github.com/sirthias/parboiled2/issues/61 */ import org.parboiled2._ case class Parboiled2CsvParser(input: ParserInput, delimeter: Ssortingng) extends Parser { def DQUOTE = '"' def DELIMITER_TOKEN = rule(capture(delimeter)) def DQUOTE2 = rule("\"\"" ~ push("\"")) def CRLF = rule(capture("\r\n" | "\n")) def NON_CAPTURING_CRLF = rule("\r\n" | "\n") val delims = s"$delimeter\r\n" + DQUOTE def TXT = rule(capture(!anyOf(delims) ~ ANY)) val WHITESPACE = CharPredicate(" \t") def SPACES: Rule0 = rule(oneOrMore(WHITESPACE)) def escaped = rule(optional(SPACES) ~ DQUOTE ~ (zeroOrMore(DELIMITER_TOKEN | TXT | CRLF | DQUOTE2) ~ DQUOTE ~ optional(SPACES)) ~> (_.mkSsortingng(""))) def nonEscaped = rule(zeroOrMore(TXT | capture(DQUOTE)) ~> (_.mkSsortingng(""))) def field = rule(escaped | nonEscaped) def row: Rule1[Seq[Ssortingng]] = rule(oneOrMore(field).separatedBy(delimeter)) def file = rule(zeroOrMore(row).separatedBy(NON_CAPTURING_CRLF)) def parsed() : Try[Seq[Seq[Ssortingng]]] = file.run() } 

L’espace blanc par défaut pour les parsingurs RegexParsers est \s+ , qui inclut de nouvelles lignes. Ainsi, CR , LF et CRLF ne peuvent jamais être traités, car ils sont automatiquement ignorés par l’parsingur.