4. JSON文件读写与校验

在科学计算中,实验参数、仪器配置、仿真结果元数据往往需要以结构化方式保存。JSON(JavaScript Object Notation)凭借纯文本、跨语言、层级表达自然的特点,成为配置文件与轻量数据交换的首选格式。Python 标准库中的 json 模块无需安装即可使用,它把 Python 对象与 JSON 文本互相转换,这一对过程分别称为序列化(Python → JSON 文本)与反序列化(JSON 文本 → Python)。

需要先建立一个关键认识:JSON 只支持有限的数据类型——对象(映射为 Python dict)、数组(list)、字符串、数字、布尔值和 null(映射为 None)。Python 的 tuple 会被写成数组,set、complex、datetime、numpy.ndarray 等类型默认无法直接序列化,会抛出 TypeError。因此科学计算场景下,通常要先把数组转成列表、把时间转成 ISO 格式字符串,再交给 json 处理。

序列化与反序列化的基本操作

最常用的四个函数是 json.dumps() / json.loads()(字符串级)与 json.dump() / json.load()(文件级)。写文件时推荐显式指定 ensure_ascii=False 以保留中文,并用 indent 控制缩进便于人工检查:

import json

config = {
    "experiment": "热传导系数测定",
    "operator": "张工",
    "samples": [
        {"id": "S01", "thickness_mm": 2.5, "temps_C": [25.0, 41.3, 58.7]},
        {"id": "S02", "thickness_mm": 3.0, "temps_C": [25.0, 39.8, 55.2]},
    ],
    "valid": True,
    "note": None,
}

with open("config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, ensure_ascii=False, indent=2)

with open("config.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded["samples"][1]["temps_C"][-1])   # 55.2

读取时务必使用 encoding="utf-8",否则在部分平台上中文会乱码或解码失败。反序列化得到的 dict 中,所有数字都是 int 或 float,原来的 tuple 已变成 list,这一点在后续做类型判断时容易踩坑。

嵌套结构的访问与安全处理

真实配置文件常有四五层嵌套。直接用 data["a"]["b"]["c"] 链式取值,一旦中间某层缺失就抛 KeyError,程序中断。稳健的写法是逐层判断,或使用 dict.get() 提供默认值:

temps = loaded.get("samples", [{}])[0].get("temps_C", [])
if not temps:
    print("警告:缺少温度序列,使用默认值")

对于数组型嵌套,遍历时同样要防止元素不是字典。更系统的做法是把“结构是否合法”交给 schema 校验,而不是散落在业务代码里做大量 if 判断。

用 schema 做结构校验

JSON Schema 是一套描述 JSON 文档结构的规范,第三方库 jsonschema 可实现校验。它声明字段类型、必填项、数值范围、数组元素结构等,校验失败时给出具体路径,比手工判断更可靠。下面是一份针对上述配置的 schema 示例:

from jsonschema import validate, ValidationError

schema = {
    "type": "object",
    "required": ["experiment", "samples"],
    "properties": {
        "experiment": {"type": "string", "minLength": 1},
        "operator": {"type": "string"},
        "valid": {"type": "boolean"},
        "samples": {
            "type": "array",
            "minItems": 1,
            "items": {
                "type": "object",
                "required": ["id", "thickness_mm", "temps_C"],
                "properties": {
                    "id": {"type": "string", "pattern": "^S\\d{2}$"},
                    "thickness_mm": {"type": "number", "exclusiveMinimum": 0},
                    "temps_C": {
                        "type": "array",
                        "items": {"type": "number"},
                        "minItems": 2,
                    },
                },
            },
        },
    },
}

try:
    validate(instance=loaded, schema=schema)
    print("校验通过")
except ValidationError as e:
    print("校验失败:", e.message)
    print("出错路径:", list(e.path))

注意 exclusiveMinimum 在较新草案中直接写数值,旧写法需配合 minimum 与布尔值,实际使用时应确认 jsonschema 版本与所采用的 schema 草案一致。校验通过后再进入业务计算,能把“数据格式错误”和“计算逻辑错误”清晰分离。

JSON 读写与校验数据流 Python 对象 dict / list / str int / float / bool ndarray 需先转 list 序列化 json.dump / dumps ensure_ascii=False indent=2 config.json UTF-8 纯文本 对象 / 数组 / 标量 null ↔ None 反序列化 json.load / loads encoding="utf-8" tuple 变 list schema 校验 jsonschema.validate required / type pattern / minItems 业务计算 读取 samples 温度序列处理 结果写回 JSON 校验失败:ValidationError e.message 说明原因,list(e.path) 定位出错字段路径 不通过 通过

嵌套结构处理的实践要点

处理深层嵌套时,建议遵循三条原则。第一,先校验后使用:把 schema 校验放在读取之后、计算之前,避免脏数据流入数值计算。第二,用路径定位问题:ValidationError 的 path 属性给出从根到出错字段的键序列,例如 ['samples', 1, 'thickness_mm'],可直接打印给用户。第三,写回时保持结构一致:若在计算中给每个样本追加了 result 字段,写回前应确认 schema 允许额外字段,否则下次读取校验会失败;可在 schema 中设置 "additionalProperties": true 或显式声明该字段。

此外,若数据量较大,json 全量加载会占用较多内存,此时可考虑逐行 JSON(JSON Lines)格式,每行一个独立对象,配合 for line in f 逐条 json.loads,既降低内存又便于流式处理。对于需要保留 numpy 数组形状与 dtype 的场景,可在序列化前把数组转为 {"__ndarray__": ..., "dtype": ..., "shape": ...} 的字典,读取时再还原,这是科学计算中常见的自定义序列化约定。

写 JSON 时统一使用 ensure_ascii=False 与 indent=2,既保留中文可读性,又让版本控制中的 diff 更清晰;读取时始终显式声明 encoding="utf-8"。
不要用 eval() 解析 JSON 文本,也不要用 json.loads 去解析来源不可信的巨型文件而不设大小限制;对不可信输入应先做 schema 校验再使用。