使用 Python 软件包来处理较为复杂的 JSON 文件

最近在工作中遇到需要使用 Python 处理较复杂的 JSON 数据,不仅需要对嵌套的
JSON 数据做查询、筛选,还需要替换其中的一些元素。这里将处理这个问题的过程记录下来,权当记忆。

复杂 json 示例

下面展示的是一个较为复杂的 JSON 数据,这里列出只是展示一个较为复杂的 JSON 数据会怎么复杂。

[
    // 一个 data group 根数据库的示例
    {
        // 首先是激活条件
        "active": {
            "ref-library": "",
            "ref-library_name": "",
            "ref-value": "",
            "ref_path": "",
            "type": "Condition_Default"
        },
        // 接着是它包含的键值对,可包含多个,这里只显示一个
        "items": [
            {
                "active": {
                    "ref-library": "",
                    "ref-library_name": "",
                    "ref-value": "",
                    "ref_path": "",
                    "type": "Condition_Default"
                },
                "enum_list": null,
                "max": null,
                "min": null,
                "name": "periodic_dimension",
                "show": {
                    "show_on_interface": false
                },
                "type": "int_array",
                "unique_name": "/CartesianGeometry/periodic_dimension",
                "value": "0,0,0"
            }
        ],
        // 然后是它包含的子数据库,可能会嵌套
        "librarys": [
            {
                "active": {
                    "ref-library": "",
                    "ref-library_name": "",
                    "ref-value": "",
                    "ref_path": "",
                    "type": "Condition_Default"
                },
                "items": [
                    {
                        "active": {
                            "ref-library": "",
                            "ref-library_name": "",
                            "ref-value": "",
                            "ref_path": "",
                            "type": "Condition_Default"
                        },
                        "enum_list": null,
                        "max": null,
                        "min": null,
                        "name": "SAT_MODEL",
                        "show": {
                            "show_on_interface": false
                        },
                        "type": "string",
                        "unique_name": "/CartesianGeometry/CADModel/SAT_MODEL",
                        "value": "model.sat"
                    }
                ],
                "librarys": [],
                "name": "CADModel",
                "show": {
                    "show_on_interface": false
                },
                "type": "Library_Fixed",
                "uid": "{b717c641-4145-44ac-8468-efb7afa451f1}",
                "unique_name": "/CartesianGeometry/CADModel"
            }
        ],
        // 最后是 data group 的额外属性
        "name": "CartesianGeometry",
        "show": {
            "show_name": "CartesianGeometry",
            "show_on_interface": true
        },
        "type": "Library_Fixed",
        "uid": "{909515c3-d978-4021-943b-0d6e7fc3136d}",
        "unique_name": "/CartesianGeometry"
    },
    // 后面再添加类似的其他 data group
]

Python 标准库中对 json 数据的处理

在 Python 标准库中,自带 json 模块,可以使用 import json 来使用它。

主要的函数有如下几个:

  • json.dumps:将 Python 对象序列化为 json 字符串
  • json.dump:将 Python 对象序列化为 json 对象
  • json.loads:将 json 字符串导入为 Python 对象
  • json.load:将 json 对象导入为 Python 对象

另外,json 模块还提供一个命令行接口 json.tool,可以在终端中使用,常用的例子如下:

$ echo '{"json": "obj"}' | python -m json.tool
{
    "json": "obj"
}

另外最为常见的使用场景是读入 json 文件的数据,然后经过修改,再将 json 数据写回 json 文件中。

下面的代码就展示这两个常用的使用场景:

# 打开文件将 json 数据读入 data_json 对象中
with open(modelFile) as f:
    data_json = json.load(f)

# 打开新文件,将 data_json 数据写入新的 json 文件中
with open(modModleFile, 'w') as f:
    f.write(json.dumps(data_json, indent=2))

常见处理 JSON 的 Python 软件包

针对上面我们的需求,需要对复杂 JSON 数据做处理,所以就在网上进行了相关的搜索。
针对 JSON 数据的筛选,搜索后知道有个 JSONPath 的提议,仿照
XPath 的语法来查询 JSON 数据。

然后就根据 JSONPath 这个关键字在 PyPI 中搜索都了一些软件包,下面简单罗列一下:

dpath

主页:https://www.github.com/akesterson/dpath-python
特点:使用类型 XPath 的语法来对复杂 json 数据做查询
优点:如果熟悉 XPath 的话,可以快速上手
缺点:无法做复杂的模糊查询,需要知道大致路径来获取数据

jsonpath

主页:www.ultimate.com/phil/python/#jsonpath
特点:是对上面 jsonPath 提议的一个实现,经过测试发现,相比于后面提及的这些软件,它的查询效率最高
优点:查询效率高,单文件的实现
缺点:缺乏文档,开发久远;但还在持续更新

jsonpath2

主页:https://pypi.org/project/jsonpath2/
特点:是对上面 jsonPath 提议的一个实现,有 ANTLR v4 的语法支持,可以生成 antlr 语法解析
优点:支持 antlr 语法
缺点:文档太少,只是 jsonPath 的又一个实现,特点不突出

jsonpath-rw

主页:https://github.com/kennknowles/python-jsonpath-rw
特点:提供健壮的实现,在 Python 2.7, 3.4, 3.5, 3.6, 3.7, pypy 和 pypy3 上都测试过; 实现早,开发者多(15),GitHub 上的星多(434);
优点:开发者多,将 jsonPath 表达式当做第一类对象
缺点:无法对数据做复杂的筛选

