news 2026/11/2 5:01:56

JSON解析与序列化实战:精度陷阱、Schema校验与故障排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSON解析与序列化实战:精度陷阱、Schema校验与故障排查

1. JSON 在真实项目里到底顶什么用

JSON(JavaScript Object Notation)这个教程要讲的东西,说穿了就一件事:怎么把一段结构化的数据,用一种人看得懂、机器也读得懂的文本形式表达出来,并且在两端之间可靠地传过去、解回来。我做过后端接口、做过数据清洗、也接过前端联调,JSON 出现的频率高到几乎每写十行代码就要碰一次。但真正让我意识到需要把 JSON 系统啃一遍的,是几年前一次跨团队接口对接:对方发来的报文字段全是数字,我这边解析出来金额少了两位小数,排查了大半天,最后发现是某个大整数在另一端用双精度浮点存了一次,尾数被舍掉了。从那以后我就明白,JSON 的语法只有那么几行,可它周边的坑能铺满一整条路。

这篇教程适合三类人。第一类是刚入行的开发者,能读懂 JSON 长什么样,但说不清为什么{"a": 1}里的键必须加双引号、为什么尾部多一个逗号就整个解析失败。第二类是需要和其他系统交换数据的人,比如做数据管道、做爬虫清洗、做配置管理,天天面对各种"看起来像 JSON 但就是解析不了"的文本。第三类是想把 JSON 相关工具链用扎实的人,包括格式化、校验、流式解析、跨语言精度处理。我会从语法规则讲到实操命令、从代码参数讲到故障排查,每一块都配上直接能跑的示例和我自己踩过的坑。

先给一个整体定位:JSON 是一种文本序列化格式,它只负责描述数据的结构,不负责描述数据的含义,也不负责校验业务规则。规范本身短得可以打印在一张 A4 纸上,核心就六个结构符号({ } [ ] : ,)和六种值类型。正因为规范小,各家语言的实现才有机会在细节上做出差异,而这些差异恰恰是线上事故的高发区。理解 JSON,三成在语法,七成在实现差异和工程约束。

1.1 从一次字段丢失说起:格式选择背后的成本

那次事故的细节值得展开。业务需要传一批订单,单号是一串 18 位数字,没有字母。前端拿到之后展示正常,可一旦把它当成数字参与运算或者重新序列化发回去,末尾几位就变了。原因很直接:JavaScript 的Number是 IEEE 754 双精度浮点,能精确表示的整数上限是 2^53 - 1,也就是 9007199254740991,16 位。18 位的单号超过了这个范围,精度必然丢失。JSON 规范里数字没有整数和浮点的区分,只写了一串语法,具体用多少位、用什么类型承载,完全交给解析器决定。Python 的json.loads默认把整数字面量解析成int,任意精度,不会丢;JavaScript 的JSON.parse只能用Number,就丢。

这件事教给我的第一条经验是:凡是标识类、编号类、精度敏感的数值,一律用字符串传输。订单号、身份证号、银行卡号、时间戳(毫秒级还好,但也建议字符串)、经纬度(如果精度要求高)、金额(建议用最小货币单位的整数或字符串)。这条规矩听起来土,但它能消灭掉一整类跨语言对接问题,比事后加一堆校验便宜太多。

第二条经验关于格式选型的成本核算。选一种数据交换格式,成本不在写代码那一刻,而在后面三五年的维护里。JSON 的优势不是"最省字节"或"最快",而是通用性:任何语言的标准库或主流库都支持,浏览器原生支持,日志里能直接看,出问题时随手一个在线工具或命令行就能格式化。它的劣势也很明确:没有类型系统、没有 Schema 强制、文本冗长、不支持注释和二进制。所以判断标准很简单——如果数据要在多个异构系统之间流转、要人能肉眼排查,选 JSON;如果数据量大到带宽或解析耗时成为瓶颈、且两端都是自己人,才考虑二进制格式。

1.2 为什么 JSON 能赢下这场格式战争

早期的数据交换场景里,XML 是主流。XML 强大,有 Schema、有命名空间、有 XPath,但它的问题在于结构表达的成本太高。同样一个用户对象,XML 要写开标签闭标签,属性还是子元素需要专门约定,读起来满屏尖括号。JSON 用{}和[]两个符号把对象和数组区分开,键值对用冒号连接,去掉了所有冗余的闭合标记,可读性提升非常直观。更关键的是,JSON 的语法恰好是 JavaScript 对象字面量的子集,浏览器里eval一下就能用(虽然后来证明eval极度危险,才有了JSON.parse这个专门的安全解析器),这在 Web 早期是决定性的优势。

