在scala中对csv的强类型访问?

在scala中对csv的强类型访问?,scala,csv,tuples,strong-typing,Scala,Csv,Tuples,Strong Typing,我想以强类型方式访问scala中的csv文件。例如,当我读取csv的每一行时,它会被自动解析并表示为具有适当类型的元组。我可以在传递给解析器的某种模式中预先指定类型。有没有这样做的库?如果没有,我怎么能自己实现这个功能呢?如果您知道字段的类型和类型,可能是这样的话: case class Friend(id: Int, name: String) // 1, Fred val friends = scala.io.Source.fromFile("friends.csv").getLines

我想以强类型方式访问scala中的csv文件。例如,当我读取csv的每一行时,它会被自动解析并表示为具有适当类型的元组。我可以在传递给解析器的某种模式中预先指定类型。有没有这样做的库?如果没有,我怎么能自己实现这个功能呢?

如果您知道字段的类型和类型,可能是这样的话:

case class Friend(id: Int, name: String) // 1,  Fred

val friends = scala.io.Source.fromFile("friends.csv").getLines.map { line =>
   val fields = line.split(',')
   Friend(fields(0).toInt, fields(1))
}

编辑:正如在评论中指出的那样,kantan.csv(见其他答案)可能是我进行编辑(2020-09-03)时最好的

由于CSV的非平凡引用规则,这使得它变得比应该的更复杂。您可能应该从现有的CSV解析器开始,例如,或者从一个名为scala CSV的项目开始。(有。)

然后你会得到一些字符串的集合。如果您不需要快速读取大量CSV文件,您可以尝试将每一行解析为每一种类型,并使用第一个不会引发异常的行。比如说,

import scala.util._

case class Person(first: String, last: String, age: Int) {}
object Person {
  def fromCSV(xs: Seq[String]) = Try(xs match {
    case s0 +: s1 +: s2 +: more => new Person(s0, s1, s2.toInt)
  })
}

如果您确实需要相当快地解析它们,并且您不知道可能会有什么,那么您可能应该对单个项使用某种匹配(例如正则表达式)。无论哪种方式,如果有任何可能的错误,您可能希望使用
尝试
选项
或类似的方法来打包错误。

如果您的内容有双引号来括住其他双引号、逗号和换行符,我肯定会使用这样的库来正确处理特殊字符。通常情况下,您最终使用的是
迭代器[Array[String]]
。然后使用
Iterator.map
collect
将每个
Array[String]
转换为元组,处理那里的类型转换错误。如果需要处理输入而不在内存中加载所有内容,则继续使用迭代器,否则可以转换为
向量
列表
,并关闭输入流

所以它可能看起来像这样:

val reader = new CSVReader(new FileReader(filename))
val iter = reader.iterator()
val typed = iter collect {
  case Array(double, int, string) => (double.toDouble, int.toInt, string)
}
// do more work with typed
// close reader in a finally block
case class Person (name: String, age: Int, salary: Double, isNice:Boolean = false)
根据需要如何处理错误,可以返回
Left
表示错误,返回
Right
表示成功的元组,以将错误与正确的行分开。此外,我有时会使用关闭资源来包装所有这些内容。因此,我的数据可能被包装到
resource.ManagedResource
monad中,这样我就可以使用来自多个文件的输入


最后,尽管您希望使用元组,但我发现,通常更清楚的做法是使用一个适合该问题的case类,然后编写一个方法,从
数组[String]
创建该case类对象

我建立了自己的想法,以强烈地类型化最终产品,而不仅仅是阅读阶段本身。正如所指出的,在第一阶段中使用类似Apache CSV的东西可能会更好地处理它,而第二阶段可能就是我所做的。这是代码,欢迎使用。我们的想法是用T类型对CSVReader[T]进行类型转换。。构造时,还必须向读取器提供类型为[T]的Factor对象。这里的想法是类本身(或者在我的示例中是一个helper对象)决定构造细节,从而将其与实际读取分离。您可以使用隐式对象传递辅助对象,但我在这里没有这样做。唯一的缺点是CSV的每一行必须是相同的类类型,但是您可以根据需要扩展这个概念

