使用Jerkson在Scala中处理JSON

介绍

先前的教程介绍了Scala中的基本XML处理 ,但是正如我指出的那样,XML如今并不是数据序列化的主要选择。 取而代之的是, JSON(JavaScript对象表示法)被更广泛地用于数据交换,部分原因是它不那么冗长,并且可以更好地捕获用于定义许多对象的核心数据结构(例如列表和地图)。 它最初是为使用JavaScript设计的,但事实证明它是一种与语言无关的格式,非常有效。 它的一个非常好的功能是,它很容易将Java和Scala之类的语言中定义的对象转换为JSON,然后再次转换为JSON,这将在本教程中进行演示。 如果适当地对齐了类定义和JSON结构,那么在给定合适的JSON处理库的情况下,这种转换非常简单。

在本教程中,我将使用Jerkson库介绍Scala中的基本JSON处理,该库本质上是围绕Jackson库(用Java编写)的Scala包装器。 请注意, lift-json之类的其他库是完全不错的选择,但由于Jackson的性能Jerkson似乎在流JSON方面具有一些效率优势 。 当然,由于Scala与Java配合良好,因此您可以直接使用任何您喜欢的基于JVM的JSON库,包括Jackson。

这篇文章还展示了如何快速开始使用SBT,它将使您能够轻松地将第三方库作为依赖项进行访问,并开始编写使用它们的代码,并可以使用SBT对其进行编译。

注意 :作为“ Jason”,我坚持认为JSON应发音为Jay-SAHN (在第二个音节上加重音)以区别于名称。 :)

设定

在这样的教程上下文中使用Jerkson库的一种简单方法是让读者建立一个新的SBT项目,将Jerkson声明为依赖项,然后使用SBT的控制台操作启动Scala REPL。 这将对获取外部库和设置类路径的过程进行分类,以便可以在SBT启动的Scala REPL中使用它们。 请按照本节中的说明进行操作。

注意 :如果您已经在使用Scalabha 0.2.5版(或更高版本),请跳到本节的底部,以了解如何使用Scalabha的版本运行REPL。 另外,如果您有自己的现有项目,则当然可以将Jerkson添加为依赖项,根据需要导入其类并在常规编程设置中使用它。 然后,下面的示例将作为一些简单的配方供您在项目中使用。

首先,创建一个工作目录并下载SBT启动jar。

$ mkdir ~/json-tutorial
$ cd ~/json-tutorial/
$ wget http://typesafe.artifactoryonline.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.11.3/sbt-launch.jar

注意 :如果您的计算机上未安装wget ,则可以在浏览器中下载上述sbt-launch.jar文件,并将其移至〜/ json-tutorial目录。

现在,将以下内容另存为文件〜/ json-tutorial / build.sbt 。 请注意,在每个声明之间保持空行很重要。

name := 'json-tutorial'

version := '0.1.0 '

scalaVersion := '2.9.2'

resolvers += 'repo.codahale.com' at 'http://repo.codahale.com'

libraryDependencies += 'com.codahale' % 'jerkson_2.9.1' % '0.5.0'

然后将以下内容保存到文件〜/ json-tutorial / runSbt中

java -Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=384M -jar `dirname $0`/sbt-launch.jar '$@'

使该文件可执行并运行,它将显示SBT进行了大量工作,然后让您看到SBT提示。

$ cd ~/json-tutorial
$ chmod a+x runSbt
$ ./runSbt update
Getting org.scala-sbt sbt_2.9.1 0.11.3 ...
downloading http://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt_2.9.1/0.11.3/jars/sbt_2.9.1.jar ...
[SUCCESSFUL ] org.scala-sbt#sbt_2.9.1;0.11.3!sbt_2.9.1.jar (307ms)
...
... more stuff including getting the the Jerkson library ...
...
[success] Total time: 25 s, completed May 11, 2012 10:22:42 AM
$

此时,您应该回到Unix Shell中,现在我们可以使用SBT运行Scala REPL了。 重要的是,此REPL实例将在类路径中具有Jerkson库及其依赖项,以便我们可以导入所需的类。

./runSbt console
[info] Set current project to json-tutorial (in build file:/Users/jbaldrid/json-tutorial/)
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.9.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_31).
Type in expressions to have them evaluated.
Type :help for more information.

