跳转到内容
新建笔记

Python JSON:文本、文件、类型转换与读取约束

json 把 Python 中的基本数据转换为 JSON 文本,也能把 JSON 文本解析回来。它适合配置、接口消息和跨语言交换;转换过程保留约定的数据值,不保留 Python 对象身份或所有类型。

本文按 Python 3.11 的标准库接口组织示例。

四个接口:字符串与文件分别处理

跳转到“四个接口:字符串与文件分别处理”
接口输入输出或作用
json.dumps(obj)Python 对象返回 str
json.loads(text)JSON str;也支持符合要求的 bytes、bytearray返回 Python 对象
json.dump(obj, file)对象与能写入字符串的文件对象向文件写 JSON 文本
json.load(file)能读取 JSON 内容的文件对象解析一个 JSON 文档

dumps 返回的不是字节流。写普通 JSON 文件时,明确使用文本模式和 UTF-8;传输字节时,再对字符串调用 .encode("utf-8")。ensure_ascii=False 控制中文是否保留为字符,它并不负责设置文件编码。Python json:基本接口

下面是可直接运行的往返示例。它先创建数据和文件,不依赖工作目录里预先存在 person.json。

import json
from pathlib import Path
from tempfile import TemporaryDirectory
person = {
"name": "小林",
"age": 20,
"active": True,
"address": {"city": "北京", "street": "中山路"},
"nickname": None,
}
text = json.dumps(person, ensure_ascii=False, indent=2, allow_nan=False)
assert isinstance(text, str)
assert "小林" in text
assert json.loads(text) == person
assert json.loads(text.encode("utf-8")) == person
with TemporaryDirectory() as directory:
path = Path(directory) / "person.json"
with path.open("w", encoding="utf-8") as file:
json.dump(person, file, ensure_ascii=False, indent=2, allow_nan=False)
file.write("\n")
with path.open("r", encoding="utf-8") as file:
restored = json.load(file)
assert restored == person

值能往返,不代表类型和关系原样往返

跳转到“值能往返,不代表类型和关系原样往返”
Python 输入JSON 表示默认解析结果与边界
dictobject恢复为 dict;JSON 键是字符串
list、tuplearray都恢复为 list,元组类型丢失
strstring恢复为 str
int、有限 floatnumber通常恢复为 int、float;接收方的数值范围和精度可能不同
True、Falsetrue、false恢复为布尔值
Nonenull恢复为 None
set、bytes、日期、自定义实例无默认编码先制定转换约定,或提供明确的 default 编码函数

json.loads(json.dumps({1: "one"})) 得到 {"1": "one"}。如果输入同时含整数键 1 和字符串键 "1",转换后还会发生名称冲突。接口数据最好从开始就使用字符串键。

两个字段引用同一个列表时,JSON 可以写出两份相同的数组,但解析结果不会保留原来的共享关系;循环引用默认会报错。它因此不能代替 copy 的对象图复制 或 pickle 的 Python 对象序列化。类型转换表

格式控制与常见解析误解

跳转到“格式控制与常见解析误解”

indent=2 便于阅读;separators=(",", ":") 删除不必要的分隔空格;sort_keys=True 便于比较结果,但不等于实现某个密码学签名所要求的规范化格式。

JSON 字符串和键使用双引号,不支持 Python 的单引号字面量、注释或尾随逗号。JSON 文档的顶层可以是数组、对象,也可以是一个字符串、数字、布尔值或 null;要求顶层必须为对象是应用自己的约定。

Python 默认编码允许输出 NaN、Infinity,默认解析也接受这些扩展值;其他 JSON 实现未必接受。编码时可设 allow_nan=False。解析对象遇到重复键时默认保留最后一个值,若业务不允许覆盖,需要自己拒绝。兼容性说明

为配置读取加上明确约束

跳转到“为配置读取加上明确约束”

下面的读取函数只接受字符串输入,并明确拒绝过长输入、重复键、非有限浮点数及非对象顶层。parse_constant 拒绝 NaN 等名字;parse_float 还处理 1e400 这类普通数字文本转换为无穷大的情况。这不是完整的业务 schema 验证,字段名、类型、范围仍应另外检查。

import json
import math
def load_config(text):
if not isinstance(text, str):
raise TypeError("configuration must be text")
if len(text.encode("utf-8")) > 65_536:
raise ValueError("configuration exceeds 64 KiB")
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate key: {key}")
result[key] = value
return result
def finite_float(token):
value = float(token)
if not math.isfinite(value):
raise ValueError("non-finite number")
return value
def reject_constant(token):
raise ValueError(f"unsupported numeric constant: {token}")
result = json.loads(
text,
object_pairs_hook=unique_object,
parse_float=finite_float,
parse_constant=reject_constant,
)
if not isinstance(result, dict):
raise ValueError("configuration root must be an object")
return result
assert load_config('{"name":"传感器","gain":1.25}') == {
"name": "传感器", "gain": 1.25
}
for invalid in ('{"x":1,"x":2}', '{"x":NaN}', '{"x":1e400}', '[1,2]'):
try:
load_config(invalid)
except ValueError:
pass
else:
raise AssertionError(f"unexpectedly accepted: {invalid}")

数字需要十进制精确语义时,可以通过 parse_float=decimal.Decimal 接收浮点形式的数字文本;这同时改变了解析结果的类型,后续再次写出 JSON 时也要约定编码方式。strict=True 主要约束字符串中的控制字符,不能替代上述重复键或非有限数检查。解码器参数

解析语法失败时,JSONDecodeError 提供行号、列号和位置,可用于报错定位。限制输入大小只能控制一部分资源消耗;复杂度、嵌套深度与字段验证仍是读取流程的一部分。

一个文件包含一个文档

跳转到“一个文件包含一个文档”

连续对同一文件调用两次 json.dump,通常会拼成两个相邻文档,不能再作为单个 JSON 文档交给 json.load。少量记录可写成一个列表;逐行记录则应明确采用 JSON Lines 约定,每行单独编码、单独解析,且每条记录内部不能使用跨行缩进。这两种存储布局不要混用。