Я хотел бы получить доступ к файлам csv в scala строго типизированным образом. Например, когда я читаю каждую строку csv, она автоматически анализируется и представляется как кортеж с соответствующими типами. Я мог заранее указать типы в какой-то схеме, которая передается парсеру. Существуют ли библиотеки, которые существуют для этого? Если нет, как я могу реализовать эту функцию самостоятельно?Сильно типизированный доступ к csv в scala?
ответ
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.
Если вы знаете # и типы полей, может быть, как это ?:
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))
}
Это делается более сложным, чем это следовало из нетривиальных котирования правил 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
или что-то вроде ошибок пакета.
Если ваш контент содержит двойные кавычки, чтобы заключить другие двойные кавычки, запятые и новые строки, я определенно использовал бы библиотеку, такую как 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]
.
Я построил свою собственную идею, чтобы строго придумать конечный продукт, больше, чем сама сцена считывания .., которая, как указывалось, может быть лучше обработана в качестве этапа с чем-то вроде 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
}
Я создал сильно типизированных помощник CSV для Scala, называется object-csv. Это не полноценная структура, но ее можно легко отрегулировать. С его помощью вы можете сделать это:
val peopleFromCSV = readCSV[Person](fileName)
Где Person является случай класса, определяется следующим образом:
case class Person (name: String, age: Int, salary: Double, isNice:Boolean = false)
Вы можете использовать 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.
Мне нравится, как это выглядит, но я пытаюсь выяснить, как это работает. Я не понимаю, что происходит в CsvParser.scala.template. Каковы эти шаблоны в разделе шаблонов? – mushroom
Шаблон обрабатывается http://github.com/marklister/sbt-boilerplate, который генерирует файл scala. Если вы строите проект, вы можете увидеть результаты в каталоге target/scala-2.10/src_managed. Это точно так же, как собственный Tuple1 Scala ... Tuple22, похоже, работает. CsvParsers существуют для ячеек с 1 по 22, и компилятор выбирает правильный для сигнатуры типа (схемы), которую вы предоставляете. –
Есть ли рекомендуемый способ обработки нулевых значений или значений, которые не могут быть проанализированы? Было бы неплохо, если бы я мог указать Option [T] в качестве параметра типа; он может попытаться разобрать его как T и дать None, если он не смог разобрать или был пуст. – mushroom