Статьи

Обработка JSON в Scala с помощью Jerkson

Вступление

Предыдущий учебник охватывал базовую обработку XML в Scala , но, как я уже отметил, XML в наши дни не является основным выбором для сериализации данных. Вместо этого JSON (JavaScript Object Notation) более широко используется для обмена данными, отчасти потому, что он менее многословен и лучше отражает основные структуры данных (такие как списки и карты), которые используются при определении многих объектов. Первоначально он был разработан для работы с JavaScript, но оказался довольно эффективным в качестве нейтрального языка. Очень приятная особенность этого состоит в том, что это просто — переводить объекты, определенные в таких языках, как Java и Scala, в JSON и обратно, как я покажу в этом уроке. Если определения классов и структуры JSON выровнены надлежащим образом, это преобразование оказывается совершенно тривиальным — при наличии подходящей библиотеки обработки JSON.

В этом руководстве я рассмотрю базовую обработку JSON в Scala с использованием библиотеки Jerkson , которая сама по себе является оболочкой Scala для библиотеки Jackson (написанной на Java). Обратите внимание, что другие библиотеки, такие как lift-json, являются отличной альтернативой, но у Джексона, похоже, есть некоторые преимущества в эффективности потоковой передачи JSON благодаря производительности Джексона . Конечно, поскольку Scala прекрасно работает с Java, вы можете напрямую использовать любую библиотеку JSON на основе JVM, которая вам нравится, включая Джексона.

В этом посте также показано, как быстро начать работу с SBT, что позволит вам легко получить доступ к сторонним библиотекам в качестве зависимостей и начать писать код, который использует их и может быть скомпилирован с помощью SBT.

Примечание : как «Джейсон» я настаиваю на том, чтобы JSON произносился как Jay-SAHN (с ударением на втором слоге), чтобы отличать его от названия. 🙂

Начало настройки

Простой способ использования библиотеки Jerkson в контексте подобного учебного пособия заключается в том, что читатель может настроить новый проект SBT, объявить Jerkson как зависимость, а затем запустить Scala REPL с помощью консольного действия SBT. Это сортирует процесс получения внешних библиотек и настройки пути к классам так, чтобы они были доступны в инициированном SBT Scala REPL. Следуйте инструкциям в этом разделе, чтобы сделать это.

Примечание : если вы уже работали с Scalabha версии 0.2.5 (или более поздней), перейдите к нижней части этого раздела, чтобы узнать, как запустить REPL с использованием сборки Scalabha. В качестве альтернативы, если у вас есть собственный проект, вы, конечно, можете просто добавить Jerkson в качестве зависимости, импортировать его классы по мере необходимости и использовать его в своих обычных настройках программирования. Примеры, приведенные ниже, помогут в качестве простых рецептов для использования его в вашем проекте.

Сначала создайте каталог для работы и загрузите jar запуска SBT.

1
2
3
$ 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 . Имейте в виду, что важно сохранять пустые строки между каждым объявлением.

1
2
3
4
5
6
7
8
9
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 .

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

Сделайте этот файл исполняемым и запустите его, что покажет SBT, выполняющего кучу работы, а затем оставит вас с приглашением SBT.

01
02
03
04
05
06
07
08
09
10
11
$ 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, и теперь мы готовы запустить Scala REPL с использованием SBT. Важно то, что этот экземпляр REPL будет иметь библиотеку Jerkson и ее зависимости в пути к классам, чтобы мы могли импортировать нужные нам классы.

01
02
03
04
05
06
07
08
09
10
./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 по умолчанию), вы увидите что-то вроде следующего.

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

Если это то, что вы получили, попробуйте снова следовать приведенным выше инструкциям, чтобы убедиться, что ваши настройки точно такие же, как указано выше. Однако, если вы продолжаете испытывать проблемы, альтернативой является получение версии 0.2.5 Scalabha (в которой уже есть Jerkson в качестве зависимости), следуйте инструкциям по ее настройке и затем выполните следующие команды.

