测试基础 深入分析: Python 中最高效的 JSONPath 库性能大比拼

花菜 · 2023年11月03日 · 最后由 cooling 回复于 2023年11月04日 · 7804 次阅读

1.背景

在接口测试时,绝大部分的响应都 json 格式,json 的取值就一定绕不过 jsonpath.

某天,就在技术群讨论了 jsonpath 相关的问题.

讨论归讨论,结果最终还是的看实际代码怎么样.

接下来,将会实测 python 中几个常用 jsonpath 库 (jsonpath,jsonpath-ng,jmespath,gjson) 性能到底如何

image.png

2.耗时和内存测试

2.1 运行 1 次的耗时和内存

/Users/rikasai/.virtualenvs/jsonpath_compare/bin/python /Users/rikasai/code/python/jsonpath_compare/main.py 
test_jsonpath 总耗时: 0.002014 秒
test_jsonpath 使用了 0.038136 MB 内存,峰值为 0.039424 MB

test_gjson 总耗时: 0.001517 秒
test_gjson 使用了 0.023019 MB 内存,峰值为 0.028319 MB

test_jsonpath_ng 总耗时: 0.027746 秒
test_jsonpath_ng 使用了 0.2931 MB 内存,峰值为 0.323259 MB

test_jmespath 总耗时: 0.000571 秒
test_jmespath 使用了 0.022307 MB 内存,峰值为 0.025619 MB

运行一次的结果大概已经能看到 jsonpath_ng,不管内存占用还是耗时都是跟另外三个相差甚远

2.2 运行 1000 次的耗时和内存

/Users/rikasai/.virtualenvs/jsonpath_compare/bin/python /Users/rikasai/code/python/jsonpath_compare/main.py 
test_jsonpath 总耗时: 0.126801 秒
test_jsonpath 使用了 0.218614 MB 内存,峰值为 2.219974 MB

test_gjson 总耗时: 0.264562 秒
test_gjson 使用了 0.108182 MB 内存,峰值为 2.135345 MB

test_jsonpath_ng 总耗时: 17.054877 秒
test_jsonpath_ng 使用了 3.061619 MB 内存,峰值为 6.086255 MB

test_jmespath 总耗时: 0.137950 秒
test_jmespath 使用了 0.137203 MB 内存,峰值为 1.973523 MB

1 次可能说明不了问题,运行 1000 次, 跟运行 1 次时结果是类似.

可以看到 jsonpath_ng 相比另外三个要,耗时和内存占用都是非常离谱,完全不应该放在一起比较那种

另外三个 jmespath,jsonpath,gjson 不相伯仲

3.性能分析

3.1 cProfile 对比

下面分别对比这四个库,json 执行一次 jsonpath 取值,涉及函数调用次数和原始调用耗时

完整的调用统计非常多,只放出调用次数较多的部分,按照调用次数到序排序

3.1.1 jsonpath

在 cProfile 的输出中,ncalls 列表示函数被调用的次数。看到 219/196 这意味着函数被递归地调用了。

第一个数字 219 表示函数被调用的总次数。

第二个数字 196 表示函数被非递归地调用的次数。

/Users/rikasai/.virtualenvs/jsonpath_compare/bin/python /Users/rikasai/code/python/jsonpath_compare/main.py 
         1417 function calls (1350 primitive calls) in 0.001 seconds

Ordered by: call count

ncalls tottime percall cumtime percall filename:lineno(function)
251 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects}
219/196 0.000 0.000 0.000 0.000 {built-in method builtins.len}
160 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}
119 0.000 0.000 0.000 0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:164(getitem)
61 0.000 0.000 0.000 0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:233(next)
56 0.000 0.000 0.000 0.000 {built-in method builtins.min}
50 0.000 0.000 0.000 0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:160(
len__)
46 0.000 0.000 0.000 0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:254(get)
33 0.000 0.000 0.000 0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:172(append)
33 0.000 0.000 0.000 0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:249(match)
25 0.000 0.000 0.000 0.000 {built-in method builtins.ord}
24/8 0.000 0.000 0.000 0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:174(getwidth)

