bigywater.tech
数据编码与压缩 · 2

JSON 的真实开销:字段名、数字文本、转义与大整数

JSON 是爬虫结果最常见的格式。它的字节都花在哪里,数字、中文、转义、大整数各有什么代价,换更快的实现能快多少?这篇把 JSON 本身量一遍,给后面的格式对比一个基线。

26.1%
products 的 JSON 里键名(含引号)占的字节比例;字符串值占 59.3%,数字只占 5.7%
2.00×
中文用 ensure_ascii=True 写成 \uXXXX,字节数是 UTF-8 原文的倍数
15.2% 对 3.3%
把键名缩成 1 个字母:原始字节省下 15.2%,zstd-3 压缩之后只省下 3.3%

字节花在哪

实测 · 本机执行输出
  字符串值(含引号)        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 篇)。

分步演示

JSON 的开销
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8(系统 Python,自带 compression.zstd)/ zstandard 0.25.0 / lz4 4.4.5 / zstd、lz4、brotli、protoc 命令行(Homebrew);msgpack、protobuf、snappy 等第三方库临时装在临时目录里,用完删除;所有数据都是本机脚本生成的合成数据(固定随机种子),没有使用任何真实网站或用户的数据;计时用 time.perf_counter,取多次中位数,数字每次运行会有小幅抖动。数据集见第 1 篇;orjson 3.13.0 临时安装;node v26.10.0 用来看 JSON.parse 的行为;第一到四、六节是确定性结果(各一次),第五节是各 7 次取中位数的计时
    • 没有验证:ujson、simdjson 等其它 JSON 实现;JSON5、JSON Lines 之外的变体;流式解析大 JSON 的内存占用;浮点数“最短表示”算法(只看了输出,没读实现);Windows 或 Linux 上的数字;其它语言(Go、Java)的 JSON 库速度
    京ICP备2026067502号-1京公网安备11010502063487号