JSON 是爬虫结果最常见的格式。它的字节都花在哪里,数字、中文、转义、大整数各有什么代价,换更快的实现能快多少?这篇把 JSON 本身量一遍,给后面的格式对比一个基线。
字符串值(含引号) 4262748 字节 59.3% 键名(含引号) 1880000 字节 26.1% 结构标点 640000 字节 8.9% 数字文本 411414 字节 5.7% 合计 7194162 字节,实际 JSON 字节(去掉换行)7194162;相等;键名在一条记录里占 94 字节,共 13 个键
对 2 万条商品记录逐个记号统计:字符串值 59.3%,键名 26.1%,结构标点 8.9%,数字文本 5.7%。键名每条重复一遍(每条 94 字节),是 JSON 比带 schema 的格式大的主要原因之一;数字在这类数据里占比很小。
price(2 位小数的浮点) 文本长度:平均 4.92、最短 3、最长 7 字节;二进制(定长 8 字节) stock(0–500 的整数) 文本长度:平均 2.78、最短 1、最长 3 字节;二进制(定长 2 字节) id(7 位整数) 文本长度:平均 7.00、最短 7、最长 7 字节;二进制(定长 4 字节) timeseries 的 value(round(…,3))文本平均 5.89 字节,二进制 float64 是 8 字节 Python 的浮点重复表示:json.dumps(0.1 + 0.2) = 0.30000000000000004(17 位);json.dumps(0.1) = 0.1;json.dumps(1/3) = 0.3333333333333333;json.dumps(1e22) = 1e+22;json.dumps(1e-7) = 1e-07
JSON 的数字是十进制文本:位数少的数字文本反而比定长二进制短(stock 平均 2.78 字节对 2 字节的 16 位整数相差不大,price 平均 4.92 字节对 8 字节 float64 更短),位数多的整数更长(7 位的 id 文本 7 字节,32 位二进制 4 字节)。浮点数写出来可能很长,比如 0.1+0.2 要 17 位。
中文 1000 字 UTF-8 原文 3000 字节;JSON ensure_ascii=False 3002 (1.00×);ensure_ascii=True 6002 (2.00×) 含 emoji(😀 + 2 个汉字)×100 UTF-8 原文 1000 字节;JSON ensure_ascii=False 1002 (1.00×);ensure_ascii=True 2402 (2.40×) 引号/反斜杠/换行/制表 重复 100 次 UTF-8 原文 3400 字节;JSON ensure_ascii=False 3802 (1.12×);ensure_ascii=True 6802 (2.00×) 控制字符 1–31 各 30 次 UTF-8 原文 930 字节;JSON ensure_ascii=False 4982 (5.36×);ensure_ascii=True 4982 (5.36×) 压缩之后(zstd-3):中文 ensure_ascii=False 51 字节,ensure_ascii=True 80 字节
ensure_ascii=True(Python 的默认)把每个汉字写成 6 字节的 \uXXXX,中文文本膨胀到 2 倍,emoji 是 12 字节。关掉它(ensure_ascii=False)和 UTF-8 原文一样大。控制字符不管怎样都要转义成 6 字节。
Python:json.loads(json.dumps(9007199254740993)) == 9007199254740993:True
node 26:JSON.parse('{"id":9007199254740993}') 得到 9007199254740992 differs {"id":9007199254740992}
float("nan") json.dumps → NaN orjson.dumps → null node 解析 json 的结果:node 解析失败
float("inf") json.dumps → Infinity orjson.dumps → null node 解析 json 的结果:node 解析失败
bytes json.dumps → 抛出 TypeError orjson.dumps → 抛出 TypeError node 解析 json 的结果:-
set json.dumps → 抛出 TypeError orjson.dumps → 抛出 TypeError node 解析 json 的结果:-
datetime json.dumps → 抛出 TypeError orjson.dumps → "2026-10-12T00:00:00" node 解析 json 的结果:-
dict 的整数键 {1: "a"} json.dumps → {"1": "a"} orjson.dumps → 抛出 TypeError node 解析 json 的结果:可解析
tuple json.dumps → [1, 2] orjson.dumps → [1,2] node 解析 json 的结果:可解析JSON 没有 bytes、集合、日期等类型;大于 2^53 的整数在 JavaScript(node)里会丢精度;NaN 和 Infinity 不是合法 JSON,标准库会写出来而别的解析器会拒绝。要跨语言交换这类数据,要自己约定表示方法(整数用字符串、bytes 用 base64 等)。
json.dumps 中位 40.9 ms(40.4–41.5), 168 MiB/s orjson.dumps 中位 5.2 ms(4.9–5.4), 1323 MiB/s json.loads 中位 44.2 ms(43.0–50.9), 156 MiB/s orjson.loads 中位 20.8 ms(19.9–22.2), 331 MiB/s orjson 的输出和标准库紧凑输出字节相同:True
同样是 JSON,orjson 的序列化比标准库快约 7.9 倍、解析快约 2.1 倍,输出字节相同。比较“JSON 对 MessagePack”之类时,先分清速度差是格式造成的还是 Python 实现造成的。
写法 原始字节 zstd-3 gzip-6 原键名(object) 7214161 1331194 1484671 键名缩成 1 个字母 6114161 1287104 1459181 每条记录是数组(表头另存) 5314161 1236383 1400570 原始字节省下:缩短键名 15.2%、改数组 26.3%;压缩后省下:缩短键名 3.3%、改数组 7.1%(zstd-3)
缩短键名把原始字节省下 15.2%,改成数组省 26.3%;压缩之后只剩 3.3% 和 7.1%。重复的键名是压缩器最容易去掉的内容。这也解释了为什么“JSON 压缩后”常常和更紧凑的格式差距不大(见第 20 篇)。