☰
深入浅出Python中的JSON操作和实践
2026/10/30 2:54:57 网站建设 项目流程

前言


json是标准库里最"看起来简单"、实际最容易写错细节的模块之一。加载、转储,两个函数就够用——直到你遇到中文变成\uXXXX、datetime对象报TypeError、读文件时编码不对导致解析失败、同一个键在 JSON 里出现两次却只留下一个。


本文把json模块的关键参数逐个讲清,重点放在那些"默认行为不符合直觉"的地方:ensure_ascii默认是True(中文会被转义)、indent会顺带改变分隔符、default是处理不可序列化对象的正确钩子、object_pairs_hook能拿到重复键的原始对。同时点破两个 JSON 格式本身的限制:没有注释,也没有日期类型。


代码都基于 Python 3,本机没有解释器,只能逐行人工推演,不声称运行过。


一、四个入口与它们的签名


模块对外只有四个常用函数,两两对称:


# 适用于 Python 3.8+
import json

# 对象 <-> 字符串
s = json.dumps({"a": 1}) # dict -> str
obj = json.loads(s) # str/bytes/bytearray -> Python 对象

# 对象 <-> 文件(fp 需要有 write / read 方法)
with open("data.json", "w", encoding="utf-8") as f:
json.dump({"a": 1}, f)

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

必须记住:dumps/loads带s(string),dump/load不带s,操作的是文件对象。这对命名是最常见的笔误来源。


dumps的完整参数表(关键字专用)是:skipkeys、ensure_ascii、check_circular、allow_nan、cls、indent、separators、default、sort_keys。loads则是:cls、object_hook、parse_float、parse_int、parse_constant、object_pairs_hook。从 3.6 起所有可选参数都是关键字专用,位置传参会报错。


二、ensure_ascii:中文为什么会变样


ensure_ascii的默认值是True。这意味着:默认情况下,所有非 ASCII 字符都会被转义成\uXXXX形式。


# 适用于 Python 3.8+
import json

data = {"城市": "北京"}

print(json.dumps(data))
# 默认输出里"城市""北京"会变成 \u 转义序列

print(json.dumps(data, ensure_ascii=False))
# 传 False 才会原样保留中文字符

转义本身不改变语义——json.loads能把两种形式都还原成同样的 Python 字符串。所以这不是"错误",只影响可读性和文件体积。但如果你要把 JSON 给人看,或者给需要可读文本的下游用,就该显式传ensure_ascii=False;写文件时还要保证文件是以 UTF-8 打开,否则可能因为编码不匹配而写入失败。


# 适用于 Python 3.8+
import json

with open("out.json", "w", encoding="utf-8") as f:
json.dump({"城市": "北京"}, f, ensure_ascii=False, indent=2)

注意open里那个encoding="utf-8":不写它就会用平台默认编码,在 Windows 上通常不是 UTF-8,于是中文可能写坏、读回来json.load直接抛JSONDecodeError(这是json模块在解析失败时抛出的异常类型)。跨平台代码里,编码一定要显式写。


三、格式化:indent、sort_keys与separators


indent可以让输出带上换行与缩进,方便人读。它接受正整数(缩进这么多空格),也接受字符串(比如"\t")。传None(默认)则不换行。


有一个容易被忽略的联动:separators的默认值取决于indent。当indent为None时,默认分隔符是紧凑的(', ', ': ');当indent不是None时,默认分隔符变成(',', ': ')——即逗号后不再加空格,避免行尾多出空白。如果你为了减小体积手动传了separators=(",", ":"),那就完全紧凑、没有空格。


sort_keys=True会让字典按键排序后输出。默认是False,也就是保持插入顺序。官方文档明确说明:json模块的编码器与解码器默认保持输入输出顺序,只有底层容器本身无序时顺序才会丢失。


