Come analizzare JSON in Scala utilizzando le classi Scala standard?


113

Sto usando la build in classe JSON in Scala 2.8 per analizzare il codice JSON. Non voglio usare Liftweb o qualsiasi altro a causa della riduzione al minimo delle dipendenze.

Il modo in cui lo sto facendo sembra troppo imperativo, c'è un modo migliore per farlo?

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

Risposte:


130

Questa è una soluzione basata su estrattori che eseguiranno il cast di classe:

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

All'inizio del ciclo for racchiudo artificialmente il risultato in una lista in modo che produca una lista alla fine. Quindi nel resto del ciclo for utilizzo il fatto che i generatori (using <-) e le definizioni di valore (using =) faranno uso dei metodi unapply.

(Risposta precedente modificata: controlla la cronologia delle modifiche se sei curioso)


Scusa se riesco a trovare un vecchio post, ma qual è il significato del primo Some (M (map)) del loop? Capisco che la M (map) stia estraendo la mappa nella variabile "map", ma per quanto riguarda alcuni?
Federico Bonelli

1
@FedericoBonelli, JSON.parseFullritorna Option[Any], quindi inizia con List(None)o List(Some(any)). È Someattivo per la corrispondenza del modello Option.
huynhjl

21

Questo è il modo in cui eseguo la corrispondenza del pattern:

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

puoi fare un esempio del tuo jsonStr, non funziona con l'esempio sopra di jsonStr
priya khokher

Potrebbe valere la pena pubblicare una domanda sul tuo problema. Al momento non ho Scala installato sulla mia macchina, quindi non ho una stringa JSON pronta.
Matthias Braun

12

Mi piace la risposta di @ huynhjl, mi ha portato sulla strada giusta. Tuttavia, non è eccezionale per gestire le condizioni di errore. Se il nodo desiderato non esiste, ottieni un'eccezione cast. L'ho adattato leggermente per utilizzarlo Optionper gestirlo meglio.

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

Naturalmente, questo non gestisce gli errori tanto quanto li evita. Questo produrrà un elenco vuoto se manca uno qualsiasi dei nodi json. Puoi usare a matchper verificare la presenza di un nodo prima di agire ...

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

3
Penso che CC inapply possa essere notevolmente semplificato def unapply(a: Option[Any]): Option[T] = a.map(_.asInstanceOf[T]).
Suma

Scala 2.12 sembra aver bisogno di ';' prima delle righe con "=" nella comprensione.
akauppi

Per me, il codice più in alto non "ha prodotto un elenco vuoto se manca uno qualsiasi dei nodi json", ma ha dato un MatchErrorinvece (Scala 2.12). Necessario per avvolgere il for in un blocco try / catch per quello. Qualche idea migliore?
akauppi

7

Ho provato alcune cose, favorendo la corrispondenza dei modelli come un modo per evitare il casting, ma ho avuto problemi con la cancellazione del tipo sui tipi di raccolta.

Il problema principale sembra essere che il tipo completo del risultato dell'analisi rispecchia la struttura dei dati JSON ed è macchinoso o impossibile da dichiarare completamente. Immagino che sia per questo che viene utilizzato Any per troncare le definizioni del tipo. L'uso di Any porta alla necessità di casting.

Ho violato qualcosa di seguito che è conciso ma è estremamente specifico per i dati JSON impliciti nel codice nella domanda. Qualcosa di più generale sarebbe più soddisfacente ma non sono sicuro che sarebbe molto 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}

Mi piace che l'utente di implicit's lo estragga.
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)) )

3
Questo è deprecato nell'ultima scala, Unbundled. Qualche idea su come usarlo allora?
Sanket_patil

4

Puoi fare così! Codice JSON molto facile da analizzare: 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)
  }

}

2

Questo è il modo in cui eseguo la Scala Parser Combinator Library:

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)

  }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.