Jak analizować JSON w Scali przy użyciu standardowych klas Scala?

113

Używam kompilacji w klasie JSON w Scala 2,8 do analizowania kodu JSON. Nie chcę korzystać z Liftweb jednego lub innego ze względu na zminimalizowanie zależności.

Sposób, w jaki to robię, wydaje się zbyt konieczny, czy jest lepszy sposób, aby to zrobić?

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
źródło

Odpowiedzi:

130

To rozwiązanie bazujące na ekstraktorach, które wykonają klasę rzutów:

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

Na początku pętli for sztucznie zawijam wynik w listę, tak aby na końcu była lista. W dalszej części pętli for wykorzystuję fakt, że generatory (używające <-) i definicje wartości (używające =) będą korzystać z metod niezastosowanych.

(Starsza odpowiedź została usunięta - sprawdź historię edycji, jeśli jesteś ciekawy)

huynhjl
źródło
Przepraszam, że odkopuję stary post, ale jakie jest znaczenie pierwszego Some (M (mapa)) w pętli? Rozumiem, że M (mapa) wyodrębnia mapę do zmiennej „mapa”, ale co z Some?
Federico Bonelli,
1
@FedericoBonelli, JSON.parseFullzwraca Option[Any], więc zaczyna się od List(None)lub List(Some(any)). Służy Somedo dopasowania wzorca Option.
huynhjl
21

Oto sposób dopasowania wzorca:

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
źródło
czy możesz podać przykład swojego jsonStr, nie działa z powyższym przykładem jsonStr
priya khokher
Warto zadać własne pytanie dotyczące Twojego problemu. Obecnie nie mam zainstalowanej Scali na moim komputerze, więc nie mam gotowego ciągu JSON.
Matthias Braun,
12

Podoba mi się odpowiedź @ huynhjl, poprowadziła mnie właściwą ścieżką. Jednak nie radzi sobie dobrze z błędami. Jeśli żądany węzeł nie istnieje, otrzymasz wyjątek rzutowania. Dostosowałem to nieco, Optionaby lepiej sobie z tym poradzić.

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

Oczywiście nie tyle obsługuje to błędy, ile pozwala na ich uniknięcie. Spowoduje to utworzenie pustej listy, jeśli brakuje któregokolwiek z węzłów json. Możesz użyć a, matchaby sprawdzić obecność węzła przed wykonaniem ...

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
źródło
3
Myślę, że CC unapply można znacznie uprościć do def unapply(a: Option[Any]): Option[T] = a.map(_.asInstanceOf[T]).
Suma
Wydaje się, że Scala 2.12 potrzebuje ';' przed wierszami z „=” w celu zrozumienia.
akauppi
W moim przypadku kod znajdujący się na samej górze nie „dał pustej listy, jeśli brakuje któregokolwiek z węzłów json”, ale MatchErrorzamiast tego dał (Scala 2.12). W tym celu konieczne było zawinięcie for w blok try / catch. Jakieś fajniejsze pomysły?
akauppi
7

Wypróbowałem kilka rzeczy, preferując dopasowywanie wzorców jako sposób na uniknięcie rzutowania, ale napotkałem problemy z usuwaniem typów w typach kolekcji.

Wydaje się, że głównym problemem jest to, że pełny typ wyniku analizy odzwierciedla strukturę danych JSON i jest albo uciążliwy, albo niemożliwy do pełnego określenia. Wydaje mi się, że dlatego Any jest używany do obcinania definicji typów. Używanie Any prowadzi do konieczności odlewania.

Zhakowałem poniżej coś, co jest zwięzłe, ale jest bardzo specyficzne dla danych JSON sugerowanych przez kod w pytaniu. Coś bardziej ogólnego byłoby bardziej satysfakcjonujące, ale nie jestem pewien, czy byłoby to bardzo eleganckie.

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
źródło
Lubię, gdy użytkownik implicit's to wyodrębnia.
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
źródło
3
Jest to przestarzałe w najnowszej scali, Unbundled. Masz jakiś pomysł, jak to wykorzystać?
Sanket_patil
4

Możesz to zrobić! Bardzo łatwy do przeanalizowania kod 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
źródło
2

Oto sposób, w jaki robię bibliotekę 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
źródło