【问题标题】:Read CSV in Scala into case class instances with error handling将 Scala 中的 CSV 读入带有错误处理的案例类实例
【发布时间】:2015-03-03 01:53:30
【问题描述】:

我想在 Scala 中读取一个 CSV 字符串/文件,这样给定一个案例类 C 和一个错误类型 Error,解析器填充一个 Iterable[Either[Error,C]]。是否有任何图书馆可以做到这一点或类似的事情?

例如,给定一个类和错误

case class Person(name: String, age: Int)

type Error = String

和 CSV 字符串

Foo,19
Ro
Bar,24

解析器会输出

Stream(Right(Person("Foo",1)), Left("Cannot read 'Ro'"), Right(Person("Bar", 24)))

更新:

我认为我的问题并不清楚,所以让我澄清一下:有没有办法在 Scala 中读取 CSV 而无需定义样板文件?给定 any 案例类,有没有办法自动加载它?我想以这种方式使用它:

val iter = csvParserFor[Person].parseLines(lines)

【问题讨论】:

  • 不太可能有一个图书馆可以做到这一点(无论如何,要求图书馆推荐对于 SO 来说是题外话)。已经有任意数量的 CSV 阅读问题/答案(例如 stackoverflow.com/questions/17126365/…),那么您自己在哪里实施呢?
  • 我刚刚删除了这里的内容,因为讨论有点偏离轨道,我们在下面得到了相关信息。我不是 100% 肯定这其中涉及到“粗鲁”,但我认为这是一个有争议的问题。谈话已经结束了。
  • 我们可以假设 case 类只包含基本值类型(Int、Double)和字符串吗?如果不是,如何在 CSV 中编码更复杂的类型?如果您正在对 csv 进行更一般的序列化/反序列化,我建议您查看 scala-pickling 和 CSV 后端...
  • 是的,我们假设case类只包含基本值
  • 你能解释一下用例吗? CSV 文件是外部的,还是通过序列化案例类创建的?如果你只是序列化/反序列化,我建议像 scala-pickling

标签: scala csv


【解决方案1】:

Scala 2.13 开始,可以通过unapplying a string interpolator 模式匹配Strings:

// case class Person(name: String, age: Int)
val csv = "Foo,19\nRo\nBar,24".split("\n")
csv.map {
  case s"$name,$age" => Right(Person(name, age.toInt))
  case line          => Left(s"Cannot read '$line'")
}
// Array(Right(Person("Foo", 19)), Left("Cannot read 'Ro'"), Right(Person("Bar", 24)))

请注意,您也可以在提取器中使用regexes。

在我们的例子中,如果年龄不是整数,则认为行无效会有所帮助:

// val csv = "Foo,19\nRo\nBar,2R".split("\n")

val Age = "(\\d+)".r

csv.map {
  case s"$name,${Age(age)}" => Right(Person(name, age.toInt))
  case line @ s"$name,$age" => Left(s"Age is not an integer in '$line'")
  case line                 => Left(s"Cannot read '$line'")
}
//Array(Right(Person("Foo", 19)), Left("Cannot read 'Ro'"), Left("Age is not an integer in 'Bar,2R'"))

