使用 Groovy™ 解析 JSON

作者: Paul King

发布时间:2022-07-10 下午 02:00


json logo Groovy 对处理一系列结构化数据格式(如 JSON、TOML、YAML 等)有着出色的支持。本篇博客文章将探讨 JSON

Groovy 文档中关于此主题的文档相当完善,可在 Groovy 文档 中找到。此外,还有许多在线资源提供更多详细信息,包括 Groovy - JSON 教程在 Groovy 中使用 JSONGroovy 妙处:放松……Groovy 将解析你的邪恶 JSON 等,仅举几例。本文将进行快速总结,并提供更多设置信息和各种选项的详细信息。

开箱即用的体验

如果你安装了 Groovy 安装 zip(或 Windows 上的 .msi),你将拥有包含 JsonSlurpergroovy-json 模块,因此本文和前面提到的其他链接中显示的大多数示例应该可以开箱即用。

JsonSlurper 是用于解析 JSON 的主要类。

JsonSlurper in GroovyConsole

此示例展示了解析嵌入在字符串中的 JSON,但还有其他方法用于解析文件、URL 和其他流。

另一个使用 groovysh 的示例

paulk@pop-os:~$ groovysh
Groovy Shell (4.0.3, JVM: 18.0.1)
Type ':help' or ':h' for help.
----------------------------------------------------------------------------------------
groovy:000> new groovy.json.JsonSlurper().parseText('{ "myList": [1, 3, 5] }').myList
===> [1, 3, 5]

或使用 Jupyter/BeakerX Notebook

JsonSlurper in Jupyter notebook

Gradle

如果你使用像 Gradle 这样的构建工具,你可能更喜欢从依赖仓库引用你的依赖项,而不是拥有本地安装的分发版。

假设你在文件 src/test/groovy/JsonTest.groovy 中使用 JsonSlurper 进行以下测试

import groovy.json.JsonSlurper
import org.junit.Test

class JsonTest {
    @Test
    void testJson() {
        def text = '{"person":{"name":"Guillaume","age":33,"pets":["dog","cat"]}}'
        def json = new JsonSlurper().parseText(text)
        assert json.person.pets.size() == 2
    }
}

你可以在如下所示的 build.gradle 文件中引用相关的 Groovy 依赖项,在本例中是 groovy-jsongroovy-test

apply plugin: 'groovy'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "org.apache.groovy:groovy-json:4.0.3" // for JsonSlurper
    testImplementation "org.apache.groovy:groovy-test:4.0.3" // for tests
}

这两个工件都会传递性地引入核心 groovy 工件,因此无需显式引用。

运行 gradle test 应该会运行测试并生成报告

Test results for Class JsonTest

如果你愿意,也可以像这样使用 groovy-all 工件

apply plugin: 'groovy'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "org.apache.groovy:groovy-all:4.0.3"
}

此工件不包含任何 jar 包,但包含所有常见的 Groovy 模块作为传递依赖项。

[注意:在早期的 Groovy 4 版本中,你可能需要将 Groovy 引用为一个平台,例如:testImplementation platform("org.apache.groovy:groovy-all:4.0.1")。现在只有在使用 groovy-bom 工件时才需要这样做。]

Maven

当使用 Maven 构建工具时,你将创建如下所示的 pom.xml 并使用两个插件

<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>myGroupId</groupId>
    <artifactId>groovy-json-maven</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.gmavenplus</groupId>
                <artifactId>gmavenplus-plugin</artifactId>
                <version>1.13.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compileTests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M7</version>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.apache.groovy</groupId>
            <artifactId>groovy-json</artifactId>
            <version>4.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.groovy</groupId>
            <artifactId>groovy-test</artifactId>
            <version>4.0.3</version>
        </dependency>
    </dependencies>
</project>

或者,你可以再次引用 groovy-all 工件,如这个备用构建文件所示

<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>myGroupId</groupId>
    <artifactId>groovy-json-maven</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.gmavenplus</groupId>
                <artifactId>gmavenplus-plugin</artifactId>
                <version>1.13.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compileTests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M7</version>
                <dependencies>
                    <dependency>
                        <groupId>org.apache.maven.surefire</groupId>
                        <artifactId>surefire-junit47</artifactId>
                        <version>3.0.0-M7</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.apache.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <type>pom</type>
            <version>4.0.3</version>
        </dependency>
    </dependencies>
</project>

在引用 groovy-all 工件时,我们使用 <type>pom</type> 指定它是一个 pom 工件。我们还需要配置 surefire 插件以使用 JUnit4。groovy-all 工件也引入了 JUnit5 支持,surefire 插件将默认使用它,并且不会找到我们的测试。

运行测试应该会产生

[INFO] ------------------------------------------------------
[INFO]  T E S T S
[INFO] ------------------------------------------------------
[INFO] Running JsonTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.36 s - in JsonTest

高级特性

为了适应不同的场景,JsonSlurper 由几个内部实现类提供支持。你不能直接访问这些类,而是在实例化 slurper 时设置一个解析器 类型

类型 何时使用

CHAR_BUFFER

默认,最不易出错的解析器,急切解析整数、日期等。