class CsvReader/**
 * @param fname
 * @param hasHeader : ignore header row
 * @param delim     : "\t" , etc     
 */

 [T] ( factory:CsvFactory[T], fname:String, delim:String) {

  private val f = Source.fromFile(fname)
  private var lines = f.getLines  //iterator
  private var fileClosed = false

  if (lines.hasNext) lines = lines.dropWhile(_.trim.isEmpty) //skip white space

  def hasNext = (if (fileClosed) false else lines.hasNext)

  lines = lines.drop(1) //drop header , assumed to exist


 /**
 * also closes the file 
 * @return the line
 */
def nextRow ():String = {  //public version
    val ans = lines.next
    if (ans.isEmpty) throw new Exception("Error in CSV, reading past end "+fname)
    if (lines.hasNext) lines = lines.dropWhile(_.trim.isEmpty) else close()

    ans 
  }

  //def nextObj[T](factory:CsvFactory[T]): T = past version

  def nextObj(): T = {  //public version

    val s = nextRow()
    val a = s.split(delim)        
    factory makeObj a
  }

  def allObj() : Seq[T] = {

    val ans = scala.collection.mutable.Buffer[T]()
    while (hasNext) ans+=nextObj()

    ans.toList
  }

  def close() = {
    f.close;
    fileClosed = true
  }

} //class 
接下来是示例帮助器工厂和示例“Main”

最后,作者(注意工厂方法也要求使用“makerow”

似乎非常适合您的要求:

scala> val data = CsvParser[String,Int,Double].parseFile("sample.csv")
data: com.github.marklister.collections.immutable.CollSeq3[String,Int,Double] = 
CollSeq((Jan,10,22.33),
        (Feb,20,44.2),
        (Mar,25,55.1))
在引擎盖下使用

一个
CollSeq3
是一个
IndexedSeq[Product3[T1,T2,T3]]
和一个
Product3[Seq[T1],Seq[T2],Seq[T3]
加了一点糖。我是这本书的作者

这是


Product3本质上是arity 3的一个元组。

我为Scala创建了一个强类型CSV帮助程序,名为。它不是一个成熟的框架,但可以轻松调整。使用它,您可以做到:

val peopleFromCSV = readCSV[Person](fileName)
其中Person是case类,定义如下:

val reader = new CSVReader(new FileReader(filename))
val iter = reader.iterator()
val typed = iter collect {
  case Array(double, int, string) => (double.toDouble, int.toInt, string)
}
// do more work with typed
// close reader in a finally block
case class Person (name: String, age: Int, salary: Double, isNice:Boolean = false)
在或“我的关于它”中阅读更多关于它的信息。

您可以使用它,它的设计正是为了这个目的

假设您有以下输入:

1,Foo,2.0
2,Bar,false
使用kantan.csv,您可以编写以下代码来解析它:

import kantan.csv.ops._

new File("path/to/csv").asUnsafeCsvRows[(Int, String, Either[Float, Boolean])](',', false)
您将得到一个迭代器,其中每个条目的类型为
(Int,String,或[Float,Boolean])
。请注意,CSV中的最后一列可以是多种类型,但这可以通过
方便地处理

这一切都是以完全类型安全的方式完成的,不涉及反射,在编译时进行验证

根据您愿意走多远,还有一个用于自动case类和sum类型派生的模块,以及对and类型和类型类的支持


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

使用case类的优点是什么?它为
Person(name:String,age:Int)
等字段命名。因此,稍后当您需要访问它时,您可以执行
p.name
而不是
t.\u 1
。例如,它在
行中运行良好。sortBy(\u.name)
我喜欢它的外观,但我正在试图弄清楚它是如何工作的。我真的不太明白CsvParser.scala.template中发生了什么。样板部分中的这些模板是什么?模板被处理后会生成一个scala文件。如果你构建项目,你可以在target/scala-2.10/src\u中看到结果目录。这与scala自己的Tuple1…Tuple22的工作方式完全相同。CSVParser存在于从1到22的算术中,编译器为您提供的类型签名(架构)选择正确的一个。是否有建议的方法来处理空值或失败的值
import kantan.csv.ops._

new File("path/to/csv").asUnsafeCsvRows[(Int, String, Either[Float, Boolean])](',', false)