scala> import com.codahale.jerkson.Json._
import com.codahale.jerkson.Json._

如果没有进一步输出,则一切就绪。 如果情况不对(或者您在默认的Scala REPL中运行),则会看到类似以下内容。

scala> import com.codahale.jerkson.Json._
<console>:7: error: object codahale is not a member of package com
import com.codahale.jerkson.Json._

如果是这样,请尝试再次按照上面的说明进行操作,以确保您的设置与上面的设置完全相同。 但是,如果仍然遇到问题,另一种方法是获取Scalabha的0.2.5版本 (已经具有Jerkson作为依赖项),按照说明进行设置,然后运行以下命令。

$ cd $SCALABHA_DIR
$ scalabha build console

如果您只想看一些使用Jerkson作为API的示例并且不进行交互使用,那么完全没有必要进行SBT设置-继续阅读并根据需要修改示例。


处理一个简单的JSON示例

像往常一样,让我们​​从一个非常简单的示例开始,该示例显示JSON的一些基本属性。

{'foo':42'bar':['a','b','c'],'baz':{'x':1,'y':2}}

这描述了具有三个字段foobarbaz的数据结构。 foo字段的值是整数42bar的值是字符串列表,而baz的值是从字符串到整数的映射。 这些是语言无关(但通用)的类型。

我们首先考虑使用Jerkson的parse方法将这些值分别反序列化为Scala对象。 请记住,文件中的JSON是字符串,因此在所有这些情况下的输入都是字符串(有时,当JSON中带有引号时,我会使用三引号引起来的字符串)。 在每种情况下,我们都通过在参数前提供类型说明来告诉parse方法期望的类型。

scala> parse[Int]('42')
res0: Int = 42

scala> parse[List[String]]('''['a','b','c']''')
res1: List[String] = List(a, b, c)

scala> parse[Map[String,Int]]('''{ 'x': 1, 'y': 2 }''')
res2: Map[String,Int] = Map(x -> 1, y -> 2)

因此,在每种情况下,字符串表示形式都会转换为适当类型的Scala对象。 如果不确定类型是什么,或者例如我们知道List是异构的,则可以使用Any作为期望的类型。

scala> parse[Any]('42')
res3: Any = 42

scala> parse[List[Any]]('''['a',1]''')
res4: List[Any] = List(a, 1)

如果提供的期望类型不能被解析为这样,则会出现错误。

scala> parse[List[Int]]('''['a',1]''')
com.codahale.jerkson.ParsingException: Can not construct instance of int from String value 'a': not a valid Integer value
at [Source: java.io.StringReader@2bc5aea; line: 1, column: 2]
<...many more lines of stack trace...>

如何将所有属性和值一起解析? 如下将整个内容保存在变量simpleJson中。

scala> :paste

// Entering paste mode (ctrl-D to finish)

val simpleJson = '''{'foo': 42,
'bar': ['a','b','c'],
'baz': { 'x': 1, 'y': 2 }}'''

// Exiting paste mode, now interpreting.

simpleJson: java.lang.String =
{'foo': 42,
'bar': ['a','b','c'],
'baz': { 'x': 1, 'y': 2 }}

由于它是从字符串到不同类型的值的映射,因此我们能做的最好的事情是将其反序列化为Map [String,Any]

scala> val simple = parse[Map[String,Any]](simpleJson)
simple: Map[String,Any] = Map(bar -> [a, b, c], baz -> {x=1, y=2}, foo -> 42)

要使它们成为比Any更具体的类型,您需要将它们转换为适当的类型。

scala> val fooValue = simple('foo').asInstanceOf[Int]
fooValue: Int = 42

scala> val barValue = simple('bar').asInstanceOf[java.util.ArrayList[String]]
barValue: java.util.ArrayList[String] = [a, b, c]

scala> val bazValue = simple('baz').asInstanceOf[java.util.LinkedHashMap[String,Int]]
bazValue: java.util.LinkedHashMap[String,Int] = {x=1, y=2}

当然,您可能希望使用Scala类型 ,如果将隐式转换从Java类型导入到Scala类型 ,这很容易。

scala> import scala.collection.JavaConversions._
import scala.collection.JavaConversions._

