Python 中如何优雅地处理 JSON5 文件

JSON5 概述

JSON5 is an extension to the popular JSON file format that aims to be easier to write and maintain by hand (e.g. for config files) . From json5.org/

JSON5 是 JSON 的一个超集,通过引入部分 ECMAScript 5.1 的特性来扩展 JSON 的语法,以减少 JSON 格式的某些限制。同时,保持兼容现有的 JSON 格式。从官网作者介绍来看,JSON5 注重的是更人性化的编写和维护,一般用于软件的配置文件场景。

JSON5 拓展了 JSON 的能力,支持以下特性:

  • 注释

  • 尾随逗号

  • 单引号

  • 字符串字面量

  • 数字 (包括 Infinity, NaN, hexadecimal)

  • 对象/数组字面量

  • 多行字符串

格式官方文档 spec.json5.org/

遇到的问题

最近团队开始对接华为鸿蒙系统,在鸿蒙工程中,配置文件都是 json5 文件格式。例如存储 APP 信息的 app.json5

TypeScript 复制代码
{
  "app": {
    // 包名
    "bundleName": "com.xxx.sample",
    // 厂家信息
    "vendor": "sample",
    // 版本号
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:app_launcher",
    "label": "$string:app_name",
    "generateBuildHash": true
  }
}

JSON5 文件中会存储数据信息和注释信息,有助于在阅读的时候了解数据结构。

需求:脚本修改 JSON5 文件时保留注释信息

需求:在 CD 构建时,根据传入的版本号修改 app.json5 中的版本号信息,然后将修改的文件提交到对应的版本分支上。

处理方式:因为是 python 的脚本,所以就找了 python 中可以操作 json5 的库,首先就是最常见的json5 库,它提供与标准 json 库类似的 API,可以读写 json5 文件,例如下面的例子

TypeScript 复制代码
import json5

data = json5.load(open('app.json5','r'))
print(json5.dumps(data, indent=4))
// 输出结果
{
    "app": {    
        "bundleName": "com.xxx.sample",
        "vendor": "sample",
        "versionCode": 1000000,
        "versionName": "1.0.0",
        "icon": "$media:app_launcher",
        "label": "$string:app_name",
        "generateBuildHash": true    
    }
}

可以看到虽然 API 可以正确的解析和输出文件数据,但是注释信息却没有了 。使用官方推荐的nodejs 版本 json5 库也是一样的结果。也有人开issue提需求是否可以提供 API 可以保留注释信息,但是作者最终还是暂时婉拒了这个需求

后续虽然有人提交了支持这个 feature 的 PR,但是也是迟迟没有合入。所以目前来说较为官方的库都是没有支持读写 json5 文件时保留注释信息的。

虽然在搜索解决方案的时候,也有人提出说直接使用一个字段(例如"comment")存储注释信息,但是个人认为这是非常不优雅的:明明 JSON5 推出就是支持注释的,最终又要回退到 JSON🙄。

解决方式

虽然官方库没有支持读写时保留注释信息,但是还是有部分扩展库是支持的。这些扩展库也在 JSON5 的 Github 的 Wiki 中In-the-Wild部分列举了出来。

json-five

其中json-five这个库支持读写 JSON5 文件时保留注释信息

下面是官方提供的一个 demo

Python 复制代码
from json5.loader import loads, ModelLoader
from json5.dumper import dumps, ModelDumper
from json5.model import BlockComment
json_string = """{"foo": "bar"}"""model = loads(json_string, loader=ModelLoader())print(model.value.key_value_pairs[0].value.wsc_before)  # [' ']
model.value.key_value_pairs[0].key.wsc_before.append(BlockComment("/* comment */"))
dumps(model, dumper=ModelDumper()) # '{/* comment */"foo": "bar"}'

可以看出,虽然json-five支持了保留注释信息,但是在数据的操作上非常麻烦,基本不能像使用 json 库时将数据当做 dict 进行操作,这样很不优雅😓。

扩展 json-five

于是对现有数据结构进行了扩展,支持[]操作符进行获取或者赋值,简化 json5 操作流程。

Python 复制代码
# -*- coding: UTF-8 -*-
'''
支持保留注释和格式的JSON5处理工具
'''
# pip3 install json-five
import json5
from json5.dumper import modelize
from json5.model import JSONArray, JSONObject, String, JSONText, Value, KeyValuePair, walk

# 重写JSONObject的__getitem__方法,支持通过字符串获取值,如果不存在则返回None
def _find(self, key):
    if isinstance(key, str) and isinstance(self, JSONObject):
        for item in self.key_value_pairs:
            if isinstance(item.key, String):
                if item.key.characters == key:
                    return item.value
    elif isinstance(key, int) and isinstance(self, JSONArray):
        return self.values[key]
    elif isinstance(self, JSONText):
        return self.value[key]
    return None