另一个常被忽略的点是 JSON 的惰性设计。它故意不定义整数和浮点的区别、不定义日期类型、不定义二进制类型、不支持注释、不允许尾随逗号。这些"缺失"在当时被批评为不够完备,但事后看,正是这些缺失让它足够简单,简单到任何人花十分钟就能手写一个解析器,简单到不会因为规范演进产生版本兼容问题。对比一下那些功能丰富的格式,往往要背着历史包袱做兼容,JSON 没有这个负担。

当然,简单也意味着有些活必须由使用者自己补。日期得约定成 ISO 8601 字符串,二进制得用 Base64 编码再塞进字符串,大整数得转字符串,枚举值得靠文档约束。这些约定不属于 JSON 规范,但属于工程规范,一个团队必须把这些约定写下来,否则不同模块各写各的,迟早对不上。

1.3 什么情况下不该硬上 JSON

我不太喜欢"XX 万能"的说法。JSON 有明确的不适用区。第一种是配置文件。JSON 不支持注释,配置文件里想写一句"这个字段是留给灰度环境用的",只能另开一个_comment字段,很别扭。配置文件更适合 YAML 或 TOML,它们支持注释、支持多行字符串,人写起来舒服得多。第二种是大数据量的批量传输。JSON 的键名会重复出现在每一条记录里,一万条记录就重复一万次键名,字节浪费很可观,这时候列式格式或二进制格式优势明显。第三种是强类型契约场景。如果两端都是自家服务,字段类型和结构必须严格对齐,用带 Schema 的二进制格式能省掉大量运行时校验。第四种是需要保留精度的科学计算数据,浮点字面量在文本和二进制之间来回转换会引入误差,专业场景有专门的格式。

判断逻辑我通常这么走:先问这份数据要不要人看。要人看,JSON 或 YAML;不要人看且量大,二进制。再问要不要频繁手改。要手改,YAML 或 TOML;程序生成程序消费,JSON。最后问结构会不会频繁演进。会,且两端独立发布,那就必须配 Schema 校验,JSON Schema 是现成的选择。

2. 语法逐条啃:六种值、转义、数字陷阱

JSON 的语法规范(RFC 8259)读起来不到十页,我把其中最容易被忽略、也最容易引发线上问题的部分抽出来逐条讲。这一节的所有结论我都建议你亲手在命令行试一遍,光看是记不住的。

2.1 结构骨架与六种值类型

JSON 文本的顶层可以是一个对象、一个数组,或者任意一个值。严格来说,RFC 8259 允许顶层是任何 JSON 值,包括单个数字42或字符串"hello",但很多老解析器只接受对象或数组,这是一个兼容性差异点。如果你的数据要被多种语言的库消费,顶层统一用对象{}包裹最稳妥,这是我在多个项目里保平安的做法。

六种值类型分别是:对象({}包裹的键值对集合)、数组([]包裹的有序值序列)、字符串(双引号包裹)、数字、布尔(true/false)、空值(null)。就这六种,没有日期、没有二进制、没有 undefined、没有函数。对象里的键必须是双引号包裹的字符串,这一点和 JavaScript 对象字面量不同,JS 里{a: 1}合法,JSON 里不合法,必须写{"a": 1}。单引号同样不合法。这条规则看着简单,但从日志里复制粘贴出来的文本经常混着单引号,导致解析失败。