INDEX_OVERLAY

适用于 REST 调用、WebSocket 消息、AJAX、进程间通信。最快的解析器,使用索引指向现有字符缓冲区。

CHARACTER_SOURCE

用于处理更大的 JSON 文件。

LAX

在许多情况下允许注释、无引号或单引号。

这是一个例子

import groovy.json.JsonSlurper
import static groovy.json.JsonParserType.*

def slurper = new JsonSlurper(type: LAX)
def json = slurper.parseText('''{person:{'name':"Guillaume","age":33,"pets":["dog" /* ,"cat" */]}}''')
assert json.person.pets == ['dog']

请注意 person 键缺失的引号,name 键的单引号,以及 "cat" 被注释掉。严格的 JSON 解析器不允许这些更改。

其他 JSON 库

Groovy 不要求你使用 groovy-json 类。你可以使用你喜欢的 Java 库与 Groovy。你仍然会受益于 Groovy 的许多简写符号。

这是一个使用 Gson 的示例

@Grab('com.google.code.gson:gson:2.9.0')
import com.google.gson.JsonParser

def parser = new JsonParser()
def json = parser.parse('{"person":{"name":"Guillaume","age":33,"pets":["dog","cat"]}}')
assert json.person.pets*.asString == ['dog', 'cat']

这是一个使用 Jackson JSON 支持的示例

@Grab('com.fasterxml.jackson.core:jackson-databind:2.13.3')
import com.fasterxml.jackson.databind.ObjectMapper

def text = '{"person":{"name":"Guillaume","age":33,"pets":["dog","cat"]}}'
def json = new ObjectMapper().readTree(text)
assert json.person.pets*.asText() == ['dog', 'cat']

集成查询

Groovy 4 也支持语言集成查询语法,称为 GINQ 或 GQuery。我们也可以将其用于 JSON。

假设我们有关于水果、其价格(每 100 克)和维生素 C 浓度(每 100 克)的 JSON 格式信息

{
    "prices": [
        {"name": "Kakuda plum",      "price": 13},
        {"name": "Camu camu",        "price": 25},
        {"name": "Acerola cherries", "price": 39},
        {"name": "Guava",            "price": 2.5},
        {"name": "Kiwifruit",        "price": 0.4},
        {"name": "Orange",           "price": 0.4}
    ],
    "vitC": [
        {"name": "Kakuda plum",      "conc": 5300},
        {"name": "Camu camu",        "conc": 2800},
        {"name": "Acerola cherries", "conc": 1677},
        {"name": "Guava",            "conc": 228},
        {"name": "Kiwifruit",        "conc": 144},
        {"name": "Orange",           "conc": 53}
    ]
}

现在,假设我们预算有限,想选择性价比最高的水果来帮助我们达到每日维生素 C 需求。我们 连接 价格vitC 信息,并按性价比最高的水果排序。我们将选择前 2 种,以防我们购物时首选的水果缺货。我们的 GQuery 处理如下所示

def jsonFile = new File('fruit.json')
def json = new JsonSlurper().parse(jsonFile)
assert GQL {
    from p in json.prices
    join c in json.vitC on c.name == p.name
    orderby c.conc / p.price in desc
    limit 2
    select p.name
} == ['Kakuda plum', 'Kiwifruit']

从这些数据中我们可以看到,卡卡杜李和猕猴桃是我们的最佳选择。

快速性能比较

作为一种非常粗略的性能衡量,JsonSlurper 以及所有 4 种解析器类型,以及 Gson 和 Jackson,被用来解析 https://github.com/flowcommerce/json-reference 中的时区值,并检查布里斯班的当前时区是否与悉尼的时区相同。这个 json 文件绝不是巨大的。它只有不到 3000 行,大小不到 60K。在 4 次运行后取最佳时间(包括编译时间)——这绝对是一个不应太认真对待的微基准测试,但可能是一个粗略的指导。为了好玩,一个使用 GraalVM 制作的 Groovy JsonSlurper 脚本的本地版本,其类型设置为 INDEX_OVERLAY。它的计时也包括在内。

$ time groovy GroovyJsonIndexOverlay.groovy
real    0m1.365s
user    0m4.157s
sys     0m0.145s

$ time groovy GroovyJsonCharacterSource.groovy
real    0m1.447s
user    0m4.472s
sys     0m0.174s

$ time groovy GroovyJsonLax.groovy
real    0m1.452s
user    0m4.338s
sys     0m0.171s

$ time groovy GroovyJson.groovy
real    0m1.383s
user    0m4.050s
sys     0m0.165s

$ time groovy Gson.groovy
real    0m1.814s
user    0m5.543s
sys     0m0.209s

$ time groovy Jackson.groovy
real    0m2.007s
user    0m6.332s
sys     0m0.208s

$ time ./groovyjsonindexoverlay
real    0m0.015s
user    0m0.011s
sys     0m0.004s

总结

我们已经了解了使用 Groovy 设置项目以解析 JSON 的基础知识,以及根据场景可用的众多选项中的一些。我们还了解了如何使用其他 JSON 库,在处理过程中利用 GQuery 语法,并查看了一些非常粗略的性能数据。