JSON 能解析就能上线吗?用 Python 拦住重复键与非有限数值

举报
yd_217714792 发表于 2026/10/26 17:21:54 2026/10/26
【摘要】 通过实际测试的 Python 标准库预检器,拦截 JSON 重复键、NaN 与浮点溢出,并解释有界读取、BOM、退出码和日志不回显敏感内容的实现边界。

从一个容易被忽略的配置问题说起

在准备部署配置时,我们经常把“json.loads 没报错”当作检查通过。但语法解析和配置可信是两件事。例如同一个对象里两次出现 port,Python 标准库默认保留最后一个值,前面的配置会悄悄丢掉。这不一定导致程序立即崩溃,却可能让人工审阅与程序实际运行的端口不一致。

本文在本地构建一个只依赖 Python 标准库的配置预检器,实际验证重复键、非标准数值、浮点溢出、编码和文件大小边界。它不调用云服务,不上传配置,也不修改原始文件。以下实现与测试由 AI 辅助编写,示例已在 Python 3.14 环境实际运行。这里没有声称完成生产部署或云上性能测试。

先复现默认行为

import json
print(json.loads('{"port": 80, "port": 443}'))
# {'port': 443}
print(json.loads('{"value": NaN}'))
# {'value': nan}
print(json.loads('{"value": 1e400}'))
# {'value': inf}

第一种情况丢失了重复键的信息。第二种中的 NaN 不是标准 JSON 数值,但 Python 默认接受它。第三种更隐蔽:1e400 是合法的 JSON 数字写法,转换成 Python 浮点数时却溢出成无穷大。仅设置 parse_constant 不能覆盖第三种情况。

RFC 8259 建议对象成员名称唯一;不同实现对重复名称的处理可能不同。本文选择比通用解析器更严格的配置策略:发现重复键立即失败。这个策略应在团队中说明,而不能宣称所有重复名称文本都会被所有解析器拒绝。

为什么不能先解析后检查重复键

如果先 json.loads,再遍历字典,重复键已经被覆盖,无法可靠恢复。应使用 object_pairs_hook,在每个对象的键值对转成字典之前检查名称是否重复。这个钩子会用于嵌套对象,因此数组中包含的对象也能检查。

键名的比较发生在 JSON 字符串转义解码之后,所以 port 与 \u0070ort 会被视为同一个名称。不同对象中使用相同名称则没有问题,例如两个服务各自包含 port。这一点可以避免把“字段名称复用”误判为冲突。

完整的本地预检实现

保存下面代码为 strict_json.py。命令行接受一个文件路径:成功退出码为 0,失败为 1。失败时只输出问题类别或行列号,避免把配置正文、敏感键名、文件路径直接打印到日志。

"""Local JSON preflight: reject duplicate keys and non-finite numbers."""
import argparse
import json
import math
from pathlib import Path

MAX_BYTES = 1024 * 1024


class InvalidConfig(ValueError):
    pass


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            # Avoid echoing configuration keys, which may themselves be sensitive.
            raise InvalidConfig("duplicate object key")
        result[key] = value
    return result


def finite_float(token):
    value = float(token)
    if not math.isfinite(value):
        raise InvalidConfig("non-finite number")
    return value


def reject_constant(_token):
    raise InvalidConfig("non-standard numeric constant")


def parse_config(raw):
    if len(raw) > MAX_BYTES:
        raise InvalidConfig("file exceeds 1 MiB limit")
    try:
        value = json.loads(
            raw.decode("utf-8-sig"),
            object_pairs_hook=unique_object,
            parse_float=finite_float,
            parse_constant=reject_constant,
        )
    except json.JSONDecodeError as exc:
        raise InvalidConfig(
            f"invalid JSON at line {exc.lineno}, column {exc.colno}"
        ) from None
    except (UnicodeDecodeError, RecursionError, ValueError) as exc:
        if isinstance(exc, InvalidConfig):
            raise
        raise InvalidConfig("invalid encoding, nesting or numeric limit") from None
    if not isinstance(value, dict):
        raise InvalidConfig("configuration root must be an object")
    return value


def read_config(path):
    with Path(path).open("rb") as stream:
        return parse_config(stream.read(MAX_BYTES + 1))


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("path")
    args = parser.parse_args()
    try:
        read_config(args.path)
    except (InvalidConfig, OSError) as exc:
        # OSError may contain private paths; only emit its type.
        reason = str(exc) if isinstance(exc, InvalidConfig) else type(exc).__name__
        print("FAIL: " + reason)
        return 1
    print("OK: JSON preflight passed")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

运行方式:python strict_json.py config.json。

成功输出 OK: JSON preflight passed。重复键会输出 FAIL: duplicate object key。语法错误只报告行列号,不回显原始数据。文件不存在等文件系统异常只打印异常类型,不输出可能含有用户名的本地路径。

四个容易漏掉的实现细节

一是同时使用 parse_constant 和 parse_float。前者拒绝 NaN、Infinity、-Infinity;后者在转换后使用 math.isfinite 拒绝溢出。拒绝无穷大并不等于实现十进制精确运算,0.1 等普通小数仍使用二进制浮点。如果涉及货币,应另行评估 decimal.Decimal、精度策略及下游序列化。

二是对文件做有界读取。先读取 MAX_BYTES + 1 个字节,再判断是否超过限制,避免先把很大的文件整体读入内存。这里的 1 MiB 是演示工具的策略参数,不是 JSON 标准限制,也不是对敌意输入的完整资源隔离。

三是把 UTF-8 BOM 的处理写成明确策略。utf-8-sig 同时接受普通 UTF-8 和带 BOM 的 UTF-8,不自动猜测其他字符编码。无效 UTF-8 会明确失败,避免使用替换字符悄悄损坏配置内容。

四是区分通用 JSON 与应用配置。JSON 的顶层本来可以是数组或标量;这里强制顶层对象,是本工具的配置约束。若业务允许数组,应修改这一条,而不是把数组称作不合法 JSON。

实际验证结果

本地使用 unittest 运行了 14 个测试方法,全部通过。其中子测试还覆盖了多种数值、根类型和错误输入。检查项包括:普通嵌套对象、顶层重复键、嵌套重复键、转义后的重复键、不同对象中的同名字段、三种非标准数值、1e400 溢出、大整数、UTF-8 BOM、语法及编码错误、根对象约束、大小上限、命令行退出码与敏感名称不回显、有界文件读取。

其中两个边界尤其值得固定成回归测试:9007199254740993 在 Python 整数中应保持原值,但不能据此推断 JavaScript Number 或后续数据库也保持精度;port 与转义后的同名键必须被识别为重复,而不同对象的同名键应通过。

放进部署流程时还缺什么

这个工具只回答配置文本是否满足上述解析规则,不检查端口范围、必填字段、网络可达性或云账号权限。实际使用时,应继续加入 schema、业务约束和独立的服务健康检查。例如 port 应是 1 到 65535 的整数,enabled 不应接受字符串 true。

它也不展开环境变量,不执行表达式,不从网络下载配置。在 CI 中可把退出码作为后续步骤的前置条件,但不要把“预检通过”写成“部署成功”。对于不可信输入,还需要进程级资源限制和更严格的深度限制;捕获 RecursionError 只用于把解析失败转换成可读错误,并不能代替沙箱。

参考资料:Python 官方文档中 json 模块章节;RFC 8259(The JavaScript Object Notation (JSON) Data Interchange Format)。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。