2013-06-15 4 views
8

Ich möchte auf CSV-Dateien in Scala in einer stark typisierten Weise zugreifen. Zum Beispiel, wenn ich jede Zeile des CSV lese, wird es automatisch analysiert und als Tupel mit den entsprechenden Typen dargestellt. Ich könnte die Typen vorher in einer Art Schema angeben, das an den Parser übergeben wird. Gibt es Bibliotheken, die dafür existieren? Wenn nicht, wie könnte ich diese Funktionalität selbst implementieren?Stark getippten Zugriff auf CSV in Scala?

Antwort

12

product-collections erscheint eine gute Passform für Ihre Anforderungen zu sein:

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-collectionsopencsv unter der Haube verwendet.

Ein CollSeq3 ist ein IndexedSeq[Product3[T1,T2,T3]] und auch ein Product3[Seq[T1],Seq[T2],Seq[T3]] mit etwas Zucker. Ich bin der Autor von.

Hier a link to the io page of the scaladoc

Product3 ist im Wesentlichen ein Tupel von arity 3.

+0

Ich mag die Art, wie das aussieht, aber ich versuche herauszufinden, wie es funktioniert. Ich verstehe nicht wirklich, was in CsvParser.scala.template vor sich geht. Was sind diese Vorlagen im Textbaustein? – mushroom

+0

Die Vorlage wird von http://github.com/marklister/sbt-boilerplate verarbeitet, die eine Scala-Datei generiert. Wenn Sie das Projekt erstellen, können Sie die Ergebnisse im Verzeichnis target/scala-2.10/src_managed sehen. Es ist genau so, wie Scalas eigene Tuple1 ... Tuple22 zu funktionieren scheint. CsvParsers existieren für Aritäten 1 bis 22 und der Compiler wählt das richtige für die Typenunterschrift (Schema) aus, die Sie bereitstellen. –

+0

Gibt es eine empfohlene Methode zum Umgang mit Nullwerten oder Werten, die nicht analysiert werden können? Es wäre schön, wenn ich Option [T] als Typparameter geben könnte; es könnte versuchen, es als ein T zu analysieren und eine None zu geben, wenn es nicht parsen konnte oder leer war. – mushroom

-1

Wenn Sie die # und Feldtypen kennen, oder vielleicht wie folgt ?:

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

Dies wird komplizierter, als es wegen der nicht-trivialen zitiert Regeln für CSV sollte. Sie sollten wahrscheinlich mit einem vorhandenen CSV-Parser beginnen, z. OpenCSV oder eines der Projekte namens scala-csv. (Es gibt atleastthree.)

Dann sind Sie mit irgendeiner Art von Sammlung von Sammlungen von Strings enden. Wenn Sie keine umfangreichen CSV-Dateien schnell lesen müssen, können Sie einfach versuchen, jede Zeile in jeden Ihrer Typen zu zerlegen und den ersten zu verwenden, der keine Ausnahme auslöst. Zum Beispiel

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

Wenn Sie sie brauchen, um zu analysieren ziemlich schnell, und Sie wissen nicht, was es sein könnte, sollten Sie vielleicht eine Art von Anpassung (z Regexes) zu den einzelnen Positionen verwendet werden. Wie auch immer, wenn es eine Fehlermöglichkeit gibt, möchten Sie wahrscheinlich Try oder Option oder so etwas verwenden, um Fehler zu packen.

2

Wenn Ihr Inhalt doppelte Anführungszeichen hat, um andere doppelte Anführungszeichen, Kommas und Zeilenumbrüche einzufügen, würde ich definitiv eine Bibliothek wie opencsv verwenden, die sich mit Sonderzeichen richtig beschäftigt. Normalerweise enden Sie mit Iterator[Array[String]]. Dann verwenden Sie Iterator.map oder collect, um jede Array[String] in Ihre Tupel zu transformieren, die sich mit Typkonvertierungsfehlern befassen. Wenn Sie die Eingabe bearbeiten müssen, ohne alle im Speicher zu laden, arbeiten Sie dann weiter mit dem Iterator, andernfalls können Sie in eine Vector oder List konvertieren und den Eingabestream schließen.

