2013-06-15 3 views
8

Я хотел бы получить доступ к файлам csv в scala строго типизированным образом. Например, когда я читаю каждую строку csv, она автоматически анализируется и представляется как кортеж с соответствующими типами. Я мог заранее указать типы в какой-то схеме, которая передается парсеру. Существуют ли библиотеки, которые существуют для этого? Если нет, как я могу реализовать эту функцию самостоятельно?Сильно типизированный доступ к csv в scala?

ответ

12

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 под капотом.

A CollSeq3 является IndexedSeq[Product3[T1,T2,T3]], а также Product3[Seq[T1],Seq[T2],Seq[T3]] с небольшим количеством сахара. Я автор product-collections.

Вот a link to the io page of the scaladoc

Product3 по существу кортеж арностью 3.

+0

Мне нравится, как это выглядит, но я пытаюсь выяснить, как это работает. Я не понимаю, что происходит в CsvParser.scala.template. Каковы эти шаблоны в разделе шаблонов? – mushroom

+0

Шаблон обрабатывается http://github.com/marklister/sbt-boilerplate, который генерирует файл scala. Если вы строите проект, вы можете увидеть результаты в каталоге target/scala-2.10/src_managed. Это точно так же, как собственный Tuple1 Scala ... Tuple22, похоже, работает. CsvParsers существуют для ячеек с 1 по 22, и компилятор выбирает правильный для сигнатуры типа (схемы), которую вы предоставляете. –

+0

Есть ли рекомендуемый способ обработки нулевых значений или значений, которые не могут быть проанализированы? Было бы неплохо, если бы я мог указать Option [T] в качестве параметра типа; он может попытаться разобрать его как T и дать None, если он не смог разобрать или был пуст. – mushroom

-1

Если вы знаете # и типы полей, может быть, как это ?:

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

Это делается более сложным, чем это следовало из нетривиальных котирования правил 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) 
    }) 
} 

Если вам нужно разобрать их достаточно быстро, и вы не знаете, что там может быть, вы, вероятно, следует использовать какие-то совпадения (например, регулярных выражений) по отдельным позициям. В любом случае, если есть вероятность ошибки, вы, вероятно, захотите использовать Try или Option или что-то вроде ошибок пакета.

2

Если ваш контент содержит двойные кавычки, чтобы заключить другие двойные кавычки, запятые и новые строки, я определенно использовал бы библиотеку, такую ​​как opencsv, которая правильно обрабатывает специальные символы. Обычно вы получаете Iterator[Array[String]]. Затем вы используете Iterator.map или collect, чтобы преобразовать каждый Array[String] в свои кортежи, где есть ошибки преобразования типов. Если вам нужно обработать ввод без загрузки всего в память, вы продолжаете работать с итератором, иначе вы можете преобразовать в Vector или List и закрыть входной поток.

Так что, может выглядеть следующим образом:

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, поэтому я могу использовать ввод, поступающий из нескольких файлов.

Наконец, хотя вы хотите работать с кортежами, я обнаружил, что, как правило, яснее иметь класс case, подходящий для проблемы, а затем написать метод, который создает объект класса case из Array[String].

+0

В чем преимущества использования класса корпуса? – mushroom

+1

Он дает имена полям, например 'Person (name: String, age: Int)'. Поэтому, когда вам нужно получить к нему доступ, вы можете сделать 'p.name', а не' t._1'. Он хорошо работает, например, в 'rows.sortBy (_. Name)' – huynhjl

0

Я построил свою собственную идею, чтобы строго придумать конечный продукт, больше, чем сама сцена считывания .., которая, как указывалось, может быть лучше обработана в качестве этапа с чем-то вроде Apache CSV, а на этапе 2 может быть то, что у меня есть сделанный. Вот код, к которому вы можете присоединиться. Идея состоит в том, чтобы придать тип CSVReader [T] с типом T .. при построении, вы должны предоставить читателю объект Factor Type [T]. Идея здесь заключается в том, что сам класс (или в моем примере вспомогательный объект) решает детали конструкции и тем самым отделяет это от фактического чтения. Вы можете использовать неявные объекты, чтобы передать помощника, но я этого не сделал. Единственным недостатком является то, что каждая строка 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 

    } 
1

Я создал сильно типизированных помощник CSV для Scala, называется object-csv. Это не полноценная структура, но ее можно легко отрегулировать. С его помощью вы можете сделать это:

val peopleFromCSV = readCSV[Person](fileName) 

Где Person является случай класса, определяется следующим образом:

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

Подробнее об этом в GitHub, или в моем blog post об этом.

0

Вы можете использовать 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 модуль для автоматизированного класса случая и типа суммы вывода, а также поддержка scalaz и cats типов и классов типов.

Полное раскрытие информации: Я автор kantan.csv.