将 Scala 中的 CSV 读入带有错误处理的 case class 实例
Read CSV in Scala into case class instances with error handling
我想在 Scala 中读取 CSV String/File,这样给定一个案例 class C
和一个错误类型 Error
,解析器将填充一个 Iterable[Either[Error,C]]
。有没有图书馆做这个或类似的事情?
例如,给定 class 和错误
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 案例 class,有没有办法自动加载它?我想这样使用它:
val iter = csvParserFor[Person].parseLines(lines)
这是一个 Shapeless 实现,它采用与 your proposed example 中的方法略有不同的方法。这是基于我过去编写的一些代码,与您的实现的主要区别在于这个实现更通用一些——例如,实际的 CSV 解析部分被分解出来,以便于使用专用库.
首先是通用 Read
类型 class(还没有 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
:
的转换(可能会失败)的类型 class
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"))
}
}
最后让它适用于 case classes:
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")
(请注意,这会为每一行生成 Generic
和 FromRow
实例,但如果关注性能,则很容易更改它。)
这是使用 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)
kantan.csv seems like what you want. If you want 0 boilerplate, you can use its 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)))
请注意,实际的 return 类型是一个迭代器,因此您实际上不必像 Stream
.[= 示例那样随时将整个 CSV 文件保存在内存中15=]
如果 shapeless 依赖太多,你可以放弃它并提供你自己的案例 class 类型 classes with minimal boilerplate:
implicit val personCodec = RowCodec.caseCodec2(Person.apply, Person.unapply)(0, 1)
完全披露:我是 kantan.csv 的作者。
从 Scala 2.13
开始,可以通过 unapplying a string interpolator:
对 String
进行模式匹配
// 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)))
请注意,您还可以在提取器中使用 regex
es。
在我们的案例中,如果年龄不是整数,则将行视为无效可能会有所帮助:
// 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'"))
我想在 Scala 中读取 CSV String/File,这样给定一个案例 class C
和一个错误类型 Error
,解析器将填充一个 Iterable[Either[Error,C]]
。有没有图书馆做这个或类似的事情?
例如,给定 class 和错误
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 案例 class,有没有办法自动加载它?我想这样使用它:
val iter = csvParserFor[Person].parseLines(lines)
这是一个 Shapeless 实现,它采用与 your proposed example 中的方法略有不同的方法。这是基于我过去编写的一些代码,与您的实现的主要区别在于这个实现更通用一些——例如,实际的 CSV 解析部分被分解出来,以便于使用专用库.
首先是通用 Read
类型 class(还没有 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"))
}
}
最后让它适用于 case classes:
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")
(请注意,这会为每一行生成 Generic
和 FromRow
实例,但如果关注性能,则很容易更改它。)
这是使用 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)
kantan.csv seems like what you want. If you want 0 boilerplate, you can use its 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)))
请注意,实际的 return 类型是一个迭代器,因此您实际上不必像 Stream
.[= 示例那样随时将整个 CSV 文件保存在内存中15=]
如果 shapeless 依赖太多,你可以放弃它并提供你自己的案例 class 类型 classes with minimal boilerplate:
implicit val personCodec = RowCodec.caseCodec2(Person.apply, Person.unapply)(0, 1)
完全披露:我是 kantan.csv 的作者。
从 Scala 2.13
开始,可以通过 unapplying a string interpolator:
String
进行模式匹配
// 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)))
请注意,您还可以在提取器中使用 regex
es。
在我们的案例中,如果年龄不是整数,则将行视为无效可能会有所帮助:
// 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'"))