### 3.1.2 gjson
```shell
         1167 function calls (1088 primitive calls) in 0.001 seconds

   Ordered by: call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      176    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
  164/146    0.000    0.000    0.000    0.000 {built-in method builtins.len}
      127    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
       59    0.000    0.000    0.000    0.000 {method 'startswith' of 'str' objects}
       52    0.000    0.000    0.000    0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:164(__getitem__)
       42    0.000    0.000    0.000    0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:233(__next)
       34    0.000    0.000    0.000    0.000 {built-in method builtins.min}
       27    0.000    0.000    0.000    0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:254(get)
       26    0.000    0.000    0.000    0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:160(__len__)
       26    0.000    0.000    0.000    0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:249(match)

3.1.3 jsonpath_ng

 24933 function calls (24438 primitive calls) in 0.013 seconds

   Ordered by: call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     3668    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
     3634    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
     2528    0.000    0.000    0.000    0.000 {built-in method builtins.id}
     2241    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
1623/1420    0.000    0.000    0.000    0.000 {built-in method builtins.len}
      984    0.001    0.000    0.002    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/ply/yacc.py:2165(lr0_goto)
      897    0.000    0.000    0.000    0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:233(__next)
      850    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
      795    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/ply/yacc.py:127(__getattribute__)
      795    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/ply/yacc.py:130(__call__)
      622    0.000    0.000    0.000    0.000 {method 'match' of 're.Pattern' objects}
      386    0.000    0.000    0.000    0.000 /Users/rikasai/.pyenv/versions/3.9.11/lib/python3.9/sre_parse.py:164(__getitem__)
      299    0.000    0.000    0.000    0.000 {built-in method builtins.min}

3.1.4 jmespath

163 function calls (154 primitive calls) in 0.000 seconds

   Ordered by: call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       28    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/jmespath/lexer.py:129(_next)
       14    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/jmespath/parser.py:463(_current_token)
       12    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/jmespath/lexer.py:26(tokenize)
       11    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
       11    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
       10    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/jmespath/parser.py:460(_advance)
      8/1    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/jmespath/visitor.py:87(visit)
        6    0.000    0.000    0.000    0.000 {built-in method builtins.len}
        6    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/jmespath/parser.py:469(_lookahead_token)
        4    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/jmespath/parser.py:466(_lookahead)
      3/1    0.000    0.000    0.000    0.000 /Users/rikasai/.virtualenvs/jsonpath_compare/lib/python3.9/site-packages/jmespath/parser.py:118(_expression)

3.1.5 cProfile 结论

cProfile 结果和上面的耗时和结果大体是吻合的

很明显的看到 jsonpath_ng 取值一次居然涉及到了 24933 次函数调用

而 jsonpath 和 gjson 都是一千多次,jmespath 最好,仅仅需要 163 次,遥遥领先!

还是有一个疑惑,上面 1000 次耗时和内存测试,发现 jmespath,jsonpath,gjson 是不相伯仲的

但 jmespath 函数调用确实比另外两个相差了至少 1000 次,结果似乎对不上

把运行次数拉大到 10000 次,结果就很明显了,确实是 jmespath 更强

test_jsonpath 总耗时: 1.746682 秒
test_jsonpath 使用了 0.210114 MB 内存,峰值为 19.831589 MB

test_gjson 总耗时: 2.730377 秒
test_gjson 使用了 0.184749 MB 内存,峰值为 18.914425 MB

test_jmespath 总耗时: 1.194248 秒
test_jmespath 使用了 0.127859 MB 内存,峰值为 18.852175 MB

为什么这里 jsonpath_ng 没有 10000 次的测试结果,因为太消耗时间了,被我手动终止...

3.2 火焰图

四个库同时运行 1000 次,因为 jsonpath_ng 占用时间太大了

可以看到,大部分的耗时都是在这个函数parse_token_stream(isonpath_ng/parser.py:47)

如果需要优化,可以从这个地方着手
image.png

4.总结

通过以上的测试和分析,我们可以得出以下结论:

  1. 在耗时和内存占用方面,jsonpath-ng的表现是最差的,与其它三个库相比,无论是运行 1 次还是 1000 次,jsonpath-ng的耗时和内存占用都是最高的。因此,在性能要求较高的场景下,jsonpath-ng不是一个好的选择。

  2. jmespath在运行 1 次和 1000 次的耗时和内存占用都表现较好,且 cProfile 的结果显示jmespath的函数调用次数也是最少的,因此,jmespath是性能最好的一个选项。

  3. jsonpathgjson在耗时和内存占用方面表现相近,且都优于jsonpath-ng。在函数调用次数方面,jsonpathgjson也相近,但略多于jmespath

综上所述,如果你在寻找一个性能优越的 jsonpath 库,jmespath是最佳选择。而jsonpathgjson则是性能较为中等的选项,jsonpath-ng则是最不推荐的选项。

5.附录

本次测试的所有代码和用到的数据都会可以直接在我的 GitHub 仓库看到
https://github.com/lihuacai168/jsonpath_compare

公众号原文

共收到 9 条回复 时间 点赞

在公众号后台有小伙伴留言说,对比的还不够全面,只是对比了 python3.9(gjson 要求最低是 3.9)

下面补充 python3.10,3.11,3.12 的结果
python3.10

python3.11

python3.12

花菜 回复

哈哈,那个后台小伙伴是我,哈哈

抓哇的怎么说

cooling 回复

我还在想怎么让你知道我补充的内容呢

那交给 Java 大佬了,我不熟悉

花菜 回复

哈哈,大哥见笑了😊 👍

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册