scala> val barValue = simple('bar').asInstanceOf[java.util.ArrayList[String]].toList
barValue: List[String] = List(a, b, c)

scala> val bazValue = simple('baz').asInstanceOf[java.util.LinkedHashMap[String,Int]].toMap
bazValue: scala.collection.immutable.Map[String,Int] = Map(x -> 1, y -> 2)

瞧! 在Scala中使用Java库时,通常证明JavaConversions非常方便。


反序列化为用户定义的类型

尽管我们能够解析上面的简单JSON表达式,甚至可以将值转换为适当的类型,但是事情仍然有些笨拙。 幸运的是,如果您使用适当的字段定义了自己的案例类,则可以将其提供为期望的类型。 例如,这是一个简单的案例类,可以解决问题。

case class Simple(val foo: String, val bar: List[String], val baz: Map[String,Int])

显然,这具有所有正确的字段(变量名与JSON示例中的字段相同),并且变量具有我们希望它们具有的类型。

不幸的是,由于SBT的类加载问题,我们不能仅在REPL中进行其余的练习,而必须在代码中定义该类。 可以编译此代码,然后在REPL中或由其他代码使用。 为此,请将以下内容另存为〜/ json-tutorial / Simple.scala

case class Simple(val foo: String, val bar: List[String], val baz: Map[String,Int])

object SimpleExample {
  def main(args: Array[String]) {
    import com.codahale.jerkson.Json._
    val simpleJson = '''{'foo':42, 'bar':['a','b','c'], 'baz':{'x':1,'y':2}}'''
    val simpleObject = parse[Simple](simpleJson)
    println(simpleObject)
  }
}

然后使用命令:quit退出上一部分的Scala REPL会话,然后执行以下操作。 (如果发生任何问题,您可以重新启动SBT(使用runSbt )并执行以下命令。)

> compile
[info] Compiling 1 Scala source to /Users/jbaldrid/json-tutorial/target/scala-2.9.2/classes...
[success] Total time: 2 s, completed May 11, 2012 9:24:00 PM
> run
[info] Running SimpleExample SimpleExample
Simple(42,List(a, b, c),Map(x -> 1, y -> 2))
[success] Total time: 1 s, completed May 11, 2012 9:24:03 PM

您可以在Simple.scala中更改代码,再次进行编译(无需退出SBT即可),然后再次运行。 另外,既然已经编译好了,如果您使用控制台操作启动Scala REPL,那么Simple类现在就可以使用了,您可以继续使用REPL。 例如,这是与前面给出的SimpleExample main方法中使用的语句相同的语句。

scala> import com.codahale.jerkson.Json._
import com.codahale.jerkson.Json._

scala> val simpleJson = '''{'foo':42, 'bar':['a','b','c'], 'baz':{'x':1,'y':2}}'''
simpleJson: java.lang.String = {'foo':42, 'bar':['a','b','c'], 'baz':{'x':1,'y':2}}

scala> val simpleObject = parse[Simple](simpleJson)
simpleObject: Simple = Simple(42,List(a, b, c),Map(x -> 1, y -> 2))

scala> println(simpleObject)
Simple(42,List(a, b, c),Map(x -> 1, y -> 2))

JSON序列化的另一个不错的功能是,如果JSON字符串包含的信息比您要构造的对象所需要的信息多,那么它将被忽略。 例如,考虑反序列化以下示例,该示例在JSON表示中具有一个额外的字段eca

scala> val ecaJson = '''{'foo':42, 'bar':['a','b','c'], 'baz':{'x':1,'y':2}, 'eca': true}'''
ecaJson: java.lang.String = {'foo':42, 'bar':['a','b','c'], 'baz':{'x':1,'y':2}, 'eca': true}

scala> val noEcaSimpleObject = parse[Simple](ecaJson)
noEcaSimpleObject: Simple = Simple(42,List(a, b, c),Map(x -> 1, y -> 2))

eca信息悄然消失,我们仍然可以得到一个包含所有所需信息的Simple对象。 该属性对于忽略无关信息非常有用,我将在后续文章中介绍该信息,该信息对处理来自Twitter API的JSON格式的推文非常有用。