jsonpath-rw-ext

主页:https://github.com/sileht/python-jsonpath-rw-ext
特点:对上面的 jsonpath-rw 做了一些扩展,尤其是对筛选的支持
优点:支持筛选
缺点:文档不是太详细,筛选似乎只支持 list

jsonpath-ng

主页:https://github.com/h2non/jsonpath-ng
特点:结合了上面 jsonpath-rw 和 jsonpath-rw-ext 的能力,是 jsonpath-rw 的一个 fork
优点:具有上面两个包的功能,才能使用;文档详细
缺点:扩展的使用有点不太方便

在查找到了这些软件包后,我针对它们都进行了一些测试,最后测试后发现,在查询方面, jsonpath 这个包的解析速度最快。

注:同时引入 jsonpath 和 jsonpath-rw-ext 会引起冲突,我的解决办法是只安装 jsonpath-rw-ext ,将 jsonpath 的单文件实现直接放入代码中,要使用时再引入。

下面回到整体,介绍如何对复杂 JSON 数据进行查询、筛选和修改。

如何查询复杂 json 数据

经过了上面的搜索,简单来说可以直接使用上面的多个软件包,然后按照 JsonPath 的语法来对复杂数据进行查询。

下面是 JsonPath 的提议,摘自上面提及的网址

JSONPath 描述
$ 代表根对象
@ 代表当前对象
.[] 取孩子操作
.. 递归搜索后代
* 通配符,代表所有对象
[] 取下标操作,在 JSON 中,它是自带的数组操作
[,] 表示去其中的任意一个
[start:end:step] 数组切片操作
?() 执行筛选操作
() 脚本表达式,不常用

如何筛选复杂 json 数据

基于前面的搜索,如果要实现对复杂 json 数据的筛选功能,当前就只能使用上面的 jsonpath-rw-ext 或者 jsonpath-ng ,其中 jsonpath-ng 的筛选功能来自于 jsonpath-rw-ext,所以在最后的解决方案中,我直接使用了 jsonpath-rw-ext 来做复杂筛选。

在使用时,为了支持变量值的替换,可能还需要使用转义符。

另外,在做筛选时,可以利用 jsonPath 中 .. @ 等运算符来做模糊筛选。

具体的使用方法可以参考上面两个软件包的主页。这里就不再介绍。

下面重点介绍如何修改 json 数据。

如何修改复杂 json 数据

基于上面的说明,我们知道可以使用上面的这些软件包来查询复杂 json ,但是对于如何修改数据,这些软件的文档中都没有太多的涉及。

另外根据我做的这个任务的需求,可能还需要在对 json 数据做了筛选后,再对筛选到的数据做修改。

python-jsonpath-rw GitHub 主页上的 issue 21 上有类似的说明,但自己根据上的说明去实现时,感觉无法满足上面的需求,故作罢。

另外 python-jsonpath-rw 在代码仓库中似乎更新了一个 update 方法,但没有更新到 PyPI 中,所以必须下载 GitHub 的代码才能使用该功能。在最后的实现时便没有使用该功能。

最终,我在 stack overflow 上找到了一个参考的例子

from jsonpath_rw import jsonpath, parse

data = {"dogs":[{"tail": True, "properties":{"test":1}}]}

jsonpath_expr = parse("dogs.[0].properties")
jsonpath_expr.find(data)[0].value['test'] = 2
print(data)
# {'dogs': [{'tail': True, 'properties': {'test': 2}}]}

通过上面的例子我得到启发,可以使用 jsonpath-rw-ext 或者 jsonpath-ng 包的筛选功能,再使用上面例子中类型的做法来达到筛选并修改特定键值的目的,这样我的需求就可以解决了。

在经过测试后,最终用来修改 input 模板文件中给定键值对的代码如下:

import json
import jsonpath_rw_ext

def Modify_model(refVar, refValue, modelFile, modModleFile='run.json'):
    """利用 jsonpath_rw_ext 库搜索 json 格式的模板文件,并修改相应的值
        refVar: 需要修改的参数的名称
        refValue: 修改过后参数的值
        modelFile: json 格式的模板文件
        modModleFile: 修改过后的 json 模板文件
    """
    with open(modelFile) as f:
        data_json = json.load(f)
    parse_str = f'$..items[?(@.unique_name=\"{refVar}\")]'

    jsonpath_rw_ext.parser.ExtentedJsonPathParser().parse(parse_str).find(data_json)[0].value['value'] = f'{refValue}'

    if not os.path.exists(modModleFile):
        # 如果不存在文件,则直接创建空文件
        # ref: https://stackoverflow.com/questions/12654772/create-empty-file-using-python
        open(modModleFile, 'a').close()
    with open(modModleFile, 'w') as f:
        f.write(json.dumps(data_json, indent=2))

上面的代码中有几点需要说明:

  1. parse_str = f'$..items[?(@.unique_name=\"{refVar}\")]' 这句使用了 Python 的新语法,直接将 refVar 的值替换到字符串中,这需要 Python 3.6 才能支持。另外还转义了括号,使得这个函数更加通用;

  2. jsonpath_rw_ext.parser.ExtentedJsonPathParser() 这句是为了使用扩展功能才这样写的,具体的使用方法可以参考其主页章的文档;

  3. 根据 GitHub 上的 issue,似乎筛选功能只对 list 生效,相关代码在这里