【问题标题】:Strongly typed access to csv in scala?在scala中对csv的强类型访问?
【发布时间】:2013-06-15 17:55:46
【问题描述】:

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

【问题讨论】:

  • 因为有很多问题,即使存在 .csv MIME 类型的 RFC,我强烈建议您使用维护良好的 RFC 驱动的原生 Scala 库,它可以最佳地处理这个问题,kantan .csv:nrinaudo.github.io/kantan.csv

标签: scala csv tuples strong-typing


【解决方案1】:

product-collections 似乎非常适合您的要求:

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))

product-collections 在后台使用opencsv

CollSeq3IndexedSeq[Product3[T1,T2,T3]]Product3[Seq[T1],Seq[T2],Seq[T3]] 加一点糖。我是product-collections的作者。

这里是a link to the io page of the scaladoc

Product3 本质上是元组 3 的元组。

【讨论】:

  • 我喜欢它的外观,但我想弄清楚它是如何工作的。我真的不明白 CsvParser.scala.template 中发生了什么。样板部分中的这些模板是什么?
  • 模板由github.com/marklister/sbt-boilerplate处理,生成一个scala文件。如果您构建项目,您可以在 target/scala-2.10/src_managed 目录中看到结果。这与 scala 自己的 Tuple1 ... Tuple22 似乎工作的方式完全相同。 CsvParsers 存在于 1 到 22 的 arities 中,编译器会为您提供的类型签名(模式)选择正确的。
  • 是否有推荐的方法来处理空值或无法解析的值?如果我可以将 Option[T] 作为类型参数,那就太好了;它可以尝试将其解析为 T 并在解析失败或为空时给出 None。
  • 我在 README.md 中添加了一个名为“从字段解析错误中恢复”的部分。那里有一个解析 Option[Int]] 的例子。您可以提交拉取请求以支持这些转换器。请参阅 src/main/scala/io 中的文件 GeneralConverter.scala
【解决方案2】:

如果您的内容有双引号来包含其他双引号、逗号和换行符,我肯定会使用像 opencsv 这样的库来正确处理特殊字符。通常你会得到Iterator[Array[String]]。然后使用Iterator.mapcollect 将每个Array[String] 转换为处理类型转换错误的元组。如果您需要在不将所有内容加载到内存的情况下处理输入,则继续使用迭代器,否则您可以转换为 VectorList 并关闭输入流。

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

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

根据您需要如何处理错误,您可以为错误返回Left,为成功元组返回Right,以将错误与正确的行分开。此外,我有时使用scala-arm 来关闭所有这些资源。所以我的数据可能会包装到 resource.ManagedResource monad 中,这样我就可以使用来自多个文件的输入。

最后,虽然您想使用元组,但我发现拥有一个适合该问题的案例类然后编写一个从Array[String] 创建该案例类对象的方法通常更清楚。

【讨论】:

  • 使用case类有什么好处?
  • 它给像Person(name: String, age: Int)这样的字段命名。所以稍后当你需要访问它时,你可以使用p.name 而不是t._1。例如在rows.sortBy(_.name) 中效果很好
【解决方案3】:

您可以使用kantan.csv,它的设计正是出于这一目的。

假设您有以下输入:

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, Either[Float, Boolean])。请注意 CSV 中的最后一列可能有多种类型,但可以使用 Either 方便地处理。

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

根据您愿意走多远,还有一个 shapeless 模块用于自动案例类和求和类型推导,以及对 scalazcats 类型和类型类的支持.

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

【讨论】:

  • 在多次浏览此空间后,我非常高兴您生成了一个符合 RFC 4180 的解决方案!太棒了! tools.ietf.org/html/rfc4180
【解决方案4】:

我为 Scala 创建了一个强类型的 CSV 帮助程序,名为 object-csv。它不是一个完全成熟的框架,但可以轻松调整。有了它,您可以这样做:

val peopleFromCSV = readCSV[Person](fileName)

其中 Person 是案例类,定义如下:

case class Person (name: String, age: Int, salary: Double, isNice:Boolean = false)

GitHub 或我的blog post 中了解更多信息。

【讨论】:

  • 因为有很多问题,即使存在 .csv MIME 类型的 RFC,我强烈建议您使用维护良好的 RFC 驱动的原生 Scala 库,它可以最佳地处理这个问题,kantan .csv:nrinaudo.github.io/kantan.csv