关于上述示例的另一件事要注意,布尔值truefalse是有效的JSON(它们不是用引号引起来的字符串,而是实际的布尔值)。 解析布尔值甚至可以原谅,因为Jerkson会为您提供一个布尔值,即使将其定义为字符串也是如此。

scala> parse[Map[String,Boolean]]('''{'eca':true}''')
res0: Map[String,Boolean] = Map(eca -> true)

scala> parse[Map[String,Boolean]]('''{'eca':'true'}''')
res1: Map[String,Boolean] = Map(eca -> true)

如果您碰巧将布尔值转换为字符串,它将转换为字符串。

scala> parse[Map[String,String]]('''{'eca':true}''')

res2: Map[String,String] = Map(eca -> true)

但是(明智地)它不会将truefalse以外的任何String转换为布尔值。

scala> parse[Map[String,Boolean]]('''{'eca':'brillig'}''')
com.codahale.jerkson.ParsingException: Can not construct instance of boolean from String value 'brillig': only 'true' or 'false' recognized
at [Source: java.io.StringReader@6b2739b8; line: 1, column: 2]
<...stacktrace...>

而且,除少数几个选择(包括truefalse)外 ,它不允许其他所有未引用的值。

scala> parse[Map[String,String]]('''{'eca':brillig}''')

com.codahale.jerkson.ParsingException: Malformed JSON. Unexpected character ('b' (code 98)): expected a valid value (number, String, array, object, 'true', 'false' or 'null') at character offset 7.
<...stacktrace...>

换句话说,您的JSON需要语法。 从对象生成JSON

如果手头有一个对象,则可以使用generate方法从中创建JSON(序列化)非常容易。

scala> val simpleJsonString = generate(simpleObject)
simpleJsonString: String = {'foo':'42','bar':['a','b','c'],'baz':{'x':1,'y':2}}

这比XML解决方案容易得多,后者需要明确声明如何将对象转换为XML元素。 限制是任何此类对象都必须是case类的实例。 如果没有案例类,则需要进行一些特殊处理(本教程中未讨论)。


丰富的JSON示例

按照XML上一教程的内容 ,我创建了与其中使用的音乐XML示例相对应的JSON。 您可以将其作为Github gist music.json找到

https://gist.github.com/2668632

将该文件另存为/tmp/music.json

提示 :您可以使用Python中的mjson工具轻松地将压缩的JSON格式化为更易于阅读的格式。

