¿Cómo analizar JSON en Scala usando clases estándar de Scala?

113

Estoy usando la clase build in JSON en Scala 2.8 para analizar el código JSON. No quiero usar Liftweb ni ningún otro debido a la minimización de dependencias.

La forma en que lo estoy haciendo parece demasiado imperativa, ¿hay una mejor manera de hacerlo?

import scala.util.parsing.json._
...
val json:Option[Any] = JSON.parseFull(jsonString)
val map:Map[String,Any] = json.get.asInstanceOf[Map[String, Any]]
val languages:List[Any] = map.get("languages").get.asInstanceOf[List[Any]]
languages.foreach( langMap => {
val language:Map[String,Any] = langMap.asInstanceOf[Map[String,Any]]
val name:String = language.get("name").get.asInstanceOf[String]
val isActive:Boolean = language.get("is_active").get.asInstanceOf[Boolean]
val completeness:Double = language.get("completeness").get.asInstanceOf[Double]
}
Phil
fuente

Respuestas:

130

Esta es una solución basada en extractores que harán el reparto de clases:

class CC[T] { def unapply(a:Any):Option[T] = Some(a.asInstanceOf[T]) }

object M extends CC[Map[String, Any]]
object L extends CC[List[Any]]
object S extends CC[String]
object D extends CC[Double]
object B extends CC[Boolean]

val jsonString =
    """
      {
        "languages": [{
            "name": "English",
            "is_active": true,
            "completeness": 2.5
        }, {
            "name": "Latin",
            "is_active": false,
            "completeness": 0.9
        }]
      }
    """.stripMargin

val result = for {
    Some(M(map)) <- List(JSON.parseFull(jsonString))
    L(languages) = map("languages")
    M(language) <- languages
    S(name) = language("name")
    B(active) = language("is_active")
    D(completeness) = language("completeness")
} yield {
    (name, active, completeness)
}

assert( result == List(("English",true,2.5), ("Latin",false,0.9)))

Al comienzo del ciclo for, envuelvo artificialmente el resultado en una lista para que produzca una lista al final. Luego, en el resto del ciclo for, uso el hecho de que los generadores (usando <-) y las definiciones de valor (usando =) harán uso de los métodos de no aplicar.

(Respuesta anterior eliminada; verifique el historial de edición si tiene curiosidad)

huynhjl
fuente
Lamento desenterrar una publicación antigua, pero ¿cuál es el significado del primer Some (M (mapa)) en el bucle? Entiendo que M (mapa) está extrayendo el mapa a la variable "mapa", pero ¿qué pasa con Some?
Federico Bonelli
1
@FedericoBonelli, JSON.parseFullregresa Option[Any], entonces comienza con List(None)o List(Some(any)). El Somees para hacer coincidir patrones Option.
huynhjl
21

Esta es la forma en que hago la coincidencia de patrones:

val result = JSON.parseFull(jsonStr)
result match {
  // Matches if jsonStr is valid JSON and represents a Map of Strings to Any
  case Some(map: Map[String, Any]) => println(map)
  case None => println("Parsing failed")
  case other => println("Unknown data structure: " + other)
}
Matthias Braun
fuente
¿Puede dar un ejemplo de su jsonStr, no funciona con el ejemplo anterior de jsonStr?
priya khokher
Podría valer la pena publicar una pregunta propia sobre su problema. Actualmente no tengo Scala instalado en mi máquina, así que no tengo una cadena JSON lista.
Matthias Braun
12

Me gusta la respuesta de @huynhjl, me llevó por el camino correcto. Sin embargo, no es bueno para manejar condiciones de error. Si el nodo deseado no existe, obtiene una excepción de transmisión. He adaptado esto un poco para utilizarlo Optiony manejarlo mejor.

class CC[T] {
  def unapply(a:Option[Any]):Option[T] = if (a.isEmpty) {
    None
  } else {
    Some(a.get.asInstanceOf[T])
  }
}

object M extends CC[Map[String, Any]]
object L extends CC[List[Any]]
object S extends CC[String]
object D extends CC[Double]
object B extends CC[Boolean]

for {
  M(map) <- List(JSON.parseFull(jsonString))
  L(languages) = map.get("languages")
  language <- languages
  M(lang) = Some(language)
  S(name) = lang.get("name")
  B(active) = lang.get("is_active")
  D(completeness) = lang.get("completeness")
} yield {
  (name, active, completeness)
}

Por supuesto, esto no maneja los errores tanto como los evita. Esto producirá una lista vacía si falta alguno de los nodos json. Puede utilizar a matchpara comprobar la presencia de un nodo antes de actuar ...

for {
  M(map) <- Some(JSON.parseFull(jsonString))
} yield {
  map.get("languages") match {
    case L(languages) => {
      for {
        language <- languages
        M(lang) = Some(language)
        S(name) = lang.get("name")
        B(active) = lang.get("is_active")
        D(completeness) = lang.get("completeness")
      } yield {
        (name, active, completeness)
      }        
    }
    case None => "bad json"
  }
}
murrayju
fuente
3
Creo que CC unapply se puede simplificar significativamente a def unapply(a: Option[Any]): Option[T] = a.map(_.asInstanceOf[T]).
Suma
Scala 2.12 parece necesitar ';' antes de las líneas con '=' en el para comprensión.
akauppi
Para mí, el código superior no "arrojó una lista vacía si falta alguno de los nodos json", sino que dio un MatchError(Scala 2.12). Necesitaba envolver el for en un bloque try / catch para eso. ¿Alguna idea mejor?
akauppi
7

Probé algunas cosas, favoreciendo la coincidencia de patrones como una forma de evitar la conversión, pero tuve problemas con el borrado de tipos en los tipos de colección.

El principal problema parece ser que el tipo completo del resultado del análisis refleja la estructura de los datos JSON y es engorroso o imposible de establecer por completo. Supongo que es por eso que se usa Any para truncar las definiciones de tipo. El uso de Any conduce a la necesidad de un casting.

He pirateado algo a continuación que es conciso pero es extremadamente específico para los datos JSON implícitos en el código de la pregunta. Algo más general sería más satisfactorio pero no estoy seguro de si sería muy elegante.

implicit def any2string(a: Any)  = a.toString
implicit def any2boolean(a: Any) = a.asInstanceOf[Boolean]
implicit def any2double(a: Any)  = a.asInstanceOf[Double]

case class Language(name: String, isActive: Boolean, completeness: Double)

val languages = JSON.parseFull(jstr) match {
  case Some(x) => {
    val m = x.asInstanceOf[Map[String, List[Map[String, Any]]]]

    m("languages") map {l => Language(l("name"), l("isActive"), l("completeness"))}
  }
  case None => Nil
}

languages foreach {println}
Don Mackenzie
fuente
Me gusta que el usuario de implícitos lo extraiga.
Phil
4
val jsonString =
  """
    |{
    | "languages": [{
    |     "name": "English",
    |     "is_active": true,
    |     "completeness": 2.5
    | }, {
    |     "name": "Latin",
    |     "is_active": false,
    |     "completeness": 0.9
    | }]
    |}
  """.stripMargin

val result = JSON.parseFull(jsonString).map {
  case json: Map[String, List[Map[String, Any]]] =>
    json("languages").map(l => (l("name"), l("is_active"), l("completeness")))
}.get

println(result)

assert( result == List(("English", true, 2.5), ("Latin", false, 0.9)) )
Yuriy Tumakha
fuente
3
Esto está en desuso en la última scala, Unbundled. ¿Alguna idea de cómo usarlo entonces?
Sanket_patil
4

¡Puedes hacerlo así! Muy fácil de analizar el código JSON: P

package org.sqkb.service.common.bean

import java.text.SimpleDateFormat

import org.json4s
import org.json4s.JValue
import org.json4s.jackson.JsonMethods._
//import org.sqkb.service.common.kit.{IsvCode}

import scala.util.Try

/**
  *
  */
case class Order(log: String) {

  implicit lazy val formats = org.json4s.DefaultFormats

  lazy val json: json4s.JValue = parse(log)

  lazy val create_time: String = (json \ "create_time").extractOrElse("1970-01-01 00:00:00")
  lazy val site_id: String = (json \ "site_id").extractOrElse("")
  lazy val alipay_total_price: Double = (json \ "alipay_total_price").extractOpt[String].filter(_.nonEmpty).getOrElse("0").toDouble
  lazy val gmv: Double = alipay_total_price
  lazy val pub_share_pre_fee: Double = (json \ "pub_share_pre_fee").extractOpt[String].filter(_.nonEmpty).getOrElse("0").toDouble
  lazy val profit: Double = pub_share_pre_fee

  lazy val trade_id: String = (json \ "trade_id").extractOrElse("")
  lazy val unid: Long = Try((json \ "unid").extractOpt[String].filter(_.nonEmpty).get.toLong).getOrElse(0L)
  lazy val cate_id1: Int = (json \ "cate_id").extractOrElse(0)
  lazy val cate_id2: Int = (json \ "subcate_id").extractOrElse(0)
  lazy val cate_id3: Int = (json \ "cate_id3").extractOrElse(0)
  lazy val cate_id4: Int = (json \ "cate_id4").extractOrElse(0)
  lazy val coupon_id: Long = (json \ "coupon_id").extractOrElse(0)

  lazy val platform: Option[String] = Order.siteMap.get(site_id)


  def time_fmt(fmt: String = "yyyy-MM-dd HH:mm:ss"): String = {
    val dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    val date = dateFormat.parse(this.create_time)
    new SimpleDateFormat(fmt).format(date)
  }

}
Echo Zeng
fuente
2

Esta es la forma en que hago la biblioteca Scala Parser Combinator:

import scala.util.parsing.combinator._
class ImprovedJsonParser extends JavaTokenParsers {

  def obj: Parser[Map[String, Any]] =
    "{" ~> repsep(member, ",") <~ "}" ^^ (Map() ++ _)

  def array: Parser[List[Any]] =
    "[" ~> repsep(value, ",") <~ "]"

  def member: Parser[(String, Any)] =
    stringLiteral ~ ":" ~ value ^^ { case name ~ ":" ~ value => (name, value) }

  def value: Parser[Any] = (
    obj
      | array
      | stringLiteral
      | floatingPointNumber ^^ (_.toDouble)
      |"true"
      |"false"
    )

}
object ImprovedJsonParserTest extends ImprovedJsonParser {
  def main(args: Array[String]) {
    val jsonString =
    """
      {
        "languages": [{
            "name": "English",
            "is_active": true,
            "completeness": 2.5
        }, {
            "name": "Latin",
            "is_active": false,
            "completeness": 0.9
        }]
      }
    """.stripMargin


    val result = parseAll(value, jsonString)
    println(result)

  }
}
hmehdi
fuente