Python学习:json对象快速访问(续)

问题

我们再次回到jsonpath的问题,起初使用jsonpath-ng实现快速访问json对象节点。但jsonpath-ng的性能问题,导致这个方法实在是糟糕,无法使用,悲剧的是已经写了很多代码,再用全路径访问json工作量有点太大,有点进退两难。

回过头思考,起初我的目的,就是不必要写全路径访问json节点,通过jsonpath只需要写一个节点进行模糊匹配,实现节点访问。实际上该需求并不复杂,并没有用到jsonpath的复杂功能。能不能不用jsonpath,也能实现?

幸运的是方案是存在的,而且实现起来也很容易。python语言天生就支持json对象,不需要再进行对象的序列化,可以直接利用这个语言特性。

方案

节点对象获取

首先我们解决json节点获取问题。我们写下如下的代码:

 

def get_one_json_node(data: dict, field: str):
    if isinstance(data, list):
        for it in data:
            ret = get_one_json_node(it, field)
            if ret is not None:
                return ret

    if not isinstance(data, dict):
        return None
    if field in data.keys():
        return data[field]

    for it in data.keys():
        ret = get_one_json_node(data[it], field)
        if ret:
            return ret
    return None

测试代码:

 

class TestJsonGet:
    js = {'a': 10, 'b': 20, 'c': {'e': 10, 'f': 'string'}, 'c1': {'e': {'f1': 30}, 'f': 'string'},
  'c2': {'e': 10, 'f': 'string'}, 'z': [{'z1': 10}, {'z1': 20}]}

    def test_get1(self):
        assert 10 == get_one_json_node(self.js, 'a')
        assert 10 == get_one_json_node(self.js, 'e')

    def test_get2(self):
        assert 10  == get_one_json_node(self.js, 'c.e')

通过pytest测试发现,test_get2测试失败。原因是有多个e节点,我们需要支持c.e的访问,于是再封装一个上层方法:

 

def get_json_node(data: dict, field: str):
    node_path = field.split(".")
    node = data
    find = False
    for it in node_path:
        node = get_one_json_node(node, it)
        if not node:
            return None
        else:
            find = True

    if find:
        return node
    else:
        return None

测试代码:

 

class TestJsonGet:
    js = {'a': 10, 'b': 20, 'c': {'e': 10, 'f': 'string'}, 'c1': {'e': {'f1': 30}, 'f': 'string'},
          'c2': {'e': 10, 'f': 'string'}, 'z': [{'z1': 10}, {'z1': 20}]}

    def test_get3(self):
        assert 10 == get_json_node(self.js, 'a')
        assert 10 == get_json_node(self.js, 'e')
        assert 10 == get_json_node(self.js, 'c.e')

    def test_get4(self):
        assert 10 == get_json_node(self.js, 'z.z1')

这次pytest测试就全部通过了(实际上,我也测试了数组节点的访问。为了简单起见,匹配第一个节点时get就直接返回,无需返回所有匹配节点)。节点的get方法执行时间0.1ms左右,相比jsonpath-ng提升近百倍了,可以接受。测试结果如下:

 

 pytest -s test/test_jsonhelper.py::TestJsonGet
======================================================= test session starts =======================================================
platform darwin -- Python 3.8.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/xxx/data/code/python
collected 4 items                                                                                                                 

test/test_jsonhelper.py ..func:{get_json_node} exec time is:{0.00317} ms
func:{get_json_node} exec time is:{0.08789} ms
func:{get_json_node} exec time is:{0.00195} ms
.func:{get_json_node} exec time is:{0.00317} ms

节点的写入

同理,对于json节点设置可以参考get方法,写两个函数,具体代码如下:

 

def set_one_json_node(data: dict, field: str, value):
    if isinstance(data, list):
        for it in data:
            ret = set_one_json_node(it, field, value)
            if ret is not None:
                return ret

    if not isinstance(data, dict):
        return None
    if field in data.keys():
        data[field] = value
        return data

    for it in data.keys():
        ret = set_one_json_node(data[it], field, value)
        if ret:
            return ret
    return None

def set_json_node(data: dict, field: str, value):
    pos = field.find('.')
    if pos != -1:
        parent = field[0:pos]
        node = get_json_node(data, parent)
        if node is None:
            return None
        else:
            return set_one_json_node(node, field[pos + 1:], value)
    else:
        return set_one_json_node(data, field, value)

测试代码如下:

 