不允许出现的东西还包括:注释(//和/* */一律不认)、尾随逗号([1,2,]和{"a":1,}都不行)、NaN 和 Infinity(这两个不是合法 JSON 值,虽然 JavaScript 的JSON.stringify(NaN)会输出null,反向JSON.parse("NaN")直接抛错)、前导零(01不合法,0.1合法)、十六进制字面量(0x1F不合法)。空白字符只允许空格、制表符、换行、回车四种,其他 Unicode 空白(比如不间断空格 U+00A0)在字符串外面出现就是语法错误。

注意:从网页或文档里复制 JSON 时,最容易带进来的是不间断空格(NBSP)和全角引号。这两个东西肉眼几乎看不出来,解析器报的错往往是"unexpected token"这种毫无指向性的信息。排查时先看报错位置的字符编码,比反复读代码有效。

对象内的键允许重复,但规范说行为"不可预测",多数实现是后出现的覆盖前面出现的。如果你需要在解析时检测重复键,Python 的object_pairs_hook和 JavaScript 的自定义解析库都能做到,这是数据校验的一个实用技巧,下面会讲。

2.2 字符串转义完整清单

字符串是 JSON 里唯一需要认真处理字符的场合。必须转义的字符只有六个,但实际编码时还有一层"控制字符"的要求。

转义序列含义备注
\"双引号必须转义
\\反斜杠必须转义
\/正斜杠可以不转义,转义是合法的但没必要
\b退格 U+0008必须转义
\f换页 U+000C必须转义
\n换行 U+000A必须转义
\r回车 U+000D必须转义
\t制表 U+0009必须转义
\uXXXXUnicode 码点四位十六进制,大小写均可

规律很清晰:所有 U+0000 到 U+001F 的控制字符都必须转义,其余字符可以直接以 UTF-8 字节形式出现在文本里(前提是文件本身按 UTF-8 编码)。\/是历史遗留,早期为了防止</script>这种序列在 HTML 内嵌脚本里提前闭合标签,所以有了这个可选转义,现在除了特定场景没人会主动用它。

真正麻烦的是增补平面字符,也就是码点超过 U+FFFF 的那些,包括绝大多数 emoji、部分生僻汉字。JSON 的\uXXXX只能表示四位,所以这些字符必须拆成代理对,用两个\u序列表示。比如 U+1F600(笑脸)要写成\uD83D\uDE00。不同实现处理这件事的方式差异很大:Python 的json.dumps在ensure_ascii=True(默认值)时会把中文和 emoji 全部转成\uXXXX形式,emoji 自动拆代理对,看起来乱但正确;ensure_ascii=False则直接输出 UTF-8 字节。JavaScript 的JSON.stringify对增补平面字符直接输出原字符,不转义。

这里有个隐蔽的坑:孤立的代理项。如果字符串里存在一个没有配对的\uD800,严格实现会报错或替换成 U+FFFD,宽松实现可能原样保留,导致数据损坏。Python 的json.loads默认对孤立代理项是接受的,但在输出到某些下游系统时会炸。如果你处理的文本来自不可信来源,解析后建议做一次字符清洗。

实操技巧:在 Python 里,json.loads遇到字符串中的原始换行(未转义的 U+000A)会报Invalid control character。用json.loads(s, strict=False)可以放过这类字符,但这是宽松化处理,只在清洗脏数据时用,正常协议里绝不能依赖它。

2.3 数字与文本的精度陷阱

JSON 规范对数字只给了语法描述:可选负号、整数部分、可选小数部分、可选指数部分。没有规定精度上限,没有规定整数和浮点的分界。这直接导致同一个字面量在不同语言里被解析成不同类型、不同精度。

JavaScript 侧全部走Number,双精度浮点,安全整数范围是 ±(2^53 - 1)。超出这个范围的整数字面量解析后会被"就近舍入",而且JSON.stringify输出回来的值可能和输入值不一致。这是 JSON 在 JS 生态里最需要警惕的行为。Python 侧的json.loads把不含小数点和指数部分的字面量解析成int,任意精度,不会丢;含小数点或指数的解析成float,双精度,会丢。Go 的encoding/json默认把数字解析成float64,除非你用json.Number或 Decoder 的UseNumber()。

处理大数值的标准做法有三条,按推荐度排序:

  1. 传输层用字符串。把 18 位订单号写成"202401011234567890",两端都当字符串处理,零风险。这是最省心的方案,代价是接收方如果要做数值比较需要转换。
  2. 用专门的任意精度数字库。Python 侧json.loads(s, parse_int=Decimal)可以指定解析器,把整数字面量转成Decimal,或parse_float=Decimal处理小数。JS 侧没有原生方案,需要自己写解析器或用支持大数的库。
  3. 限制数值范围。在接口文档里明确写死:所有数值字段绝对值不超过 1e15。这是管理手段,比技术手段脆,但成本最低。

还有一个几乎所有人都会忽略的点:-0。JSON 语法允许-0,解析之后 JavaScript 里-0 === 0为真,但Object.is(-0, 0)为假,1 / -0得到-Infinity。Python 里json.loads("-0")得到整数0,但json.loads("-0.0")得到-0.0,float("-0.0")的符号位是保留的。一般业务里没有影响,做数值计算或哈希敏感的场景要注意。

小数还有一个绕不开的问题:0.1 + 0.2不等于0.3。这不是 JSON 的错,是二进制浮点的固有特性,但 JSON 的文本表示会把这个误差暴露在日志里,让人误以为是序列化出了问题。涉及金额的计算,用最小货币单位的整数(分、厘)或者字符串承载的Decimal,绝不要用 JSON 里的浮点数字面量直接参与金额运算。

3. 动手实操:解析、生成、校验一条龙

理论讲完,进入能直接抄的部分。这一节按"命令行验证 → Python 实现 → JavaScript 实现 → Schema 校验"的顺序走,每条命令和代码我都标注了关键参数的含义,你可以对着改。

3.1 命令行三件套:验证、格式化、取字段

日常排查最快的路径是命令行,不写代码就能判断一段文本是不是合法 JSON、结构长什么样。

# 1. 验证合法性 + 美化输出,-e 表示对输入做 eval 解析,-s 表示不输出解析结果(只看报错) echo '{"name":"测试","tags":["a","b"],"n":3}' | jq -e . # 2. 压缩成一行,去掉所有空白,适合塞进 URL 或日志 jq -c . data.json # 3. 提取指定字段,-r 去掉输出字符串的双引号 jq -r '.name' data.json # 4. 过滤数组元素 jq '.[] | select(.score > 90) | .name' students.json # 5. 查看字段类型,排查"数字被当成字符串"这类问题 jq '.id | type' data.json

jq是这类工具里的事实标准,绝大多数 Linux 发行版仓库里都有。Windows 下可以用jq的原生移植版,或者退一步用各种语言的单行命令。Python 自带的python -m json.tool也能格式化,参数少但够用:

# 格式化,--indent 指定缩进空格数,--sort-keys 按字母序排键 python -m json.tool --indent 2 --sort-keys data.json # 验证并返回退出码,脚本里判断用 python -m json.tool data.json > /dev/null && echo "合法" || echo "非法"

实操心得:jq -e的退出码规则是——解析成功且结果不是false/null时返回 0,否则返回非 0。所以拿它做"是否合法 JSON"的判断时,如果文件内容恰好是null,会被误判为失败。这种边界我踩过,判断合法性时加个.外层包裹更保险。

3.2 Python 侧:json模块的关键参数与钩子

Python 标准库的json模块是日常处理的主力,它的参数比多数人以为的多,而且每一个都有实际用途。

序列化方向,json.dumps的高频参数:

参数作用建议值
ensure_ascii是否把非 ASCII 字符转成\uXXXX生产环境传False,省字节且日志可读
indent缩进空格数调试用 2,生产用None
sort_keys键按字母序排列需要做内容比对/哈希时传True
separators分隔符元组压缩时传(',', ':')
default不可序列化对象的转换函数处理datetime、Decimal必传
allow_nan是否允许输出NaN/Infinity生产环境传False,避免产出非法 JSON

default这个参数值得单独说。Python 的json模块不认识datetime、Decimal、set这些类型,遇到就抛TypeError。很多人第一反应是写一个JSONEncoder子类,其实default回调就够了,代码量少一半:

import json from datetime import datetime, date from decimal import Decimal def to_json_default(obj): if isinstance(obj, (datetime, date)): return obj.isoformat() # 输出 2024-01-01T00:00:00 形式 if isinstance(obj, Decimal): return str(obj) # 转字符串,避免精度丢失 if isinstance(obj, set): return sorted(obj) # 排序保证输出稳定,便于比对 raise TypeError(f"不支持的类型: {type(obj).__name__}") data = {"ts": datetime(2024, 1, 1, 12, 0), "amt": Decimal("19.99"), "tags": {"b", "a"}} print(json.dumps(data, default=to_json_default, ensure_ascii=False)) # {"ts": "2024-01-01T12:00:00", "amt": "19.99", "tags": ["a", "b"]}

解析方向,json.loads的参数更隐蔽,但用好了能解决大问题:

import json from decimal import Decimal raw = '{"id": 12345678901234567890, "price": 9.9, "a": 1, "a": 2}' # parse_int / parse_float:指定数字解析器,解决精度问题 obj = json.loads(raw, parse_int=Decimal, parse_float=Decimal) print(obj["id"]) # 12345678901234567890,精确保留 print(obj["price"]) # 9.9,Decimal 精确表示 # object_pairs_hook:拿到键值对列表,可以检测重复键 def hook(pairs): keys = [k for k, _ in pairs] if len(keys) != len(set(keys)): raise ValueError(f"存在重复键: {keys}") return dict(pairs) try: json.loads(raw, object_pairs_hook=hook) except ValueError as e: print(e) # 存在重复键: ['id', 'price', 'a', 'a'] # object_hook:对每个对象做后处理,适合批量把时间字符串转成 datetime def convert(o): for k, v in o.items(): if isinstance(v, str) and len(v) == 19 and v[4] == "-": try: o[k] = datetime.fromisoformat(v) except ValueError: pass return o json.loads('{"created_at": "2024-01-01T00:00:00"}', object_hook=convert)

编码问题也在这里交代清楚。如果文件开头带 UTF-8 BOM(字节EF BB BF),直接用open(path)读进来喂给json.loads会报Unexpected UTF-8 BOM。解决办法是open(path, encoding="utf-8-sig"),这个编码名专门吃掉 BOM。Windows 环境导出的文本文件带 BOM 的比例不低,这个坑我踩过不止一次。

3.3 JavaScript 侧:JSON.parse与JSON.stringify的边界

JavaScript 的 API 只有两个方法,看起来简单,边界情况却不少。

JSON.parse(text, reviver)的第二个参数reviver是一个转换函数,会在每个键值对上被调用,返回undefined表示删除该键:

const text = '{"name":"A","pwd":"123","ts":"2024-01-01T00:00:00Z"}'; const obj = JSON.parse(text, (key, value) => { if (key === "pwd") return undefined; // 删掉敏感字段 if (key === "ts") return new Date(value); // 转成 Date 对象 return value; }); console.log(obj); // { name: 'A', ts: 2024-01-01T00:00:00.000Z }

注意reviver的遍历是自底向上的,也就是先访问叶子节点再访问父节点,所以你在里面对父对象做的判断,拿到的是已经处理过的子属性。

JSON.stringify(value, replacer, space)三个参数各有讲究:

const data = { id: "123", pwd: "abc", nested: { pwd: "x" }, n: NaN, f: function(){} }; // replacer 传数组:只保留指定键 console.log(JSON.stringify(data, ["id", "nested"])); // {"id":"123","nested":{}} // replacer 传函数:动态控制,注意 key 是空的字符串代表根对象 console.log(JSON.stringify(data, (k, v) => (k === "pwd" ? undefined : v))); // {"id":"123","nested":{},"n":null} // space 传数字最多 10 位,传字符串则用该字符串做缩进(最多前 10 个字符) console.log(JSON.stringify({a:1,b:2}, null, "\t"));

几个必须记住的行为:undefined、函数、Symbol 作为对象属性时会被跳过,作为数组元素时会被转成null;NaN和Infinity一律转成null;循环引用直接抛TypeError;Date对象会被调toJSON转成 ISO 字符串。这些行为组合起来,意味着你手上那个对象经过一次序列化再反序列化,很可能和原来不一样。做数据缓存、做深拷贝、做内容比对时,这一点必须心里有数。

大数问题在 JS 侧没有优雅解法。如果接口可能返回超过 2^53 的整数,最稳的方式是在解析前就把它当字符串处理。现在有些 JSON 库支持BigInt或自定义数字解析,但生态不统一。我的做法是在网关层就把这类字段转成字符串下发,前端拿到的永远是字符串,前端不需要知道它其实是数字。

3.4 JSON Schema:把错误挡在入口

程序里到处写if (!data.name) throw new Error(...)是很脆的,字段一多就失控。JSON Schema 把校验规则外置成一份声明式文档,校验器负责执行,业务代码只管用。

一个够用的例子:

{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["order_id", "amount", "items"], "additionalProperties": false, "properties": { "order_id": { "type": "string", "pattern": "^[0-9]{18}$" }, "amount": { "type": "integer", "minimum": 1 }, "status": { "type": "string", "enum": ["created", "paid", "closed"] }, "items": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["sku", "qty"], "properties": { "sku": { "type": "string", "minLength": 1 }, "qty": { "type": "integer", "minimum": 1 } } } } } }

几个关键字的取舍是有讲究的。additionalProperties: false表示拒绝未声明的字段,这在跨团队对接时是双刃剑:能及早发现对方多传了字段,但也容易在对方灰度发布新增字段时导致自己这边全线报错。我的做法是对接初期开成false把契约谈清楚,稳定运行三个月后改成true,避免无谓的耦合。required只列必要的,不要把可选字段也塞进去,否则对方不传就挂。数值的minimum用来兜住负数和零,业务上经常漏。

Python 侧用jsonschema库,三行代码跑起来:

from jsonschema import validate, ValidationError schema = {...} # 上面的内容 data = {"order_id": "123456789012345678", "amount": 100, "items": [{"sku": "A1", "qty": 2}]} try: validate(instance=data, schema=schema) print("校验通过") except ValidationError as e: print(f"字段 {list(e.path)} 校验失败: {e.message}")

注意:JSON Schema 有多个草案版本(draft-04、draft-07、2019-09、2020-12),不同版本关键字行为有差异,比如exclusiveMinimum在老版本是布尔值、新版本是数值。选库之前先确认它支持哪个草案,别拿着 2020-12 的 schema 喂给只认 draft-04 的校验器,报错信息会非常误导。

4. 常见故障排查实录

这一节全是实战。我把这几年遇到过的、帮别人排查过的、以及社区里反复出现的问题整理成速查表,再挑几个典型案例展开。

4.1 报错信息与真实原因速查表

解析器的报错往往只告诉你"第几行第几列出错",不告诉你错在哪。下面这张表按报错关键词反查原因,能省掉大量试错。

报错关键词大概率原因排查动作
Expecting property name enclosed in double quotes用了单引号、键没加引号、多了尾随逗号定位到报错列,检查前一个字符
Invalid control character字符串里有未转义的原始换行或制表符用strict=False临时放过,再定位源头
Expecting value空字符串、undefined、NaN字面量检查是否从 JS 直接拼了非 JSON 值
Extra data一个文件里放了多个 JSON 对象改用 JSON Lines,一行一条
Unexpected UTF-8 BOM文件头有 BOM用utf-8-sig编码打开
Unterminated string字符串没闭合,或被截断检查网络传输是否完整、字段是否超长
Maximum recursion depth嵌套层级过深检查是否有循环引用或异常数据
Invalid \escape反斜杠后面跟了非法字符,比如路径C:\Users路径需要写成C:\\Users或转成/

我重点说说最后一条,因为它太常见了。Windows 路径C:\Users\data直接塞进 JSON 字符串,\U会被当成 Unicode 转义的开头,解析器接着找四位十六进制,找不到就报Invalid \escape。正确写法是"C:\\Users\\data"或者"C:/Users/data"。Python 里还有个更隐蔽的版本:用json.dumps序列化之前,如果字符串本身是原始字符串r"C:\Users",序列化结果是"C:\\Users",正确;但如果是普通字符串"C:\Users",\U在 Python 层面就已经是转义序列了,可能直接抛SyntaxError。这种问题排查时看报错位置,往往在很前面,容易误判。

4.2 大文件与性能:别把 500MB 塞进内存

处理大 JSON 时最常见的错误是一次性读进内存再解析。500MB 的文件在 Python 里json.loads之后,对象占用的内存可能是原文件的 5 到 10 倍,机器直接被打爆。

标准解法是流式处理。如果文件结构是每行一个独立 JSON 对象(JSON Lines 格式),逐行读逐行解析,内存占用恒定:

import json with open("big.jsonl", encoding="utf-8") as f: for line_no, line in enumerate(f, 1): if not line.strip(): continue try: obj = json.loads(line) except json.JSONDecodeError as e: print(f"第 {line_no} 行解析失败: {e}") continue process(obj) # 处理单条

如果文件是一个巨大的 JSON 数组,标准库的json模块帮不上忙,用ijson库做增量解析,它能在不构建完整对象树的前提下逐个吐出数组元素:

import ijson with open("big.json", "rb") as f: for item in ijson.items(f, "item"): # 假设顶层是 {"item": [...]} process(item)

性能敏感的场景还有两个选择。Python 侧可以用orjson,序列化和反序列化的速度比标准库快数倍,且原生支持datetime和numpy数组,缺点是它要求输入必须是bytes或str且严格 UTF-8。Node.js 侧注意大 JSON 的JSON.parse会阻塞事件循环,几百 MB 的解析可能卡住好几秒;解法是用stream-json之类的流式解析器,或者把解析放到 worker 线程里。

实操心得:不要迷信"先压缩再传"。JSON 的文本压缩率很高,gzip之后往往只剩 10% 到 15%,这对网络传输帮助巨大,但对解析耗时没有帮助——解压之后还是要完整解析一遍。真正的解析瓶颈在构建对象树,只有流式解析能绕开。我见过有人以为开了 gzip 就能处理超大 JSON,结果内存该爆还是爆。

4.3 脏数据清洗:从"看起来像 JSON"到"能解析的 JSON"

真实世界里拿到的数据,很多是"像 JSON 但不是 JSON"。我整理了一套清洗流水线,按顺序过一遍,能解决八成的脏数据问题。

第一步,去 BOM 和不可见字符。用正则删掉\ufeff和零宽字符,把全角引号替换成半角。

import re def clean_text(s: str) -> str: s = s.replace("\ufeff", "") # BOM s = re.sub(r"[\u200b-\u200d\u2060]", "", s) # 零宽字符 s = s.replace("\u00a0", " ") # 不间断空格 s = s.replace("“", '"').replace("”", '"') # 全角双引号 s = s.replace("‘", "'").replace("’", "'") return s

第二步,处理注释和尾随逗号。如果数据源是人手写的配置文件,可能有//注释和多余逗号。写个状态机逐字符扫描,跳过字符串内部的符号,比正则安全得多——正则遇到{"url": "http://x"}里的//会误删。

def strip_comments(s: str) -> str: out, i, in_str = [], 0, False while i < len(s): ch = s[i] if in_str: out.append(ch) if ch == "\\": if i + 1 < len(s): out.append(s[i + 1]); i += 2; continue elif ch == '"': in_str = False i += 1 continue if ch == '"': in_str = True; out.append(ch); i += 1; continue if ch == "/" and i + 1 < len(s): if s[i + 1] == "/": while i < len(s) and s[i] != "\n": i += 1 continue if s[i + 1] == "*": i += 2 while i + 1 < len(s) and not (s[i] == "*" and s[i + 1] == "/"): i += 1 i += 2; continue out.append(ch); i += 1 return "".join(out)

第三步,处理非法字面量。把裸的NaN、Infinity、-Infinity替换成null,把undefined也换成null。这一步用正则时要加词边界,避免误伤字符串内容。

第四步,尝试修复截断。如果 JSON 在传输中被截断,最后一个完整对象后面的内容是不完整的。做法是找到最后一个"深度归零且不在字符串内"的位置,截断到那里,再补齐缺失的闭合符号。这个逻辑写起来不复杂,但要处理好字符串里的括号,否则{"msg": "}"}这种数据会被切错。

清洗完还是解析不了,就把报错的行列号和前后各 50 个字符打出来,肉眼看一下。九十的情况下,看一眼就够了。

5. 格式选型与团队协作规范

单打独斗时怎么写都行,团队协作时格式规范就是生产力。这一节讲选型对比和落地的规矩。

5.1 与 CSV、YAML、二进制格式的横向对比

维度JSONCSVYAML二进制(如 Protobuf)
可读性好好(表格类)最好差,需要工具
支持嵌套是否是是
支持注释否否是否
体积中小中最小
解析速度中快慢最快
类型系统弱(六种)极弱(全字符串)中强
浏览器原生支持是否否否
适合场景接口传输、日志大批量表格数据配置文件内部高频通信

这张表的用法是按场景查。接口传数据,JSON 的通用的优势压过一切;导出一份十万行的报表给业务方,CSV 更合适,用 Excel 一打开就能看;配置文件,YAML 的注释和多行字符串能力是刚需;跨机房高频通信,二进制能省下一半以上的带宽和解析时间。

有个组合方案值得提一下:内部用二进制,对外用 JSON。网关层做一次转换,内部服务之间跑高效的二进制协议,对客户端暴露 JSON 接口。这样既拿到了性能,又保住了对外兼容性。代价是多一层转换逻辑和一份字段映射表,要评估维护成本。

YAML 有一个坑必须强调:它对缩进极度敏感,而且no、yes、on、off在某些版本里会被解析成布尔值而不是字符串。如果配置项里有一个叫country的字段值恰好是NO(挪威的国家代码),可能被解析成false。所以关键配置里的字符串值,统一加引号,不要依赖隐式类型推断。

5.2 字段命名与版本演进的几条硬规矩

这些规矩不是规范强制的,但一个团队不统一,后期改起来很痛。

键名风格统一。JSON 社区里snake_case和camelCase都有大量使用,选一个,全项目贯彻。跨语言对接时,如果两边风格不同,在边界层做一次转换,不要让两种风格混在同一个报文里。

时间统一用 ISO 8601 带时区。"2024-01-01T12:00:00+08:00"这种格式语义明确,不用猜是本地时间还是 UTC。只写"2024-01-01 12:00:00"的接口,早晚会因为时区问题出一次事故。我见过一个系统把时间存成不带时区的字符串,跨时区部署后报表全错位。

空值的语义要区分。字段缺失、字段值为null、字段值为空字符串""、字段值为空数组[],这四个在业务上的含义完全不同。缺失表示"没提供",null表示"明确为空",""表示"提供了但内容为空",[]表示"集合为空"。接口文档里必须写清楚每个字段用哪种表达。

新增字段一律可选,删除字段走废弃流程。这是向后兼容的基本功。新增可选字段,老客户端不认识会忽略,不报错;删除字段,先标记废弃、发版公告、观察调用量归零,再从文档移除,最后才真正删除。跳过这个流程直接删,线上必然出问题。

数组和对象不要混用同一个键。有些接口在只有一个元素时返回对象,多个时返回数组,调用方得写两套分支。这种设计现在应该绝迹了,但老系统里还有,对接时先问清楚。

5.3 我踩过的几个坑,你可以直接绕过

最后分享几个具体到能省下半天时间的经验。

第一个坑,json.dumps之后再json.loads,对象不一定相等。原因是元组变列表、Decimal变字符串、datetime变字符串、键的顺序可能变(Python 3.7 之后字典保序,但跨语言不保证)。做缓存 key 计算时,务必先json.dumps(obj, sort_keys=True, separators=(",", ":"))再哈希,否则同样的数据可能算出不同的 key,缓存命中率莫名其妙地低。这个我排查过整整一个下午。

第二个坑,浮点数做键排序会出意外。sort_keys=True只对字符串键有效,如果键是数字类型,Python 的json.dumps会先把它们转成字符串再排,于是10排在2前面。要按数值排序,得自己在序列化前处理。

第三个坑,JSON Lines 和 JSON 数组别搞混。JSON Lines 是每行一个独立 JSON 对象,整个文件不是合法 JSON;JSON 数组是整个文件一个数组。两种格式的工具链完全不同,jq处理前者要加-s或者逐行处理,处理后者直接.就行。数据管道里搞混了,解析会直接失败。

第四个坑,别用正则解析 JSON。JSON 是递归结构,正则只能处理扁平文本。我见过用正则从 JSON 里抠字段的代码,在遇到字符串里含花括号、转义引号、嵌套数组时全线崩溃。真要取字段,老老实实用解析器,没有例外。

第五个坑,序列化时的编码声明。Node.js 里fs.writeFile不加编码参数,写入的是 buffer,中文可能乱码;Python 的open不加encoding="utf-8",在 Windows 上默认用 GBK,写出来的 JSON 别的系统读不了。这类问题在本地开发环境完全看不出来,一部署到另一台机器就炸。我的习惯是所有文件操作显式写encoding="utf-8",宁可多打几个字。

第六个坑,HTTP 响应里Content-Type不带charset。虽然 JSON 规范规定默认编码是 UTF-8,但有些客户端库会按响应的Content-Type决定解码方式,不带charset=utf-8时可能猜错。服务端返回 JSON 时,Content-Type: application/json; charset=utf-8这半句别省。

这些坑单独看都很小,但每一个都能消耗掉半天排查时间。把它们写成团队的编码规范,附在代码评审清单里,比事后救火划算得多。JSON 本身不难,难的是它在真实系统里和网络、编码、精度、版本演进这些东西搅在一起时的样子。语法花十分钟就能记住,剩下的功夫全在边界情况上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 5:01:55

ArkTS跨线程回调:threadsafe function原理与高频回传实践

1. 为什么原生线程不能直接回调 ArkTS1.1 一次崩溃暴露出来的线程归属问题模拟项目里有个数据采集模块&#xff0c;后台工作线程每 20ms 采一次样本&#xff0c;采完想在 ArkTS 侧更新界面上的实时曲线。最开始的做法很直觉&#xff1a;在start函数里把 ArkTS 传进来的回调用na…

作者头像 李华
网站建设 2026/11/2 5:01:51

解决AutoDL平台OpenMP线程数设置错误问题

1. 问题背景与现象分析当你在AutoDL平台上运行涉及OpenMP并行计算的程序时&#xff0c;可能会遇到这样的报错信息&#xff1a;libgomp: Invalid value for environment variable OMP_NUM_THREADS这个错误通常发生在以下几种场景&#xff1a;在终端手动设置了错误的OMP_NUM_THRE…

作者头像 李华
网站建设 2026/11/2 5:00:29

Agent记忆系统设计与实现:从工作记忆到长期记忆的完整指南

做Agent开发的朋友应该都体会过大模型的“健忘症”&#xff1a;上一轮还聊得好好的&#xff0c;换个会话就什么都不记得了。模型本身是无状态的&#xff0c;每次调用都像刚开机&#xff0c;如果不给它装一套记忆系统&#xff0c;Agent就永远只能原地打转。我最早做Agent项目时&…

作者头像 李华
网站建设 2026/11/2 5:00:25

Jev是什么?Java环境变量JAVA_HOME、PATH、CLASSPATH配置详解

Jev这个东西&#xff0c;我在带新人的时候被问过无数次。第一次听到这个名字&#xff0c;十个人里有九个会愣一下&#xff0c;然后问一句&#xff1a;“这是个框架&#xff1f;一个新语言&#xff1f;还是某个工具的名字&#xff1f;”实际上Jev不是什么高深的新技术&#xff0…

作者头像 李华
网站建设 2026/11/2 5:00:24

PyCharm集成通义灵码:AI编程助手实战指南

1. PyCharm与通义灵码&#xff1a;AI编程新体验作为一名使用PyCharm多年的Python开发者&#xff0c;我最近深度体验了通义灵码这款AI编程助手。它彻底改变了我日常编码的工作流——从手动敲击每一行代码&#xff0c;到与AI协同完成复杂任务。通义灵码由阿里云开发&#xff0c;支…

作者头像 李华
网站建设 2026/11/2 5:00:24

科研论文高效投稿方法论:从期刊匹配到审稿响应

1. 学术发表困境与解决方案作为一名在科研领域摸爬滚打多年的研究者&#xff0c;我深知论文投稿过程中的种种痛点。每次打开期刊投稿系统&#xff0c;面对密密麻麻的格式要求和审稿周期&#xff0c;总有种"望山跑死马"的无力感。直到三年前&#xff0c;我偶然发现了一…

作者头像 李华