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 草案一致。校验通过后再进入业务计算,能把“数据格式错误”和“计算逻辑错误”清晰分离。
嵌套结构处理的实践要点
处理深层嵌套时,建议遵循三条原则。第一,先校验后使用:把 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": ...} 的字典,读取时再还原,这是科学计算中常见的自定义序列化约定。
ensure_ascii=False 与 indent=2,既保留中文可读性,又让版本控制中的 diff 更清晰;读取时始终显式声明 encoding="utf-8"。eval() 解析 JSON 文本,也不要用 json.loads 去解析来源不可信的巨型文件而不设大小限制;对不可信输入应先做 schema 校验再使用。