【解决方案5】:

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

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

然后你会得到某种字符串集合的集合。如果您不需要快速读取大量 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)
  })
}

如果您确实需要相当快地解析它们并且您不知道其中可能存在什么,您可能应该对各个项目使用某种匹配(例如正则表达式)。无论哪种方式,如果有任何错误的机会,您可能想使用TryOption 或类似的东西来打包错误。

【讨论】:

  • 我强烈建议你使用一个维护良好的 RFC 驱动的原生 Scala 库,它可以最佳地处理这个问题,kantan.csv:nrinaudo.github.io/kantan.csv
【解决方案6】:

我建立了自己的想法来强烈地对最终产品进行类型转换,而不是阅读阶段本身..正如所指出的那样,使用 Apache CSV 之类的东西作为第一阶段可能会更好地处理,而第二阶段可能就是我所做的.这是欢迎您使用的代码。这个想法是使用类型 T 对 CSVReader[T] 进行类型转换 .. 在构造时,您还必须为阅读器提供 Type[T] 的 Factor 对象。这里的想法是类本身(或在我的示例中为辅助对象)决定构造细节,从而将其与实际阅读脱钩。您可以使用隐式对象来传递帮助器,但我在这里没有这样做。唯一的缺点是 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 

接下来是示例 Helper Factory 和示例“Main”

trait CsvFactory[T] {  //handles all serial controls (in and out)   

  def makeObj(a:Seq[String]):T  //for reading 

  def makeRow(obj:T):Seq[String]//the factory basically just passes this duty 

  def header:Seq[String]    //must define headers for writing 
}



/**
 * Each class implements this as needed, so the object can be serialized by the writer
 */


case class TestRecord(val name:String, val addr:String, val zip:Int)  {

  def toRow():Seq[String] = List(name,addr,zip.toString) //handle conversion to CSV

}


object TestFactory extends CsvFactory[TestRecord] {

  def makeObj (a:Seq[String]):TestRecord =  new TestRecord(a(0),a(1),a(2).toDouble.toInt)
  def header = List("name","addr","zip")
  def makeRow(o:TestRecord):Seq[String] = {
    o.toRow.map(_.toUpperCase())
  }

}

object CsvSerial {

  def main(args: Array[String]): Unit = {

    val whereami = System.getProperty("user.dir")
    println("Begin CSV test in "+whereami) 

    val reader = new CsvReader(TestFactory,"TestCsv.txt","\t")


    val all = reader.allObj() //read the CSV info a file
    sd.p(all)
    reader.close

    val writer = new CsvWriter(TestFactory,"TestOut.txt", "\t")

    for (x<-all) writer.printObj(x)
    writer.close

  } //main  
}

CSV 示例(制表符分隔.. 如果您从编辑器复制,可能需要修复)

Name    Addr    Zip "Sanders, Dante R." 4823 Nibh Av.   60797.00 "Decker, Caryn G." 994-2552 Ac Rd. 70755.00 "Wilkerson, Jolene Z." 3613 Ultrices. St.  62168.00 "Gonzales, Elizabeth W."   "P.O. Box 409, 2319 Cursus. Rd."    72909.00 "Rodriguez, Abbot O."  Ap #541-9695 Fusce Street   23495.00 "Larson, Martin L."    113-3963 Cras Av.   36008.00 "Cannon, Zia U."   549-2083 Libero Avenue  91524.00 "Cook, Amena B."   Ap
#668-5982 Massa Ave 69205.00

最后是作者(注意工厂方法也需要“makerow”

import java.io._


    class CsvWriter[T] (factory:CsvFactory[T], fname:String, delim:String, append:Boolean = false) {

      private val out   = new PrintWriter(new BufferedWriter(new FileWriter(fname,append)));
      if (!append)  out.println(factory.header mkString delim )

      def flush() = out.flush()


      def println(s:String) =    out.println(s)

      def printObj(obj:T) =  println( factory makeRow(obj) mkString(delim) )
      def printAll(objects:Seq[T]) = objects.foreach(printObj(_))
      def close() = out.close

    }

【讨论】:

    【解决方案7】:

    如果你知道字段的#和类型,可能是这样的?:

    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))
    }
    

    【讨论】:

      猜你喜欢
      • 2011-01-30
      • 1970-01-01
      • 1970-01-01
      • 2012-01-04
      • 2011-05-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-25
      相关资源
      最近更新 更多