So kann es wie folgt aussehen:

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 

Je nachdem, wie Sie mit Fehlern umgehen müssen, Sie Left für Fehler und Right Erfolgs Tupel zurückgeben kann, die Fehler aus den richtigen Zeilen zu trennen. Außerdem wickle ich manchmal all dies unter Verwendung scala-arm zum Schließen von Ressourcen. Also meine Daten möglicherweise in die resource.ManagedResource Monade verpackt, so dass ich die Eingabe aus mehreren Dateien verwenden kann.

Schließlich, obwohl Sie mit Tupeln arbeiten möchten, habe ich festgestellt, dass es in der Regel klarer ist, eine Fallklasse zu haben, die für das Problem geeignet ist, und dann eine Methode schreiben, die dieses Fallklassenobjekt aus einem Array[String] erstellt.

+0

Welche Vorteile bietet die Verwendung der Fallklasse? – mushroom

+1

Es gibt Namen für die Felder wie 'Person (Name: String, Alter: Int)'. Wenn Sie später darauf zugreifen müssen, können Sie 'p.name' anstelle von' t._1' verwenden. Es funktioniert gut zum Beispiel in 'rows.sortBy (_. Name)' – huynhjl

0

Ich baute meine eigene Idee, um das Endprodukt stark typisieren zu können, mehr als die Lesephase selbst..wie gesagt könnte besser als Stufe eins mit etwas wie Apache CSV gehandhabt werden, und Stufe 2 könnte sein, was ich habe erledigt. Hier ist der Code, den Sie willkommen sind. Die Idee ist, den CSVReader [T] mit Typ T zu typisieren. Bei der Konstruktion müssen Sie dem Leser auch ein Factor-Objekt vom Typ [T] liefern. Die Idee dabei ist, dass die Klasse selbst (oder in meinem Beispiel ein Hilfsobjekt) das Konstruktionsdetail entscheidet und dieses somit vom eigentlichen Lesen entkoppelt. Sie könnten implizite Objekte verwenden, um den Helper herumzuführen, aber ich habe das hier noch nicht gemacht. Der einzige Nachteil besteht darin, dass jede CSV-Zeile vom gleichen Klassentyp sein muss, aber Sie können dieses Konzept nach Bedarf erweitern.

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 

neben dem Beispiel Helper Factory und Beispiel "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 
} 

Beispiel CSV (Tab getrennt .. Möglicherweise müssen reparieren, wenn Sie von einem Editor kopieren)

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 

Und schließlich der Schreiber (beachten Sie die Factory-Methoden erfordern dies auch mit "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 

    } 
1

ich eine stark typisierte CSV Helfer für Scala erstellt haben, genannt object-csv. Es ist kein vollwertiges Framework, aber es kann leicht angepasst werden. Mit ihm können Sie dies tun:

val peopleFromCSV = readCSV[Person](fileName) 

Wo Person ist Fall-Klasse, die wie folgt definiert:

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

Lesen Sie mehr darüber in GitHub, oder in meinem blog post darüber.

0

Sie können kantan.csv verwenden, die mit genau diesem Zweck konzipiert ist.

Stellen Sie folgende Eingabe haben:

1,Foo,2.0 
2,Bar,false 

kantan.csv verwenden, können Sie den folgenden Code schreiben, um es zu analysieren:

import kantan.csv.ops._ 

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

Und Sie haben einen Iterator, wo jeder Eintrag ist vom Typ (Int, String, Either[Float, Boolean]). Beachten Sie das Bit, bei dem die letzte Spalte in Ihrer CSV-Datei mehrere Typen umfassen kann. Dies wird jedoch bequem mit Either gehandhabt.

Dies ist alles auf eine völlig typsichere Art und Weise, keine Reflektion beteiligt, zur Kompilierzeit validiert.

Je nachdem, wie weit in den Kaninchenbau Sie bereit sind zu gehen, gibt es auch ein shapeless Modul für die automatisierte Fallklasse und Summen Typ Ableitung, sowie Unterstützung für scalaz und cats Typen und Typklassen.

Volle Offenbarung: Ich bin der Autor von kantan.csv.

Verwandte Themen