2013-06-15 13 views
8

Vorrei accedere ai file CSV in scala in modo fortemente digitato. Ad esempio, mentre leggo ogni riga del csv, viene automaticamente analizzata e rappresentata come una tupla con i tipi appropriati. Potrei specificare i tipi in anticipo in una sorta di schema che viene passato al parser. Ci sono delle librerie che esistono per fare questo? In caso contrario, come potrei implementare questa funzionalità da solo?Accesso fortemente criptato a csv in scala?

risposta

12

product-collections sembra essere una buona misura per le vostre esigenze:

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 utilizza opencsv sotto il cofano.

A CollSeq3 è un IndexedSeq[Product3[T1,T2,T3]] e anche un Product3[Seq[T1],Seq[T2],Seq[T3]] con un po 'di zucchero. Sono l'autore di product-collections.

Ecco a link to the io page of the scaladoc

Product3 è essenzialmente una tupla di arità 3.

+0

Mi piace il modo in cui appare, ma sto cercando di capire come funziona. Non capisco davvero cosa sta succedendo in CsvParser.scala.template. Quali sono questi modelli nella sezione standard? – mushroom

+0

Il modello viene elaborato da http://github.com/marklister/sbt-boilerplate che genera un file scala. Se si crea il progetto, è possibile vedere i risultati nella directory target/scala-2.10/src_managed. È esattamente lo stesso modo in cui Tuple1 ... Tuple22 di scala sembra funzionare. CsvParsers esiste per le entità da 1 a 22 e il compilatore seleziona quello corretto per la firma del tipo (schema) che fornisci. –

+0

Esiste un metodo consigliato per gestire valori o valori nulli che non vengono analizzati? Sarebbe bello se potessi dare Option [T] come parametro di tipo; potrebbe tentare di analizzarlo come una T e dare una Nessuna se non è riuscita a analizzare o era vuota. – mushroom

-1

Se si conosce il il # e tipi di campi, forse come questo ?:

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

Ciò è reso più complicato di quello che dovrebbe a causa delle regole di quoting non banali per CSV. Probabilmente dovresti iniziare con un parser CSV esistente, ad es. OpenCSV o uno dei progetti chiamato scala-csv. (Ci sono atleastthree.)

Quindi si finisce con una sorta di raccolta di raccolte di stringhe. Se non hai bisogno di leggere rapidamente file CSV di grandi dimensioni, puoi semplicemente provare ad analizzare ogni riga in ognuno dei tuoi tipi e prendere la prima che non genera un'eccezione. Ad esempio,

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

Se si ha bisogno di analizzare loro abbastanza velocemente e non si sa quello che potrebbe essere lì, probabilmente si dovrebbe utilizzare una sorta di corrispondenza (ad esempio espressioni regolari) sulle singole voci. In entrambi i casi, se c'è qualche possibilità di errore, probabilmente si desidera utilizzare Try o Option o qualche errore di pacchetto.

2

Se il contenuto contiene virgolette doppie per racchiudere altre virgolette doppie, virgole e nuove righe, utilizzerei sicuramente una libreria come opencsv che gestisce correttamente i caratteri speciali. In genere si finisce con Iterator[Array[String]]. Quindi usi Iterator.map o collect per trasformare ogni Array[String] nelle tue tuple che si occupano degli errori di conversione dei tipi lì. Se è necessario elaborare l'input senza caricare tutto in memoria, si continua a lavorare con l'iteratore, altrimenti è possibile convertire in un Vector o List e chiudere il flusso di input.

Così può assomigliare a questo:

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 

A seconda di come si deve fare con gli errori, è possibile tornare Left per gli errori e per Right tuple di successo per separare gli errori dalle righe corrette. Inoltre, a volte avvolgo tutto questo usando scala-arm per chiudere le risorse. Quindi i miei dati potrebbero essere inclusi nella monade resource.ManagedResource in modo da poter utilizzare input provenienti da più file.

Infine, anche se si desidera lavorare con le tuple, ho trovato che di solito è più chiaro avere una classe di case che è appropriata per il problema e quindi scrivere un metodo che crea quell'oggetto case class da un Array[String].

+0

Quali sono i vantaggi dell'utilizzo della classe case? – mushroom

+1

Fornisce nomi ai campi come 'Persona (nome: String, età: Int)'. Quindi più tardi, quando devi accedervi, puoi fare "p.name" piuttosto che "t._1". Funziona bene ad esempio in 'rows.sortBy (_. Name)' – huynhjl

0

Ho costruito la mia idea per digitare fortemente il prodotto finale, più che la fase di lettura stessa .. che come indicato potrebbe essere meglio gestito come uno stadio con qualcosa come Apache CSV, e lo stage 2 potrebbe essere quello che ho fatto. Ecco il codice sei il benvenuto. L'idea è di digitare il CSVReader [T] con il tipo T .. dopo la costruzione, è necessario fornire al lettore anche un oggetto Fattore di Tipo [T]. L'idea qui è che la classe stessa (o nel mio esempio un oggetto helper) decide i dettagli della costruzione e quindi la disaccoppia dalla lettura vera e propria. Potresti usare oggetti impliciti per passare l'aiutante, ma non l'ho fatto qui. L'unico lato negativo è che ogni riga del CSV deve essere dello stesso tipo di classe, ma è possibile espandere questo concetto secondo necessità.

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 

prossimo l'esempio Helper fabbrica e l'esempio "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 
} 

Esempio CSV (scheda separata .. potrebbe aver bisogno di riparare se si copia da un editor)

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 

E infine lo scrittore (notare che i metodi di fabbrica richiedono anche questo con "creatore"

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

Ho creato un aiutante CSV fortemente tipizzato per Scala, chiamato object-csv. Non è un quadro completo, ma può essere regolato facilmente. Con esso si può fare questo:

val peopleFromCSV = readCSV[Person](fileName) 

Dove persona è di classe caso, definito in questo modo:

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

Per saperne di più su di esso in GitHub, o nel mio blog post su di esso.

0

È possibile utilizzare kantan.csv, progettato appositamente per questo scopo.

Immaginate di avere il seguente testo:

1,Foo,2.0 
2,Bar,false 

Utilizzando kantan.csv, si potrebbe scrivere il seguente codice di analizzarlo:

import kantan.csv.ops._ 

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

E si otterrebbe un iteratore in cui ogni voce è di tipo (Int, String, Either[Float, Boolean]). Nota il bit in cui l'ultima colonna nel tuo CSV può essere di più di un tipo, ma questo è convenientemente gestito con Either.

Questo è tutto fatto in un modo completamente sicuro, senza alcuna riflessione, convalidato in fase di compilazione.

A seconda di quanto in basso nella tana del coniglio che sei disposto ad andare, c'è anche un modulo per la classe shapeless automatizzata caso e il tipo di somma di derivazione, così come il supporto per scalaz e cats tipi e classi di tipo.

Full disclosure: Sono l'autore di kantan.csv.