class TestJsonSet:
    js = {'a': 10, 'b': 20, 'c': {'e': 10, 'f': 'string'}, 'c1': {'e': {'f1': 30}, 'f': 'string'},
          'c2': {'e': 10, 'f': 'string'}, 'z': [{'z1': 10}, {'z1': 20}]}

    def test_set1(self):
        js = copy.deepcopy(self.js)
        set_one_json_node(js, 'a', 20)
        set_one_json_node(js, 'e', 30)
        assert 20 == get_one_json_node(js, 'a')
        assert 30 == get_one_json_node(js, 'e')

    def test_set2(self):
        js = copy.deepcopy(self.js)
        set_one_json_node(js, 'c.e', 20)
        assert None == get_one_json_node(self.js, 'c.e')

    def test_set3(self):
        js = copy.deepcopy(self.js)
        set_json_node(js, 'a', 20)
        set_json_node(js, 'e', 30)
        assert 20 == get_json_node(js, 'a')
        assert 30 == get_json_node(js, 'e')
        set_json_node(js, 'c.e', 40)
        assert 40 == get_json_node(js, 'c.e')

    def test_set4(self):
        js = copy.deepcopy(self.js)
        set_json_node(js, 'z.z1', 100)
        assert 100 == get_json_node(js, 'z.z1')

pytest测试,set方法的执行时间和get方法执行时间类似,没有出现大的变动,相比jsonpath-ng也有巨大的提升,能够满足要求。运行结果如下:

 

pytest -s test/test_jsonhelper.py::TestJsonSet
======================================================= test session starts =======================================================
platform darwin -- Python 3.8.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/xxx/data/code/python
collected 4 items                                                                                                                 

test/test_jsonhelper.py func:{test_set1} exec time is:{0.03638} ms
..func:{set_json_node} exec time is:{0.00293} ms
func:{set_json_node} exec time is:{0.00293} ms
func:{get_json_node} exec time is:{0.00195} ms
func:{get_json_node} exec time is:{0.00195} ms
func:{get_json_node} exec time is:{0.00098} ms
func:{set_json_node} exec time is:{0.00513} ms
func:{get_json_node} exec time is:{0.00098} ms
.func:{get_json_node} exec time is:{0.00195} ms
func:{set_json_node} exec time is:{0.01514} ms
func:{get_json_node} exec time is:{0.02588} ms

讨论

我们简单实现了json访问的get和set方法,性能得到比较好的提升,对于频繁访问json对象,可以不使用jsonpath-ng,直接使用上面的方法,从而规避jsonpath-ng的性能问题。

有的时候,对于自身的需求,一定要多思考,也许存在一种简单的方法,就能解决。不一定杀鸡得用牛刀,而且可能是一把生锈的牛刀,盲目使用第三方库,可能会走不少弯路。

结合之前的总结,完善JsonHelper类型,这样就可以使用了(当然代码的健壮性,还有不少工作,依据实际需求,大家有兴趣自行完善即可)。辅助类型JsonHelper代码如下:

 

class JsonHelper:
    def __init__(self, buffer: dict):
        self.__dict__['_buffer'] = buffer

    def get(self, field: str):
        ret = self.__get_json_node(self.__dict__['_buffer'], field)

        if not ret:
            raise Exception("field:{} is not exist".format(field))
        else:
            return ret

    def set(self, field: str, value):
        ret = self.__set_json_node(self.__dict__['_buffer'], field, value)
        if ret is None:
            raise Exception("field:{} is not exist".format(field))
        return self

    def __get_one_json_node(self, data: dict, field: str):
        if isinstance(data, list):
            for it in data:
                ret = self.__get_one_json_node(it, field)
                if ret is not None:
                    return ret

        if not isinstance(data, dict):
            return None
        if field in data.keys():
            return data[field]

        for it in data.keys():
            ret = self.__get_one_json_node(data[it], field)
            if ret:
                return ret
        return None

    def __get_json_node(self, data: dict, field: str):
        node_path = field.split(".")
        node = data
        find = False
        for it in node_path:
            node = self.__get_one_json_node(node, it)
            if not node:
                return None
            else:
                find = True

        if find:
            return node
        else:
            return None

    def __set_one_json_node(self, data: dict, field: str, value):
        if isinstance(data, list):
            for it in data:
                ret = self.__set_one_json_node(it, field, value)
                if ret is not None:
                    return ret

        if not isinstance(data, dict):
            return None
        if field in data.keys():
            data[field] = value
            return data

        for it in data.keys():
            ret = self.__set_one_json_node(data[it], field, value)
            if ret:
                return ret
        return None

    def __set_json_node(self, data: dict, field: str, value):
        pos = field.find('.')
        if pos != -1:
            parent = field[0:pos]
            node = self.__get_json_node(data, parent)
            if node is None:
                return None
            else:
                return self.__set_one_json_node(node, field[pos + 1:], value)
        else:
            return self.__set_one_json_node(data, field, value)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值