欢迎光临
我们一直在努力

JSON回读后数字编号键变成字符串:先写清键的还原规则

大家好,我是 SiKi老师。关卡工具里用 {1: "forest", 2: "cave"} 保存进度,再读回来时按 data[1] 查找却报 KeyError,先别怀疑文件写坏了。我会先打印回读后的键和类型,再看保存前的字典里有没有不是字符串的键。JSON 对象的键只能是文本,这一步转换是格式本身决定的。

本文在 Linux 上的 Python 3.12.15 中使用标准库 json 和 re,另用 3.13.5 复跑同一脚本,结果一致。样例全部自拟,在内存中运行,不读写真实存档或关卡表,也没有测试 Unity、Godot 等引擎读取同一份 JSON 的行为。下面讨论的是工具端的键约定,不是引擎统一规定。

一 先做一次保存和回读的往返

用两条自拟关卡记录复现,forest 和 cave 只是示例名称:

import json

levels = {1: "forest", 2: "cave"}
text = json.dumps(levels, ensure_ascii=False)
back = json.loads(text)
print(text)
print(back, back == levels)
print([type(k).__name__ for k in back])

本地输出是 {"1": "forest", "2": "cave"}、{'1': 'forest', '2': 'cave'} False 和 ['str', 'str']。保存时整数键已经被转换成字符串,回读得到的字典和原来不相等,所以 back[1] 会抛出 KeyError,back["1"] 才能取到值。

官方文档对这一点写得很直接:JSON 键值对中的键总是 str 类型,字典转换为 JSON 时所有键都会被强制转换成字符串,因此带非字符串键的字典往返后可能不等于原字典。这不是 bug,也不是写入时丢了数据,而是我把“整数键”这个 Python 里的信息交给了一个表达不了它的格式。

二 混合键会在回读时悄悄合并

接着故意放入一对看起来一样的键:

mixed = {1: "a", "1": "b"}
text = json.dumps(mixed)
print(text)
print(json.loads(text))

本地得到 {"1": "a", "1": "b"} 和 {'1': 'b'}。保存时没有报错,生成的文本里出现了两个同名键;回读时默认保留最后一个,前一条数据就不见了。这里不是读取器丢了一行,而是两条不同的键在字符串化之后撞到了一起。

同一个机制还会影响其他类型的键:True、None、1.5 作为键时分别写成 "true"、"null"、"1.5",回读后都是普通文本。元组键默认抛出 TypeError;若开启 skipkeys=True,这类键会被直接跳过而不报错,我不建议在存档工具里为了“能保存”而打开它。另外,整数键和字符串键混在一起时使用 sort_keys=True,本地会抛出 TypeError,因为两种类型无法直接比较。

所以保存前值得先检查一次:把每个键按 JSON 的规则转换成文本,看有没有两个不同的键得到同一个结果。有冲突就报告,不要让写入函数替你决定留哪一条。

三 回读时按约定显式还原整数键

如果项目明确规定“对象键都是非负整数编号”,就在读取时显式还原,并把不符合约定的键报告出来:

import json
import re

CANONICAL = re.compile(r"0|[1-9][0-9]*")

def restore_int_keys(text):
problems = []

def hook(pairs):
result = {}
for key, value in pairs:
if not CANONICAL.fullmatch(key):
problems.append(("bad_key", key))
elif int(key) in result:
problems.append(("duplicate", key))
else:
result[int(key)] = value
return result

return json.loads(text, object_pairs_hook=hook), problems

用自拟文本 {"1": "a", "01": "b", " 2": "c", "x": "d", "1": "e"} 测试,本地得到 {1: 'a'},问题列表依次是 01、 2、x 三个不合规的键,以及重复的 1。object_pairs_hook 能看到原始的键值对顺序,所以重复键不会像默认读取那样被静默覆盖。

这里我没有直接对每个键调用 int()。本地另测 int(" 1")、int("01")、int("1_0") 都能成功,分别得到 1、1、10,比“编号键”的约定宽松得多。若直接转换,"1" 和 "01" 会合并成同一个编号,和前面讲过的关卡编号前导零是同一类风险。示例只处理一层对象,嵌套结构需要按各自的约定另行处理,本文没有实现通用的类型还原器。

四 编号也可以作为字段值保存

如果你能修改存档格式,另一种做法是不让编号当对象键,而是作为记录里的字段:

records = [{"id": 1, "name": "forest"}, {"id": 2, "name": "cave"}]
text = json.dumps(records, ensure_ascii=False)
assert json.loads(text) == records

本地断言通过。JSON 数字作为值时,默认按整数读回,往返后与原列表相等。代价是查找时要自己建立索引,并且仍要检查重复的 id、缺字段和类型错误;这一步没有省掉验收,只是让类型信息留在格式能表达的地方。

两种方案选哪一种,取决于读取这份 JSON 的还有谁。若引擎脚本或其他工具已经按字符串键读取,擅自改成数组会破坏它们。修改格式前,先列出所有读写方,并在副本上做往返测试,本文没有检查任何真实工程中的读取代码。

五 用往返测试做回归验收

本次独立验证覆盖整数键字符串化、往返不相等、整数查找失败、混合键合并、特殊键的文本形式、元组键报错、skipkeys 跳过、混合排序报错、严格还原与问题报告、数组记录往返以及输入未改变。整个过程只在内存中运行,不涉及网络接口、数据库或引擎的 JSON 解析器。

接手存档或关卡工具时,可以先准备一个包含整数键的小字典,做一次保存和回读,再比较类型和内容。第一处 == 失败的位置,比在界面上找“哪个关卡没加载”更容易定位问题。键的约定写进说明文档,比在每个读取点各自转换一次更可靠。

你遇到的是回读后按整数查不到,还是两条记录合并成了一条?描述这一点,并给出一个自拟的小字典,就能开始排查,不需要上传真实存档。

依据:Python 3.12 json 官方文档,包括 dumps 关于键强制转换为字符串、skipkeys、sort_keys 以及 object_pairs_hook 的说明。示例和表达原创,不引用具体课程信息。

赞(0)
未经允许不得转载:171主机测评 » JSON回读后数字编号键变成字符串:先写清键的还原规则
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址