这一点和 Python 的字典有关:从 3.7 起,字典保证保持插入顺序(3.6 是实现细节,不要依赖)。所以在 3.7 及以上,dumps不排序也会原样保留键顺序。注意sort_keys是按转换后的字符串键排序,而不是按原始键类型排序——如果键是数字,排序结果可能和你预期的不一样。




参数默认值作用常见误用



ensure_asciiTrue是否转义非 ASCII以为默认输出中文

indentNone缩进,int 或 str以为只影响缩进,忽略它改变分隔符

sort_keysFalse按键排序输出在 3.7+ 上误以为必须开才能保序

defaultNone不可序列化对象的兜底函数误当成object_hook的搭档

separators随indent变控制项与项之间的分隔符手写后与缩进风格冲突



四、default:处理datetime等不可序列化对象


JSON 的类型只有:对象、数组、字符串、数字、true/false、null。没有日期类型。这是 JSON 格式本身的限制,不是 Python 的。


所以datetime、date、Decimal、自定义类实例,json.dumps都不认识,默认会抛TypeError。官方文档给出的扩展方式有两种:


方式一:传default回调。当编码器遇到不能序列化的对象时,调用这个函数,它的返回值会被当作可序列化的对象继续处理。如果它也无法处理,应当抛出TypeError。


# 适用于 Python 3.8+
import datetime
import json


def json_default(obj):
if isinstance(obj, (datetime.datetime, datetime.date)):
return obj.isoformat() # 转成 ISO 8601 字符串
if isinstance(obj, set):
return sorted(obj)
raise TypeError(f"无法序列化的类型: {type(obj).__name__}")


payload = {"when": datetime.datetime(2026, 10, 7, 12, 0, 0)}
print(json.dumps(payload, default=json_default, ensure_ascii=False))

方式二:继承json.JSONEncoder并覆写default方法。这是cls参数的用途。仅当需要复用(多处都按这套规则编码)时才值得,单点使用直接用default更省事。


反序列化时的对应问题是:isoformat()出来的字符串读回来还是字符串,不会自动变回datetime。要还原就得自己解析(比如用datetime.fromisoformat,注意它接受的格式范围随版本扩展过)。JSON 里没有类型标注,谁转成字符串的、按什么格式转的,只能靠约定。


五、object_pairs_hook与重复键


JSON 标准允许对象里出现重复的键(虽然不推荐)。Python 的json.loads默认把它们塞进dict,后出现的覆盖先出现的,前面的静默丢失。


如果你想看到"原始键值对序列",用object_pairs_hook:它接收一个由键值对组成的列表,返回你构造的对象。官方文档指出,如果同时设置了object_hook,object_pairs_hook优先。


# 适用于 Python 3.8+
import json


def no_dup_pairs(pairs):
"""检测重复键;有重复就抛错,拒绝静默覆盖。"""
seen = {}
for key, value in pairs:
if key in seen:
raise ValueError(f"JSON 中存在重复键: {key!r}")
seen[key] = value
return seen


raw = '{"a": 1, "a": 2}'
print(json.loads(raw)) # 后值覆盖前值
print(json.loads(raw, object_pairs_hook=no_dup_pairs)) # 抛错,暴露问题

这个钩子的另一个用途是保序地构造有序结构——虽然 3.7 起字典已保序,这个用途的必要性下降了,但"检测重复键"的价值依然存在。对外部数据做严格校验时,重复键往往是异常信号。


六、数字:大整数与浮点精度


JSON 的数字不带类型标注。json解码时默认这样映射:带小数点或指数的解析成float,否则解析成int。


两个要留意的地方:


大整数。Python 的int是任意精度的,但出于安全考虑,从 3.11 起,默认的parse_int会对整数字符串的长度设限(利用解释器自身的整数字符串转换长度限制),以避免超长数字造成的拒绝服务攻击。如果你确实要处理超长整数,需要显式传自己的parse_int。


