使用 Groovy 解析 JSON

作者:Paul King
发布日期:2022-07-10 02:00PM


json logo Groovy 对处理各种结构化数据格式(如 JSON、TOML、YAML 等)有很好的支持。本博文着眼于 JSON.

Groovy 文档 中有关于此主题的相当好的文档。还有许多在线资源可以提供更多详细信息,包括 Groovy - JSON 教程在 Groovy 中操作 JSONGroovy 好处:放松……Groovy 将解析你复杂的 JSON,仅举几例。本博文提供了简要概述,并提供了有关各种选项的更多设置信息和详细信息。

内置体验

如果你已经安装了 Groovy 安装压缩包(或 Windows 上的 .msi),你将拥有 groovy-json 模块,其中包含 JsonSlurper,因此这里和其他链接中展示的大部分示例应该可以开箱即用。

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 笔记本

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 一起使用。

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

{
    "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 制作了使用 INDEX_OVERLAY 类型设置的 Groovy JsonSlurper 脚本的原生版本。它的计时也包含在内。

$ 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 语法,并查看了一些非常粗略的性能数据。