从 Scala 中的 CSV 列中发现类型

discover type from CSV columns in scala

我想将具有 headers 但列号未知的通用 CSV 文件读取到类型化结构中。我的问题有点像 Strongly typed access to csv in scala? 但事实上我没有模式可以传递给解析器...

直到现在,我一直在使用 Jackson CSV 映射器将每一行读取为 Map[String,String],并且运行良好。

import com.fasterxml.jackson.module.scala.DefaultScalaModule

def genericStringIterator(input: InputStream): Iterator[Map[String, String]] = {

    val mapper = new CsvMapper()

    mapper.registerModule(DefaultScalaModule)

    val schema = CsvSchema.emptySchema.withHeader

    val iterator = mapper
      .readerFor(classOf[Map[String, String]])
      .`with`(schema)
      .readValues[Map[String, String]](input)

    iterator.asScala
  }

现在,我们需要键入该字段,因此 4.2 将是一个双精度型,但“4.2”仍然是一个字符串。

我们在项目中到处都在使用 play-json,所以我知道 JsValue 已经为类似的通用内容提供了很好的类型推断。

因为 paly-json 它也是基于杰克逊的,所以我认为拥有类似

的东西会很棒
import play.api.libs.json.jackson.PlayJsonModule


def genericStringIterator(input: InputStream): Iterator[JsValue] = {

    val mapper = new CsvMapper()

    mapper.registerModule(PlayJsonModule)

    val schema = CsvSchema.emptySchema.withHeader

    val iterator = mapper
      .readerFor(classOf[JsValue])
      .`with`(schema)
      .readValues[JsValue](input)

    iterator.asScala
  }

但是当我尝试以前的代码时,出现异常:

   val iterator = CSV.genericAnyIterator(input(
      """foo,bar,baz
        |"toto",42,43
        |"tata",,45
        | titi,87,88
        |"tutu",,
        |""".stripMargin))

    iterator
      .foreach { a =>
        println(a)
      }
java.lang.RuntimeException: We should be reading map, something got wrong
    at play.api.libs.json.jackson.JsValueDeserializer.deserialize(JacksonJson.scala:165)
    at play.api.libs.json.jackson.JsValueDeserializer.deserialize(JacksonJson.scala:128)
    at play.api.libs.json.jackson.JsValueDeserializer.deserialize(JacksonJson.scala:123)
    at com.fasterxml.jackson.databind.MappingIterator.nextValue(MappingIterator.java:277)
    at com.fasterxml.jackson.databind.MappingIterator.next(MappingIterator.java:192)
    at scala.collection.convert.Wrappers$JIteratorWrapper.next(Wrappers.scala:40)
    at scala.collection.Iterator.foreach(Iterator.scala:929)
    at scala.collection.Iterator.foreach$(Iterator.scala:929)
    at scala.collection.AbstractIterator.foreach(Iterator.scala:1417)
    at my.company.csv.CSVSpec$$anon.<init>(CSVSpec.scala:240)

我做错了什么吗?

我不在乎最终有一个 play-json JsValue,任何具有通用类型字段的 Json 结构都可以。我可以使用另一个库吗?对于我发现的,所有其他库都是基于预先给 CSV Reader 的映射,对我来说重要的是能够从 CSV 中推断出类型。

好吧,我懒得想找一些开箱即用的东西:) 其实自己实现也很简单

我去寻找其他语言的库来进行这种推断(JS 中的 PapaParse,python 中的 Pandas),并发现他们正在做一个测试和重试优先于猜类型。

所以我自己实现了,效果很好!

这里是:

def genericAnyIterator(input: InputStream): Iterator[JsValue] = {

    // the result of former code, mapping to Map[String,String]
    val strings = genericStringIterator(input)

    val decimalRegExp: Regex = "(\d*){1}(\.\d*){0,1}".r

    val jsValues: Iterator[Map[String, JsValue]] = strings.map { m =>
      m.mapValues {
        case "" => None
        case "false" | "FALSE" => Some(JsFalse)
        case "true" | "TRUE" => Some(JsTrue)
        case value@decimalRegExp(g1, g2) if !value.isEmpty => Some(JsNumber(value.toDouble))
        case "null" | "NULL" => Some(JsNull)
        case value@_ => Some(JsString(value))
      }
        .filter(_._2.isDefined)
        .mapValues(_.get)
    }
    jsValues.map(map => JsObject(map.toSeq))
  }

在测试中做的

 it should "read any csv in JsObject" in new WithInputStream {

    val iterator = CSV.genericAnyIterator(input(
      """foo,bar,baz
        |"toto",NaN,false
        |"tata",,TRUE
        |titi,87.79,88
        |"tutu",,null
        |"tete",5.,.5
        |""".stripMargin))

    val result: Seq[JsValue] = iterator.toSeq

    result should be(Stream(
      Json.obj("foo" -> "toto", "bar" -> "NaN", "baz" -> false)
      , Json.obj("foo" -> "tata",  "baz" -> true)
      , Json.obj("foo" -> "titi", "bar" -> 87.79, "baz" -> 88)
      , Json.obj("foo" -> "tutu",  "baz" -> JsNull)
      , Json.obj("foo" -> "tete", "bar" -> 5, "baz" -> 0.5)
    ) )
  }