我想以强类型的方式访问 scala 中的 csv 文件。例如,当我读取 csv 的每一行时,它会被自动解析并表示为具有适当类型的元组。我可以在传递给解析器的某种模式中预先指定类型。是否有任何图书馆可以做到这一点?如果没有,我怎么能自己去实现这个功能?
7 回答
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。
ACollSeq3
是一个IndexedSeq[Product3[T1,T2,T3]]
,也是一个Product3[Seq[T1],Seq[T2],Seq[T3]]
加一点糖的。我是product-collections的作者。
Product3 本质上是元组 3 的元组。
如果您的内容有双引号来包含其他双引号、逗号和换行符,我肯定会使用像opencsv这样的库来正确处理特殊字符。通常你最终得到Iterator[Array[String]]
. 然后,您使用Iterator.map
orcollect
将每个Array[String]
转换为处理类型转换错误的元组。如果您需要在不将所有内容加载到内存的情况下处理输入,则继续使用迭代器,否则您可以转换为 a Vector
orList
并关闭输入流。
所以它可能看起来像这样:
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]
.
您可以使用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)
你会得到一个迭代器,其中每个条目都是 type (Int, String, Either[Float, Boolean])
。请注意 CSV 中最后一列的类型可能不止一种,但可以使用Either
.
这一切都以完全类型安全的方式完成,不涉及反射,在编译时验证。
根据您愿意在兔子洞中走多远,还有一个用于自动案例类和总和类型推导的无形模块,以及对scalaz和cat类型和类型类的支持。
全面披露:我是 kantan.csv 的作者。
编辑:正如评论中所指出的,kantan.csv(请参阅其他答案)可能是我进行此编辑时最好的(2020-09-03)。
由于 CSV 的非平凡引用规则,这比它应该做的更复杂。您可能应该从现有的 CSV 解析器开始,例如OpenCSV或称为 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)
})
}
如果您确实需要相当快地解析它们并且您不知道可能存在什么,您可能应该对各个项目使用某种匹配(例如正则表达式)。无论哪种方式,如果有任何错误的机会,您可能想要使用Try
或Option
类似的方法来打包错误。
我为 Scala 创建了一个强类型的 CSV 帮助器,称为object-csv。它不是一个完全成熟的框架,但可以轻松调整。有了它,你可以这样做:
val peopleFromCSV = readCSV[Person](fileName)
其中 Person 是案例类,定义如下:
case class Person (name: String, age: Int, salary: Double, isNice:Boolean = false)
我建立了自己的想法来强烈地对最终产品进行类型转换,而不是阅读阶段本身……正如所指出的那样,使用 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
}
如果您知道字段的# 和类型,可能是这样的?:
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))
}