1
2
$ cd $SCALABHA_DIR
$ scalabha build console

Если вы просто хотите увидеть некоторые примеры использования Jerkson в качестве API, а не использовать его в интерактивном режиме, тогда совершенно необязательно выполнять настройку SBT — просто прочитайте и адаптируйте примеры по мере необходимости.


Обработка простого примера JSON

Как обычно, давайте начнем с очень простого примера, который показывает некоторые основные свойства JSON.

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

Это описывает структуру данных с тремя полями, foo , bar и baz . Значение поля foo представляет собой целое число 42 , значение bar представляет собой список строк, а значение baz представляет собой карту из строк в целые числа. Это не зависящие от языка (но универсальные) типы.

Давайте сначала рассмотрим десериализацию каждого из этих значений по отдельности как объектов Scala, используя метод разбора Джексона. Имейте в виду, что JSON в файле — это строка, поэтому входные данные во всех этих случаях являются строками (иногда я буду использовать строки с тройными кавычками, когда в JSON есть сами кавычки). В каждом случае мы сообщаем методу разбора, какой тип мы ожидаем, предоставляя спецификацию типа перед аргументом.

1
2
3
4
5
6
7
8
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 в качестве ожидаемого типа.

1
2
3
4
5
scala> parse[Any]('42')
res3: Any = 42
 
scala> parse[List[Any]]('''['a',1]''')
res4: List[Any] = List(a, 1)

Если вы дадите ожидаемый тип, который не может быть проанализирован как таковой, вы получите ошибку.

1
2
3
4
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 следующим образом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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 }}

Поскольку это карта из строк в различные типы значений, лучшее, что мы можем сделать, это десериализовать ее как карту [String, Any] .

1
2
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 , вам нужно привести их к соответствующим типам.

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
8
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)

Вуаля! Когда вы работаете с библиотеками Java в Scala, JavaConversion обычно оказывается чрезвычайно удобным.


Десериализация в пользовательские типы

Хотя мы смогли проанализировать простое приведенное выше выражение JSON и даже преобразовать значения в соответствующие типы, все еще было немного неуклюже. К счастью, если вы определили свой собственный класс дел с соответствующими полями, вы можете вместо этого предоставить его в качестве ожидаемого типа. Например, вот простой класс case, который сделает свое дело.

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

Очевидно, что у этого есть все правильные поля (с переменными, названными так же как поля в примере JSON), и у переменных есть типы, которые мы хотели бы, чтобы они имели.

К сожалению, из-за проблем с загрузкой классов в SBT мы не можем выполнять оставшуюся часть этого упражнения исключительно в REPL и должны определить этот класс в коде. Этот код может быть скомпилирован и затем использован в REPL или другом коде. Для этого сохраните следующее как ~ / json-tutorial / Simple.scala .

01
02
03
04
05
06
07
08
09
10
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)
  }
}

Затем выйдите из сеанса Scala REPL, в котором вы участвовали в предыдущем разделе, с помощью команды : quit и выполните следующие действия. (Если что-то пошло не так, вы можете перезапустить SBT (с помощью runSbt ) и выполнить следующие команды.)

1
2
3
4
5
6
7
> 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, приведенном ранее.

01
02
03
04
05
06
07
08
09
10
11
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 содержит больше информации, чем нужно для создания объекта, который требуется построить из нее, она игнорируется. Например, рассмотрим десериализацию следующего примера, в котором есть дополнительное поле eca в представлении JSON.

1
2
3
4
5
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 молча ускользает, и мы все еще получаем простой объект со всей необходимой информацией. Это свойство очень удобно для игнорирования нерелевантной информации, которая, как я покажу, будет весьма полезна в последующем посте по обработке твитов в формате JSON из API Twitter.

Еще один момент, который следует отметить в приведенном выше примере, это то, что логические значения true и false являются допустимыми JSON (это не строки в кавычках, а фактические логические значения). Парсинг логического значения даже довольно прост, поскольку Джексон даст вам логическое значение, даже если оно определено как строка.

