Чтение CSV в Scala в экземплярах класса case с обработкой ошибок

Я хотел бы прочитать CSV String/File в Scala, который задал класс case 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)))

UPDATE:

Я думаю, что мой вопрос не ясен, поэтому позвольте мне уточнить: есть ли способ читать CSV в Scala без указания шаблона? Учитывая класс any, есть ли способ загрузить его автоматически? Я хотел бы использовать его следующим образом:

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

Ответ 1

Здесь реализация Shapeless, которая немного отличается от той, что находится в ваш предлагаемый пример. Это основано на некотором коде, который я написал в прошлом, и основное отличие от вашей реализации состоит в том, что это немного более общее - например, фактическая часть синтаксического анализа 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"))
    }
}

И, наконец, чтобы он работал с классами case:

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 экземпляры для каждой строки, но было бы довольно легко изменить это, если производительность является проблемой.)

Ответ 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.

Если чрезмерная зависимость слишком велика, вы можете отказаться от нее и предоставить свои классы классов класса case с минимальным шаблоном:

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

Полное раскрытие: я автор kantan.csv.

Ответ 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

Начиная с Scala 2.13, можно сопоставить 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 в экстракторе.

В нашем случае это может помочь считать строку недействительной, если возраст не является целым числом:

// 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'"))