浮点精度。JSON 里的0.1解析成 Python 的float,而float是二进制浮点,无法精确表示十进制的 0.1。所以json.loads("0.1") + json.loads("0.2")不会等于json.loads("0.3")——这是浮点本身的实现特性,不是json的 bug。要精确的十进制运算,可以让parse_float用decimal.Decimal来构造。


# 适用于 Python 3.8+
import decimal
import json

print(json.loads("0.1") + json.loads("0.2") == json.loads("0.3")) # False

exact = json.loads("0.1", parse_float=decimal.Decimal)
print(exact, type(exact).__name__)

另外,json.dumps默认allow_nan=True,会把float('nan')、float('inf')写成NaN、Infinity。这些不是合法的 JSON。如果输出要交给严格的其他语言解析器,应传allow_nan=False,让它在遇到这些值时直接报错。


七、JSON 的两个天生缺失:注释与类型


JSON 没有注释。官方规范里根本不支持注释。所以{"a": 1} // 说明这种写法解析会失败。需要配置里带注释,应当换成别的格式(比如tomllib能读的 TOML,从 3.11 起进入标准库),而不是自己用正则去"删注释再解析"——那种做法会把字符串里的//也误删。


JSON 没有日期、没有二进制、没有自定义类型。这些都必须先转成字符串或经过default转换。转换规则由你和你的下游共同约定,JSON 不提供任何元信息。


常见坑点



  1. 以为dumps默认输出中文。


❌json.dumps({"城市": "北京"})后抱怨中文全变成了\u转义。 ✅ensure_ascii默认为True;要保留原文需传ensure_ascii=False,并保证文件以 UTF-8 打开。



  1. open不写encoding。


❌ 读写 JSON 文件时不指定编码,在 Windows 上读写中文出错或解析失败。 ✅ 读写都显式写encoding="utf-8";json.load遇到非法文档会抛JSONDecodeError。



  1. 把default当成反序列化的钩子。


❌ 期待用default在读取时把字符串还原成datetime。 ✅default只在编码阶段处理不可序列化对象;反序列化的还原要靠object_hook或自己解析。



  1. 给 JSON 加注释。


❌ 在.json文件里写//或#注释,再让程序解析。 ✅ JSON 格式不支持注释;配置类数据改用支持注释的格式,不要用正则剥注释再解析。



  1. 以为indent只影响缩进。


❌ 设了indent=2后发现行尾多了空格或没有空格,与自己手写的separators冲突。 ✅ 记住indent不为None时默认分隔符会变成无空格的紧凑形式;需要精确控制就显式传separators。



  1. 在 3.7+ 上仍用sort_keys来"保证顺序"。


❌ 认为不排序键顺序就会乱。 ✅ 3.7 起字典保证插入顺序,json也保持顺序;sort_keys的用途是让输出便于比对,而非保序。



  1. 忽略重复键被静默覆盖。


❌ 对外部数据直接json.loads,同一键出现两次时前面那份悄悄丢了。 ✅ 用object_pairs_hook检测重复键并报错;注意它优先于object_hook。



  1. 把NaN/Infinity当成合法 JSON。


❌ 默认序列化结果里带NaN,再交给别的语言的解析器读取时失败。 ✅ 这类值不是合法 JSON;需要严格输出时传allow_nan=False提前暴露问题。


总结




需求做法关键参数



中文可读输出关掉 ASCII 转义ensure_ascii=False

格式化输出缩进 + 可选排序indent、sort_keys、separators

序列化日期/自定义对象编码阶段的兜底default=或覆写JSONEncoder

检测重复键拿到原始键值对object_pairs_hook=

高精度十进制替换浮点解析parse_float=decimal.Decimal

严格 IO 校验拒绝非法字面量allow_nan=False



json模块的"简单"是一种错觉:函数只有四个,参数只有十来个,但每一个默认值背后都有取舍。记住三句话就够了——中文默认会被转义、日期必须自己转换、JSON 里没有注释也没有日期类型。剩下的细节,请以json模块官方文档的当前版本为准。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询