【讨论】:

    【解决方案2】:

    kantan.csv 似乎是您想要的。如果你想要 0 样板,你可以使用它的 shapeless 模块并写:

    import kantan.csv.ops._
    import kantan.csv.generic.codecs._
    
    new File("path/to/csv").asCsvRows[Person](',', false).toList
    

    根据您的输入,将会产生:

    res2: List[kantan.csv.DecodeResult[Person]] = List(Success(Person(Foo,19)), DecodeFailure, Success(Person(Bar,24)))
    

    请注意,实际的返回类型是一个迭代器,因此您实际上不必在任何时候都将整个 CSV 文件保存在内存中,就像您的示例对 Stream 所做的那样。

    如果无形依赖过多,您可以删除它并提供您自己的案例类类型类,并使用最少的样板:

    implicit val personCodec = RowCodec.caseCodec2(Person.apply, Person.unapply)(0, 1)
    

    完全披露:我是 kantan.csv 的作者。

    【讨论】:

    • 它能否在 CSV 中处理超过 22 列(因为这是案例类的限制)?
    • @aa8y 当然,前提是您有存储它的类型。如果所有列的类型相同,例如Int,您可以请求List[Int],它支持任意长度。异构数据可能需要更多的工作 - 您可能需要实现自己的 RowDecoder 实例,但这真的很简单,它本质上是 Seq[String] => A,其中 A 是所需的类型。
    【解决方案3】:

    这是使用product-collections的解决方案

    import com.github.marklister.collections.io._
    import scala.util.Try
    case class Person(name: String, age: Int)
    val csv="""Foo,19
              |Ro
              |Bar,24""".stripMargin
    
    class TryIterator[T] (it:Iterator[T]) extends Iterator[Try[T]]{
          def next = Try(it.next)
          def hasNext=it.hasNext
    }
    
    new TryIterator(CsvParser(Person).iterator(new java.io.StringReader(csv))).toList
    res14: List[scala.util.Try[Person]] =
    List(Success(Person(Foo,19)), Failure(java.lang.IllegalArgumentException: 1 at line 2 => Ro), Success(Person(Bar,24)))
    

    除了错误处理之外,这与您要查找的内容非常接近:val iter = csvParserFor[Person].parseLines(lines):

    val iter = CsvParser(Person).iterator(reader)

    【讨论】:

      【解决方案4】:

      这是一个 Shapeless 实现,它采用与your proposed example 中的方法略有不同的方法。这是基于我过去编写的一些代码,与您的实现的主要区别在于这个更通用 - 例如,实际的 CSV 解析部分已被分解,因此可以轻松使用专用库.

      首先用于通用Read 类型类(还没有Shapeless):

      import scala.util.{ Failure, Success, Try }
      
      trait Read[A] { def reads(s: String): Try[A] }
      
      object Read {
        def apply[A](implicit readA: Read[A]): Read[A] = readA
      
        implicit object stringRead extends Read[String] {
          def reads(s: String): Try[String] = Success(s)
        }
      
        implicit object intRead extends Read[Int] {
          def reads(s: String) = Try(s.toInt)
        }
      
        // And so on...
      }
      

      然后是有趣的部分:提供从字符串列表到HList 的转换(可能会失败)的类型类:

      import shapeless._
      
      trait FromRow[L <: HList] { def apply(row: List[String]): Try[L] }
      
      object FromRow {
        import HList.ListCompat._
      
        def apply[L <: HList](implicit fromRow: FromRow[L]): FromRow[L] = fromRow
      
        def fromFunc[L <: HList](f: List[String] => Try[L]) = new FromRow[L] {
          def apply(row: List[String]) = f(row)
        }
      
        implicit val hnilFromRow: FromRow[HNil] = fromFunc {
          case Nil => Success(HNil)
          case _ => Failure(new RuntimeException("No more rows expected"))
        }
      
        implicit def hconsFromRow[H: Read, T <: HList: FromRow]: FromRow[H :: T] =
          fromFunc {
            case h :: t => for {
              hv <- Read[H].reads(h)
              tv <- FromRow[T].apply(t)
            } yield hv :: tv
            case Nil => Failure(new RuntimeException("Expected more cells"))
          }
      }
      

      最后让它与案例类一起工作:

      trait RowParser[A] {
        def apply[L <: HList](row: List[String])(implicit
          gen: Generic.Aux[A, L],
          fromRow: FromRow[L]
        ): Try[A] = fromRow(row).map(gen. from)
      }
      
      def rowParserFor[A] = new RowParser[A] {}
      

      现在我们可以编写以下代码,例如,使用OpenCSV

      case class Foo(s: String, i: Int)
      
      import au.com.bytecode.opencsv._
      import scala.collection.JavaConverters._
      
      val reader = new CSVReader(new java.io.FileReader("foos.csv"))
      
      val foos = reader.readAll.asScala.map(row => rowParserFor[Foo](row.toList))
      

      如果我们有这样的输入文件:

      first,10
      second,11
      third,twelve
      

      我们将得到以下信息:

      scala> foos.foreach(println)
      Success(Foo(first,10))
      Success(Foo(second,11))
      Failure(java.lang.NumberFormatException: For input string: "twelve")
      

      (请注意,这会让人联想到每一行的 GenericFromRow 实例,但如果性能是一个问题,很容易改变它。)

      【讨论】:

      • implicit fromRowrowParserFor 方法中怎么样?我们如何得到它?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-07-29
      • 1970-01-01
      • 2020-03-07
      • 1970-01-01
      • 1970-01-01
      • 2019-10-13
      • 1970-01-01
      相关资源
      最近更新 更多