使用 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))
上面的代码中有几点需要说明:
parse_str = f'$..items[?(@.unique_name=\"{refVar}\")]'
这句使用了 Python 的新语法,直接将 refVar 的值替换到字符串中,这需要 Python 3.6 才能支持。另外还转义了括号,使得这个函数更加通用;jsonpath_rw_ext.parser.ExtentedJsonPathParser()
这句是为了使用扩展功能才这样写的,具体的使用方法可以参考其主页章的文档;- 根据 GitHub 上的 issue,似乎筛选功能只对 list 生效,相关代码在这里。