Python 处理 JSON 数据只会 json.loads? 快来看看这两款工具

python 的一个最吸引人的特点就是语言本身提供了大量简单易用的官方库,这其中就包括 json 包和对 json 数据的支持。我们日常开发中,用到的最主要的两个方法就是

import json
json_data = json.loads(...)
json_str = json.dumps(...)

以及类似如下的调用

print(json_data['key1']['key2']['key3'])

上面这些简单的调用就足够应对至少 90% 的日常开发工作,我曾经也天真地认为这些已经足够了。不过事情真的是这样吗?

1. 起因与痛点

我们的一个视频下载服务的任务是下载某视频网站中的视频,其大致处理流程为:

接口传入视频
页面地址
获取页面 HTML
解析 HTML
获取最高清晰度视频地址
下载视频
及其他后续流程
图 1: 整体视频下载流程


该目标网站将不同清晰度的视频地址进行 BASE64 编码后,将其与其他信息一起组织成了一份 JSON 数据,直接放在了视频页面 HTML 文件的 JavaScript 源码中。

图 1 中标记为橙色的部分是页面解析流程,也是最核心的逻辑。其详细步骤为:

Step 1: 通过正则匹配出
HTML 文件中的 JSON 数据
Step 2: 使用 json.loads(...)
将字符串转为 dict
Step 3: 通过类似 json_data['key1']['key2']['key3']...
的方法获得最高清晰度视频地址
图 2: 页面解析流程


爬虫服务大多数对于目标网站的改版是敏感的,所以当发现该服务无法下载该网站视频时,基本可以断定是原来的网站改版了。

查看了日志中的错误信息,如下

json.decoder.JSONDecodeError: Expecting value: line 1 column 825 (char 824)

根据错误栈信息找到错误点,发现报错的位置是图 2 的 Step 2 处。于是打印了 json.loads(...) 函数入参的 820-830 的字符(全部 json 数据太长了很难肉眼发现问题)。发现打印出

ey":undefi

原来,目标网站 HTML 中的 JSON 数据不知道什么时候新出现了 undefined

当然这个问题我最终解决了,为了行文顺畅具体怎么解决的我后面再详细介绍。

在解决了该问题后,图 2 的 Step 3 又出了问题,数据在 JSON 中的位置由原来的 json_data['a1']['b']['c'] 变成了 json_data['a2']['e']['f']['b']['c'] !

综合上面遇到的这些问题,我们可以看出

python 自带 json 解析方法的痛点:

  • json.loads 无法处理非标准格式的 JSON 数据解析
  • 直接通过多个 key 查找 JSON 数据中的结果,程序将变得脆弱

2. 非标准格式的 JSON 数据解析 - demjson

针对非标准格式的 JSON 数据,开源库 demjson 可以帮你解决全部问题。话不多说上代码:

>>> import demjson
>>> a = '{"a": undefined, "b": 1e10, "c": NaN, "c": Infinity}'  # 特殊字符
>>> demjson.decode(a)
{'a': demjson.undefined, 'b': 10000000000, 'c': inf}
>>> b = '{a: "hello", b: null}'  # 注意 a、b 没有双引号
>>> demjson.decode(b)
{'a': 'hello', 'b': None}

直接解决了遇到的第一个痛点:非标准格式的 JSON 数据解析~

3. 写出更加健壮的 JSON 解析方法 - jsonpath & python-jsonpath-rw

针对原站 JSON 数据格式 (层级) 变动导致解析程序失效的问题,我们需要理解这样一件事

核心数据不变性: 核心的 JSON 数据格式和字段名称很难发生变动

这个虽然是我的一个假设,但基于以下三点原因,我认为是合理的:

  • 核心的数据往往有很多内、外业务方使用,对其修改会导致很大的风险(程序员仔细思考过风险,不会这么干)
  • 根据开闭原则,一名有经验的开发不会修改已经存在的数据格式和字段名称,而只会进行添加(程序员虽然没有仔细思考,但是根据自己的经验和常识,不会这么干)
  • 改动现有代码和数据格式在开发届并不流行,因为出力无功(程序员没有思考也没有经验,但根据自己的产出和绩效,不会这么干)

当然上面的前提是这个产品比较成熟稳定且有一定用户基数,否则发生什么状况都不足为奇!

3.1 jsonpath

那这和 “写出更加健壮的 JSON 解析方法” 有什么关系呢?由于 “核心的 JSON 数据格式和字段名称很难变动”,所以在我们获取数据的时候,是不是可以不通过 JSON 数据的根,一点点 “走” 到目标;而是通过最终字段名称的匹配,直接“跳”到目标数据呢?
查找 json 数据的两种方法

图 3: 走 vs 跳


jsonpath 提供了针对 JSON 数据“跳”着查询的方法。类似的方法还有 xpath 对于 xml 的查询。如果通过走的方法获得图 3 中的 target 数值,一个伪代码查询语句是 a.b.c.d.e, 而 jsonpath 的查询语句可以直接是 $..e(含义是查找任意名称为 e 的子 json 节点)。

越是大型和复杂的 JSON 数据,jsonpath 的优势就越明显。同时基于上面关于核心数据不变性的论断,这种“跳”着获取数据的方法不但高效、易写,而且健壮性更好。

关于 jsonpath 的详细语法已经超过了本文的讨论范畴,网上也有很多资料,这里就不详细介绍了。

3.2 python-jsonpath-rw

python 使用 jsonpath 的第三方库有很多,这里我推荐 python-jsonpath-rw,主要是因为这个库的 github star 较多,说明比较被认可。

简单的示例如下:

>>> from jsonpath_rw import parse
>>> test_data = {'a': {'b': {'c': {'d': {'e': 'target'}}}}}
>>> jsonpath_expr = parse('$..d')  # 查找所有名称为 d 的子 json 节点
>>> found = jsonpath_expr.find(test_data)
>>> print(found.pop().value)
{'e': 'target'}

总结

本文针对实际开发中遇到的 python 语言 json 数据解析的困难,介绍了 demjson 库、jsonpath 以及 python-jsonpath-rw 库,提出了“核心数据不变性” 的论断,希望可以给大家以启迪。

关于 json 解析程序的健壮性问题,除了 jsonpath 的方案,实际还可以根据一份标准输出值,来动态生成解析路径。不过该方案涉及到历史数据的存储、旧规则失效判定等细节问题,而这些内容和 python 官方 json 解析工具的局限性有些远,所以就没有介绍。希望以后有机会可以和大家分享这一主题。

参考与资源


原创不易,盗版必究

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值