【问题标题】:Nested Scala case classes to/from CSV与 CSV 之间的嵌套 Scala 案例类
【发布时间】:2018-09-03 18:45:45
【问题描述】:

有许多不错的库可用于将 Scala 案例类写入/读取 CSV 文件。我正在寻找能够处理嵌套案例类的东西。比如这里一个Match有两个Players

case class Player(name: String, ranking: Int)
case class Match(place: String, winner: Player, loser: Player)

val matches = List(
  Match("London", Player("Jane",7), Player("Fred",23)),
  Match("Rome", Player("Marco",19), Player("Giulia",3)),
  Match("Paris", Player("Isabelle",2), Player("Julien",5))
)

我想毫不费力地(没有样板!)在此 CSV 中写入/读取matches

place,winner.name,winner.ranking,loser.name,loser.ranking
London,Jane,7,Fred,23
Rome,Marco,19,Giulia,3
Paris,Isabelle,2,Julien,5

注意使用点“。”的自动标题行形成嵌套字段的列名,例如winner.ranking。如果有人可以演示一种简单的方法来做到这一点,我会很高兴(例如,使用反射或Shapeless)。

[动机。在数据分析期间,即使在嵌套案例类的情况下,也可以方便地使用平面 CSV 进行排序、过滤等操作。如果您可以从此类文件中加载嵌套的案例类,那就太好了。]

【问题讨论】:

  • 我会推荐github.com/melrief/PureCSV(基于无形)
  • PureCSV 是一个很棒的包。但是,它不适用于 嵌套 案例类。这道题的目标正是处理嵌套问题。

标签: scala csv export-to-csv case-class import-from-csv


【解决方案1】:

由于 case-class 是 Product,因此获取各个字段的值相对容易。获取字段/列的名称确实需要使用 Java 反射。 下面的函数接受一个案例类实例列表并返回一个行列表,每个行都是一个字符串列表。它使用递归来获取子案例类实例的值和标题。

def toCsv(p: List[Product]): List[List[String]] = {
  def header(c: Class[_], prefix: String = ""): List[String] = {
    c.getDeclaredFields.toList.flatMap { field =>
      val name = prefix + field.getName
      if (classOf[Product].isAssignableFrom(field.getType)) header(field.getType, name + ".")
      else List(name)
    }
  }

  def flatten(p: Product): List[String] =
    p.productIterator.flatMap {
      case p: Product => flatten(p)
      case v: Any => List(v.toString)
    }.toList

  header(classOf[Match]) :: p.map(flatten)
}

但是,从 CSV 构造案例类要复杂得多,需要使用反射来获取各种字段的类型、从 CSV 字符串创建值以及构造案例类实例。 为简单起见(并不是说代码很简单,只是为了不会更复杂),我假设 CSV 中的列顺序与上面的 toCsv(...) 函数生成的文件相同。 以下函数首先创建“如何处理单个 CSV 行的说明”列表(这些说明还用于验证 CSV 中的列标题是否与案例类属性匹配)。然后使用这些指令递归地一次生成一个 CSV 行。

def fromCsv[T <: Product](csv: List[List[String]])(implicit tag: ClassTag[T]): List[T] = {
  trait Instruction {
    val name: String
    val header = true
  }
  case class BeginCaseClassField(name: String, clazz: Class[_]) extends Instruction {
    override val header = false
  }
  case class EndCaseClassField(name: String) extends Instruction {
    override val header = false
  }
  case class IntField(name: String) extends Instruction
  case class StringField(name: String) extends Instruction
  case class DoubleField(name: String) extends Instruction

  def scan(c: Class[_], prefix: String = ""): List[Instruction] = {
    c.getDeclaredFields.toList.flatMap { field =>
      val name = prefix + field.getName
      val fType = field.getType

      if (fType == classOf[Int]) List(IntField(name))
      else if (fType == classOf[Double]) List(DoubleField(name))
      else if (fType == classOf[String]) List(StringField(name))
      else if (classOf[Product].isAssignableFrom(fType)) BeginCaseClassField(name, fType) :: scan(fType, name + ".")
      else throw new IllegalArgumentException(s"Unsupported field type: $fType")
    } :+ EndCaseClassField(prefix)
  }

  def produce(instructions: List[Instruction], row: List[String], argAccumulator: List[Any]): (List[Instruction], List[String], List[Any]) = instructions match {
    case IntField(_) :: tail => produce(tail, row.drop(1), argAccumulator :+ row.head.toString.toInt)
    case StringField(_) :: tail => produce(tail, row.drop(1), argAccumulator :+ row.head.toString)
    case DoubleField(_) :: tail => produce(tail, row.drop(1), argAccumulator :+ row.head.toString.toDouble)
    case BeginCaseClassField(_, clazz) :: tail =>
      val (instructionRemaining, rowRemaining, constructorArgs) = produce(tail, row, List.empty)
      val newCaseClass = clazz.getConstructors.head.newInstance(constructorArgs.map(_.asInstanceOf[AnyRef]): _*)
      produce(instructionRemaining, rowRemaining, argAccumulator :+ newCaseClass)
    case EndCaseClassField(_) :: tail => (tail, row, argAccumulator)
    case Nil if row.isEmpty => (Nil, Nil, argAccumulator)
    case Nil => throw new IllegalArgumentException("Not all values from CSV row were used")
  }

  val instructions = BeginCaseClassField(".", tag.runtimeClass) :: scan(tag.runtimeClass)
  assert(csv.head == instructions.filter(_.header).map(_.name), "CSV header doesn't match target case-class fields")

  csv.drop(1).map(row => produce(instructions, row, List.empty)._3.head.asInstanceOf[T])
}

我已使用以下方法对此进行了测试:

case class Player(name: String, ranking: Int, price: Double)
case class Match(place: String, winner: Player, loser: Player)

val matches = List(
  Match("London", Player("Jane", 7, 12.5), Player("Fred", 23, 11.1)),
  Match("Rome", Player("Marco", 19, 13.54), Player("Giulia", 3, 41.8)),
  Match("Paris", Player("Isabelle", 2, 31.7), Player("Julien", 5, 16.8))
)
val csv = toCsv(matches)
val matchesFromCsv = fromCsv[Match](csv)

assert(matches == matchesFromCsv)

如果您想将其用于生产,显然应该对其进行优化和强化...

【讨论】:

  • 谢谢迈克尔。如果你知道怎么做,你能指出如何从 csv 行构造嵌套的案例类吗? (您链接到的帖子似乎只处理一个未嵌套的案例类。)
  • 我发布了一个更新的答案,其中包含 toCsv(...) 函数和(更丑陋的)fromCsv(...) 函数。
猜你喜欢
  • 1970-01-01
  • 2019-11-08
  • 1970-01-01
  • 2021-11-06
  • 1970-01-01
  • 2016-03-13
  • 1970-01-01
  • 2013-11-08
  • 2013-09-13
相关资源
最近更新 更多