概述
json对象和xml对象都是树形结构,文本存储,容易理解。但是,构造起来却相当繁琐,嵌套比较深,代码也不好看,难以维护。
xml和xpath是一对,很容易联想到json和jsonpath也应该成对出现。jsonpath可以使用得json对象访问变得更加简洁,代码更易维护,避免过多的分支嵌套。幸运的是,jsonpath有规范定义,java和python也都有第三库可以使用。
安装jsonpath库
jsonpath第三方库,github上有好几个,比如jsonpath,jsonpath-rw,jsonpath-ng等等。由于json对象的访问涉及到字段的读写,jsonpath和jsonpath-rw都还不支持。jsonpath-ng是基于jsonpath-rw扩展的,可以支持读写、过滤等,可惜帮助少得可怜,而且都是字段读居多,写的样例很难搜索得到。本文就通过jsonpath,实现读写json的字段值。
pip3.9 install jsonpath-ng
JosnHelper定义
get/set方法
为了操作Json对象,我们就定义一个类,用来读写json对象。
from jsonpath_ng import parse
class JsonHelper:
def __init__(self, buffer: dict):
self._buffer = buffer
def get(self, field: str):
if not field.startswith('$..'):
condition = '$..' + field
else:
condition = field
ret = []
for match in parse(condition).find(self.__dict__['_buffer']):
ret.append(match.value)
if not ret:
raise Exception("field:{} is not exist".format(field))
else:
return ret[0]
def set(self, field: str, value):
if not field.startswith('$..'):
condition = '$..' + field
else:
condition = field
parse(condition).update(self.__dict__['_buffer'], value)
return self
JsonHelper比较简单,就是简单定义了get和set两个方法,set方法返回self,可以实现链式调用。使用样例如下:
js = {'a': 10, 'b': 20, 'c': {'e': 10, 'f': 'string'}}
print(JsonHelper(js).get('a'))
print(JsonHelper(js).get('b'))
print(JsonHelper(js).set('a', 1).set('b', 2).set('e', 30)._buffer)
输出:
10
20
{'a': 1, 'b': 2, 'c': {'e': 30, 'f': 'string'}}
下标方法
python的getitem和setitem复写,可以实现下标的访问,使得JsonHelper使用更进一步简化。代码如下:
class JsonHelper:
def __getitem__(self, field):
return self.get(field)
def __setitem__(self, field, value):
return self.set(field, value)
代码没什么好讲的,只是简单的调用get/set方法。使用如下:
js = {'a': 10, 'b': 20, 'c': {'e': 10, 'f': 'string'}}
print(JsonHelper(js)['a'])
print(JsonHelper(js)['b'])
JsonHelper(js)['a'] = 1
print(JsonHelper(js)['a'])
输出:
10
20
1
属性方法
python的属性可以动态添加,这个比较神奇,使得JsonHelper访问json对象简洁。实现原理,就是重载__getattr__和__setattr__两个方法:
class JsonHelper:
def __init__(self, buffer: dict):
self._buffer = buffer
def __getattr__(self, field):
if not field.startswith('$..'):
condition = '$..' + field
else:
condition = field
ret = []
for match in parse(condition).find(self.__dict__['_buffer']):
ret.append(match.value)
if not ret:
raise Exception("field:{} is not exist".format(field))
super().__setattr__(field, ret[0])
return self.__dict__[field]
def __setattr__(self, field: str, value):
super().__setattr__(field, value)
if field == '_buffer':
pass
else:
if not field.startswith('$..'):
condition = '$..' + field
else:
condition = field
if not [match for match in parse(condition).find(self.__dict__['_buffer'])]:
raise Exception("field:{} is not exist".format(field))
parse(condition).update(self.__dict__['_buffer'], value)
super().__setattr__(field, value)
使用举例,如下:
js = {'a': 10, 'b': 20, 'c': {'e': 10, 'f': 'string'}}
print(JsonHelper(js).a)
print(JsonHelper(js).b)
JsonHelper(js).a = 1
print(JsonHelper(js).a)
输出:
10
20
1
实现主要困难,在于动态添加的属性值,要同步到固有的属性_buffer中,务必确保一致性,不然会出现数据一致性问题。另外,有个缺陷,动态添加的属性,内存空间又多存了一份。当然,不使用属性访问,就不会新增空间开销。
重载__getattr__等方法,容易出现死循环,重载时不能直接使用self.buffer访问固有的属性,要通过self.__dict__进行访问。
好吧,我承认,动态新增属性完全没有必要,访问不存在的属性时自动去访问_buffer即可,无需真的动态去新增属性,这样实现更加简单一些,也不会存在一致性问题(比如删除json节点的时候,不需要同步删除属性)。重新实现的代码如下:
class JsonHelper(object):
def __init__(self, buffer: dict):
self._buffer = buffer
def __getattr__(self, field):
if field in self.__dir__() or field == '_buffer':
return super().__getattr__(field)
return self.get(field)
def __setattr__(self, field: str, value):
if field in self.__dir__() or field == '_buffer':
super().__setattr__(field, value)
return
else:
self.set(field, value)
使用属性访问的时候,对json节点名称有比较高的要求,要符合python代码规范,比如节点名称"a-b"这样带下划线、点、完全是数字的,是不能作为属性名称来使用的。
总结
通过jsonpath的基本功能,就已经实现了json对象的快速访问,使得代码更加简化,也很实用,以上三种访问形式,均能实现其功能,可以根据实际需要使用哪一种。jsonpath的功能更加强大,有兴趣的同学可以访问:https://github.com/h2non/jsonpath-ng
网友评论