1
2
3
4
5
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)

И он преобразует логическое значение в строку, если вы попросите его сделать это.

1
2
3
scala> parse[Map[String,String]]('''{'eca':true}''')
 
res2: Map[String,String] = Map(eca -> true)

Но он (разумно) не преобразует никакую строку, кроме true или false, в логическое значение.

1
2
3
4
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...>

И он не допускает значения без кавычек, кроме нескольких избранных, включая true и false .

1
2
3
4
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 из объекта

Если у вас есть объект в руке, очень легко создать из него JSON (сериализовать) с помощью метода generate .

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

Это намного проще, чем решение XML, которое требует явного объявления того, как объект должен быть превращен в элементы XML. Ограничение состоит в том, что любые такие объекты должны быть экземплярами класса case. Если у вас нет класса case, вам нужно выполнить специальную обработку (не обсуждаемую в этом руководстве).


Более богатый пример JSON

В духе предыдущего урока по XML я создал JSON, соответствующий используемому там музыкальному XML-примеру. Вы можете найти его как Github Gist music.json :

https://gist.github.com/2668632

Сохраните этот файл как /tmp/music.json .

Совет : вы можете легко отформатировать сжатый JSON, чтобы сделать его более читабельным, используя инструмент mjson в Python.

01
02
03
04
05
06
07
08
09
10
11
12
$ 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 .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)
  }
}

Пару быстрых заметок. Классы Song , Album и Artist те же, что и в предыдущем уроке по обработке XML, с двумя изменениями. Во-первых, я обернул их в пакет музыки . Это необходимо только для решения проблемы с запуском Jerkson в SBT, как мы делаем здесь. Другое состоит в том, что поля, не входящие в конструктор, помечаются как @transient : это гарантирует, что они не будут включены в выходные данные, когда мы генерируем JSON из объектов этих классов. Примером того, как это важно, является способ создания файла music.json : я прочитал в XML, как и в предыдущем уроке, а затем с помощью Jerkson сгенерировал JSON — без аннотации @transient эти поля включаются в вывод , Для справки вот код для преобразования XML в JSON (который вы можете добавить в MusicJson.scala, если хотите).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 вы можете выполнить run или run-main . Если вы выберете запустить и в вашем проекте более одного метода, SBT предоставит вам выбор.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
> 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 вы просто явно указываете имя объекта, основной метод которого вы хотите запустить.

1
2
3
4
> run-main MusicJson
 
[info] Running MusicJson
<...same output as above...>

Так или иначе, мы успешно десериализовали JSON-описание музыкальных данных. (Вы можете также получить тот же результат, введя код основного метода MusicJson в REPL при запуске его из консоли SBT.)


Вывод

В этом руководстве показано, как легко сериализовать (генерировать) и десериализовать (анализировать) объекты в формат JSON и из него. Надеюсь, это продемонстрировало относительную простоту выполнения этого с библиотекой Джерксона и Scala, и особенно относительную простоту по сравнению с работой с XML для аналогичных целей.

В дополнение к этой легкости, JSON обычно более компактен, чем эквивалентный XML. Тем не менее, он по-прежнему далек от того, чтобы быть действительно сжатым форматом, и есть много очевидных «потерь», таких как повторение имен полей для каждого объекта снова и снова. Это очень важно, когда данные представлены в виде строк JSON и отправляются по сетям и / или используются в средах распределенной обработки, таких как Hadoop. Формат файла Avro — это эволюция JSON, которая выполняет такое сжатие: она включает схему с каждым файлом, а затем каждый объект представляется в двоичном формате, который указывает только данные, а не имена полей. Помимо того, что он более компактен, он сохраняет свойства легкого разделения, что очень важно для обработки больших файлов в Hadoop.

Ссылка: Обработка JSON в Scala с Джерксоном от нашего партнера JCG Джейсона Болдриджа в блоге Bcomposes .