복붙노트

[SCALA] 구문 분석 CSV 파일을 사용 스칼라 파서 콤비

SCALA

구문 분석 CSV 파일을 사용 스칼라 파서 콤비

나는 스칼라 파서 콤비를 사용하여 CSV 파서를 작성하는 것을 시도하고있다. 문법은 RFC4180을 기반으로합니다. 나는 다음과 같은 코드를 함께했다. 그것은 거의 작동하지만, 나는 제대로 다른 기록을 분리 할 수 ​​없습니다. 내가 놓친 게 무엇입니까?

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[String]]] = ((record~((CRLF~>record)*))<~(CRLF?)) ^^ { 
    case r~rs => r::rs
  }
  def record: Parser[List[String]] = (field~((COMMA~>field)*)) ^^ {
    case f~fs => f::fs
  }
  def field: Parser[String] = escaped|nonescaped
  def escaped: Parser[String] = (DQUOTE~>((TXT|COMMA|CR|LF|DQUOTE2)*)<~DQUOTE) ^^ { case ls => ls.mkString("")}
  def nonescaped: Parser[String] = (TXT*) ^^ { case ls => ls.mkString("") }

  def parse(s: String) = parseAll(file, s) match {
    case Success(res, _) => res
    case _ => List[List[String]]()
  }
}


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

기본 RegexParsers은 공백, 탭, 캐리지 리턴, 그리고 정규 표현식 [\ S] +를 사용하여 줄 바꿈을 포함하여 공백을 무시합니다. 기록을 분리 할 수없는 위 파서의 문제는이 때문이다. 우리는 비활성화 skipWhitespace 모드로해야합니다. 그것은 바람직하지,합니다 (CSV가 "는 foobar"가에 따라서 "푸 바") 필드에있는 모든 공간을 무시하기 때문에 단지 [\ t]로 정의 공백 장착} 문제를 해결하지 못한다. 파서의 업데이트 된 소스는 이렇게이다

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[String]]] = repsep(record, CRLF) <~ (CRLF?)

  def record: Parser[List[String]] = repsep(field, COMMA)

  def field: Parser[String] = escaped|nonescaped


  def escaped: Parser[String] = {
    ((SPACES?)~>DQUOTE~>((TXT|COMMA|CRLF|DQUOTE2)*)<~DQUOTE<~(SPACES?)) ^^ { 
      case ls => ls.mkString("")
    }
  }

  def nonescaped: Parser[String] = (TXT*) ^^ { case ls => ls.mkString("") }



  def parse(s: String) = parseAll(file, s) match {
    case Success(res, _) => res
    case e => throw new Exception(e.toString)
  }
}

해결법

  1. ==============================

    1.당신이 놓친 것은 공백입니다. 나는 몇 보너스 개선에 던졌다.

    당신이 놓친 것은 공백입니다. 나는 몇 보너스 개선에 던졌다.

    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[String]]] = 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.mkString("")}
      def nonescaped: Parser[String] = (TXT*) ^^ { case ls => ls.mkString("") }
    
      def parse(s: String) = parseAll(file, s) match {
        case Success(res, _) => res
        case _ => List[List[String]]()
      }
    }
    
  2. ==============================

    2.2.11부터 스칼라 표준 라이브러리 중 스칼라 파서 콤비의 라이브러리를 훨씬 더 성능이 좋은 Parboiled2 라이브러리를 사용하지 않는 좋은 이유가 없다. 여기 Parboiled2의 DSL에서 CSV 파서의 버전은 다음과 같습니다

    2.11부터 스칼라 표준 라이브러리 중 스칼라 파서 콤비의 라이브러리를 훨씬 더 성능이 좋은 Parboiled2 라이브러리를 사용하지 않는 좋은 이유가 없다. 여기 Parboiled2의 DSL에서 CSV 파서의 버전은 다음과 같습니다

    /*  based on comments in https://github.com/sirthias/parboiled2/issues/61 */
    import org.parboiled2._
    case class Parboiled2CsvParser(input: ParserInput, delimeter: String) 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)) ~> (_.mkString("")))
      def nonEscaped = rule(zeroOrMore(TXT | capture(DQUOTE)) ~> (_.mkString("")))
    
      def field = rule(escaped | nonEscaped)
      def row: Rule1[Seq[String]] = rule(oneOrMore(field).separatedBy(delimeter))
      def file = rule(zeroOrMore(row).separatedBy(NON_CAPTURING_CRLF))
    
      def parsed() : Try[Seq[Seq[String]]] = file.run()
    }
    
  3. ==============================

    3.RegexParsers 파서의 기본 공백은 새로운 라인을 포함 \ S +이다. 자동 파서 건너으로 CR, LF 및 CRLF 그래서 기회가 처리 할 수 ​​없다.

    RegexParsers 파서의 기본 공백은 새로운 라인을 포함 \ S +이다. 자동 파서 건너으로 CR, LF 및 CRLF 그래서 기회가 처리 할 수 ​​없다.

  4. from https://stackoverflow.com/questions/5063022/use-scala-parser-combinator-to-parse-csv-files by cc-by-sa and MIT license