# 重写JSONObject的__setitem__方法,支持通过字符串设置值,如果不存在则抛出异常
def _jsonobj_set(self: JSONObject, key: str, value: Value):
    new_item = KeyValuePair(modelize(key), value)
    for index in range(len(self.key_value_pairs)):
        item = self.key_value_pairs[index]
        if isinstance(item.key, String):
            if item.key.characters == key:
                old_value = self.values[index]
                new_item.value.wsc_after = old_value.wsc_after
                new_item.value.wsc_before = old_value.wsc_before
                new_item.value._tok = old_value._tok
                new_item.value._end_tok = old_value._end_tok
                self.values[index] = new_item.value
                return
    raise KeyError(key)
    # self.keys.append(new_item.key)
    # self.values.append(new_item.value)

# 重写JSONArray的__setitem__方法,支持通过整数设置值,如果不存在则抛出异常,如果存在则覆盖原值
def _jsonarray_set(self: JSONArray, index: int, value: Value):
    self.values[index] = value

# 重写JSONObject的str_keys方法,支持返回所有字符串类型的keys,如果不存在则返回[]
def _jsonobj_str_keys(self: JSONObject):
    return [item.characters for item in self.keys if isinstance(item, String)]

JSONObject.__getitem__ = _find
JSONObject.__setitem__ = _jsonobj_set
JSONObject.str_keys = _jsonobj_str_keys
JSONArray.__getitem__ = _find
JSONArray.__setitem__ = _jsonarray_set
JSONText.__getitem__ = _find

# 加载JSON5文件,保留注释和格式,返回一个Model对象
def loadjson5_with_comment(path: str):
    return json5.load(open(path, 'r'), loader=json5.loader.ModelLoader())

# 保存JSON5文件,保留注释和格式
def savejson5_with_comment(data, path: str):
    return json5.dump(data, open(path, 'w'), dumper=json5.dumper.ModelDumper())

# 寻找所有JSONObject中key为keyword的对象,返回一个列表,如果不存在则返回[]
def find_jsonobjects(model, keyword: str) -> list[JSONObject]:
    items = []
    for item in walk(model):
        if isinstance(item, JSONObject):
            for key in item.keys:
                if isinstance(key, String) and key.characters == keyword:
                    items.append(item)
    return items

最终实现以下效果,最大限度地保留的文件格式和注释信息,优雅地满足了需求🥳。

Python 复制代码
file_path = 'app.json5'
model = loadjson5_with_comment(file_path)
model['app']['versionName'] = modelize('1.1.1')
savejson5_with_comment(model, file_path)
# 修改后文件内容
{
  "app": {
    // 包名
    "bundleName": "com.xxx.sample",
    // 厂家信息
    "vendor": "sample",
    // 版本号
    "versionCode": 1000000,
    "versionName": '1.1.1',
    "icon": "$media:app_launcher",
    "label": "$string:app_name",
    "generateBuildHash": true
  }
}

其他库

In-the-Wild中可以看到有很多库支持 json5,但是测试前面的几个 python 和 js 的库,目前只有 json-five 这个支持保留注释信息(也可能是我使用姿势问题?)

总结

JSON5 作为 JSON 的扩展,提供了更人性化的语法,非常适合静态配置文件场景,可以目前官方的库 API 读写文件时不支持保留注释信息(往往可能是配置文件中关键信息),在一些自动化场景稍显不便。

虽然目前可以通过三方库+扩展的方式达到一个基本可用的状态,还是希望官方能对此能力进行支持,让 JSON5 的处理更优雅~

相关推荐
yilylong1 小时前
鸿蒙(Harmony)实现滑块验证码
华为·harmonyos·鸿蒙
baby_hua1 小时前
HarmonyOS第一课——DevEco Studio的使用
华为·harmonyos
HarmonyOS_SDK1 小时前
融合虚拟与现实,AR Engine为用户提供沉浸式交互体验
harmonyos
- 羊羊不超越 -3 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
Mephisto.java4 小时前
【大数据学习 | kafka高级部分】kafka的优化参数整理
大数据·sql·oracle·kafka·json·database
长弓三石4 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
沐雪架构师5 小时前
mybatis连接PGSQL中对于json和jsonb的处理
json·mybatis
SameX6 小时前
鸿蒙 Next 电商应用安全支付与密码保护实践
前端·harmonyos
丁总学Java7 小时前
微信小程序,点击bindtap事件后,没有跳转到详情页,有可能是app.json中没有正确配置页面路径
微信小程序·小程序·json
SuperHeroWu77 小时前
【HarmonyOS】键盘遮挡输入框UI布局处理
华为·harmonyos·压缩·keyboard·键盘遮挡·抬起