$ cat /tmp/music.json | python -mjson.tool
[
  {
    'albums': [
     {
       'description': '\n\tThe King of Limbs is the eighth studio album by English rock band Radiohead, produced by Nigel Godrich. It was self-released on 18 February 2011 as a download in MP3 and WAV formats, followed by physical CD and 12\' vinyl releases on 28 March, a wider digital release via AWAL, and a special \'newspaper\' edition on 9 May 2011. The physical editions were released through the band's Ticker Tape imprint on XL in the United Kingdom, TBD in the United States, and Hostess Entertainment in Japan.\n ',
       'songs': [
         {
           'length': '5:15',
           'title': 'Bloom'
         },
<...etc...>

接下来,将以下代码另存为〜/ json-tutorial / MusicJson.scala

package music {

  case class Song(val title: String, val length: String) {
    @transient lazy val time = {
      val Array(minutes, seconds) = length.split(':')
      minutes.toInt*60 + seconds.toInt
    }
  }

  case class Album(val title: String, val songs: Seq[Song], val description: String) {
    @transient lazy val time = songs.map(_.time).sum
    @transient lazy val length = (time / 60)+':'+(time % 60)
  }

  case class Artist(val name: String, val albums: Seq[Album])
}

object MusicJson {
  def main(args: Array[String]) {
    import com.codahale.jerkson.Json._
    import music._
    val jsonInput = io.Source.fromFile('/tmp/music.json').mkString
    val musicObj = parse[List[Artist]](jsonInput)
    println(musicObj)
  }
}

几个快速笔记。 SongAlbumArtist类与上一个XML处理教程中使用的类相同,但有两个更改。 第一个是我裹着他们在一个包的音乐 。 这仅是为了解决我们在此处使用SBT运行Jerkson时遇到的问题 。 另一个是不在构造函数中的字段被标记为@transient :这确保了当我们从这些类的对象生成JSON时,它们不会包含在输出中。 一个显示其重要性的示例是我创建music.json文件的方式:我像上一教程中那样读取XML,然后使用Jerkson生成JSON-在没有@transient批注的情况下,这些字段包含在输出中。 作为参考,下面是执行从XML到JSON转换的代码(您可以根据需要将其添加到MusicJson.scala中 )。

object ConvertXmlToJson {
  def main(args: Array[String]) {
    import com.codahale.jerkson.Json._
    import music._
    val musicElem = scala.xml.XML.loadFile('/tmp/music.xml')

    val artists = (musicElem \ 'artist').map { artist =>
      val name = (artist \ '@name').text
      val albums = (artist \ 'album').map { album =>
        val title = (album \ '@title').text
        val description = (album \ 'description').text
        val songList = (album \ 'song').map { song =>
          Song((song \ '@title').text, (song \ '@length').text)
        }
        Album(title, songList, description)
      }
      Artist(name, albums)
    }

    val musicJson = generate(artists)
    val output = new java.io.BufferedWriter(new java.io.FileWriter(new java.io.File('/tmp/music.json')))
    output.write(musicJson)
    output.flush
    output.close
  }
}

还有其他序列化策略(例如,对象的二进制序列化), @ transient注释也同样受到它们的尊重。

给定MusicJson.scala中的代码,我们现在可以编译并运行它。 在SBT中,您可以运行runrun-main 。 如果选择运行,并且项目中有多个主要方法,那么SBT将为您提供选择。

> run

Multiple main classes detected, select one to run:

[1] SimpleExample
[2] MusicJson
[3] ConvertXmlToJson

Enter number: 2

[info] Running MusicJson
List(Artist(Radiohead,List(Album(The King of Limbs,List(Song(Bloom,5:15), Song(Morning Mr Magpie,4:41), Song(Little by Little,4:27), Song(Feral,3:13), Song(Lotus Flower,5:01), Song(Codex,4:47), Song(Give Up the Ghost,4:50), Song(Separator,5:20)),
The King of Limbs is the eighth studio album by English rock band Radiohead, produced by Nigel Godrich. It was self-released on 18 February 2011 as a download in MP3 and WAV formats, followed by physical CD and 12' vinyl releases on 28 March, a wider digital release via AWAL, and a special 'newspaper' edition on 9 May 2011. The physical editions were released through the band's Ticker Tape imprint on XL in the United Kingdom, TBD in the United States, and Hostess Entertainment in Japan.
), Album(OK Computer,List(Song(Airbag,4:44), Song(Paranoid
<...more printed output...>
[success] Total time: 3 s, completed May 12, 2012 11:52:06 AM

使用run-main ,您只需显式提供要运行其main方法的对象的名称。

> run-main MusicJson

[info] Running MusicJson
<...same output as above...>

因此,无论哪种方式,我们都已经成功地反序列化了音乐数据的JSON描述。 (您也可以通过从SBT控制台运行MusicJson的main方法的代码输入REPL来获得相同的结果。)


结论

本教程说明了将对象与JSON格式进行序列化(生成)和反序列化(解析)对象是多么容易。 希望这证明了使用Jerkson库和Scala做到这一点相对容易,尤其是与出于类似目的使用XML相比,这相对容易。

除了这种方便之外,JSON通常比等效的XML更紧凑。 但是,它仍然远不是真正的压缩格式,并且有很多明显的“浪费”,例如对每个对象一次又一次地重复字段名称。 当数据表示为JSON字符串并通过网络发送和/或在Hadoop等分布式处理框架中使用时,这一点非常重要。 Avro文件格式是JSON的演变,它执行了这种压缩:它包含一个包含每个文件的架构,然后每个对象以二进制格式表示,该二进制格式仅指定数据,而不指定字段名称。 除了更紧凑之外,它还保留了易于拆分的属性,这对于在Hadoop中处理大型文件非常重要。

参考:来自Bcomposes博客的JCG合作伙伴 Jason Baldridge的Jerkson在Scala中处理JSON

翻译自: https://www.javacodegeeks.com/2012/05/processing-json-in-scala-with-jerkson.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值