slimjson 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,361 +1,510 @@
1
- # slimjson
2
-
3
- 中文 | [English](./README_EN.md)
4
-
5
- 轻量级对象数组压缩工具 — 将重复 key 的 JSON 对象数组转换为 `{ keys, rows }` 紧凑格式,并支持序列化时省略 `null` 以进一步减小体积。
6
-
7
- ## 适用场景
8
-
9
- - **API 列表接口**:后端返回列表接口时,每个对象都携带相同的 key 名,大量冗余
10
- - **异构字段**:不同对象可能拥有不同的字段(后端按需 omit null 字段)
11
- - **网络传输压缩**:需要在网络传输中极致压缩 JSON 文本体积
12
- - **大模型上下文压缩**:将大量结构化数据(如数据库查询结果、API 响应、知识库条目)压缩后送入 prompt,减少 token 消耗,降低调用成本
13
- - **大模型工具调用**:function calling / tool_use 返回的结果往往是结构化的对象数组,压缩后再回传给模型,可显著减少上下文窗口占用,让模型在有限 token 内处理更复杂的数据
14
- - **大模型识别友好**:压缩后的 `{ keys, rows }` 格式将 schema(字段定义)与数据分离,key 只出现一次,模型能更准确地理解数据结构、按字段名提取信息,比重复 key 的原始 JSON 更不容易混淆
15
-
16
- ## 安装
17
-
18
- ```bash
19
- npm install slimjson
20
- ```
21
-
22
- ## API
23
-
24
- ### `compress(source, opts?)`
25
-
26
- 将对象数组压缩为 `{ keys, rows }` 结构:
27
-
28
- ```js
29
- import { compress } from 'slimjson';
30
-
31
- const users = [
32
- { name: 'Alice', age: 25, city: 'NYC' },
33
- { name: 'Bob', age: 30, city: 'LA' },
34
- ];
35
-
36
- const compressed = compress(users);
37
- // {
38
- // keys: ['name', 'age', 'city'],
39
- // rows: [
40
- // ['Alice', 25, 'NYC'],
41
- // ['Bob', 30, 'LA' ]
42
- // ]
43
- // }
44
- ```
45
-
46
- **参数:**
47
-
48
- | 参数 | 类型 | 默认值 | 说明 |
49
- |------|------|--------|------|
50
- | `source` | `Object[]` 或 `Object` | — | 待压缩的对象数组(单个对象会自动包裹为数组) |
51
- | `opts` | `Object` | — | 可选配置 |
52
- | `opts.trimTrailingNulls` | `boolean` | `false` | 是否去除行尾的 `null` 值 |
53
-
54
- **特点:**
55
- - `keys` 取所有对象的 key 并集,按首次出现顺序排列
56
- - 某对象缺失某字段 → 对应 row 位置填充 `null`
57
- - 嵌套对象递归处理:`keys` 中表示为 `{ "fieldName": [childKeys] }`
58
- - 对象数组(如订单条目)同样递归压缩
59
- - 当传入的是对象时,会当成数组中只有一个对象处理
60
-
61
- #### 嵌套对象示例
62
-
63
- ```js
64
- const data = [
65
- { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
66
- { name: '李四', age: 35, profile: { avatar: 'b.jpg', file: null } },
67
- { name: '王五' },
68
- ];
69
-
70
- compress(data);
71
- // {
72
- // keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
73
- // rows: [
74
- // ['张三', 28, ['a.jpg', 'Hello', null]],
75
- // ['李四', 35, ['b.jpg', null, null]],
76
- // ['王五', null, null]
77
- // ]
78
- // }
79
- ```
80
-
81
- #### `trimTrailingNulls`:去除尾部 null
82
-
83
- 启用后,每行及嵌套子行尾部的 `null` 会被去除,进一步压缩体积:
84
-
85
- ```js
86
- compress(data, { trimTrailingNulls: true });
87
- // {
88
- // keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
89
- // rows: [
90
- // ['张三', 28, ['a.jpg', 'Hello']],
91
- // ['李四', 35, ['b.jpg']],
92
- // ['王五']
93
- // ]
94
- // }
95
- ```
96
-
97
- `decompress` 会自动将缺失的尾部值补回 `null`,roundtrip 还原结果一致:
98
-
99
- ```js
100
- decompress(compress(data, { trimTrailingNulls: true }));
101
- // [
102
- // { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
103
- // { name: '李四', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
104
- // { name: '王五', age: null, profile: null }
105
- // ]
106
- ```
107
-
108
- #### 对象数组示例(订单场景)
109
-
110
- ```js
111
- const orders = [
112
- { orderId: 'A001', items: [{ name: '键盘', price: 299 }, { name: '鼠标', price: 99 }] },
113
- { orderId: 'A002', items: [{ name: '显示器', price: 1999 }] },
114
- ];
115
-
116
- compress(orders);
117
- // {
118
- // keys: ['orderId', { items: ['name', 'price'] }],
119
- // rows: [
120
- // ['A001', [['键盘', 299], ['鼠标', 99]]],
121
- // ['A002', [['显示器', 1999]]]
122
- // ]
123
- // }
124
-
125
- stringify(compress(orders));
126
- // {keys:[orderId,{items:[name,price]}],rows:[[A001,[[键盘,299],[鼠标,99]]],[A002,[[显示器,1999]]]]}
127
- // ^^^^^ 嵌套对象 key 无引号 ^^^^ 安全字符串 value 无引号
128
- ```
129
-
130
- #### 三层嵌套示例(订单 → 商品 → 规格)
131
-
132
- ```js
133
- const orders = [
134
- {
135
- orderId: 'A001',
136
- customer: '张三',
137
- items: [
138
- { name: '键盘', price: 299, specs: { color: '黑色', layout: '104键' } },
139
- { name: '鼠标', price: 99, specs: { color: '白色', dpi: '4000' } },
140
- ]
141
- },
142
- {
143
- orderId: 'A002',
144
- customer: '李四',
145
- items: [
146
- { name: '显示器', price: 1999, specs: { color: '银色', size: '27寸' } },
147
- ]
148
- },
149
- ];
150
-
151
- compress(orders);
152
- // {
153
- // keys: [
154
- // 'orderId',
155
- // 'customer',
156
- // { items: ['name', 'price', { specs: ['color', 'layout', 'dpi', 'size'] }] }
157
- // ],
158
- // rows: [
159
- // ['A001', '张三', [
160
- // ['键盘', 299, ['黑色', '104键', null, null]],
161
- // ['鼠标', 99, ['白色', null, '4000', null]]
162
- // ]],
163
- // ['A002', '李四', [
164
- // ['显示器', 1999, ['银色', null, null, '27寸']]
165
- // ]]
166
- // ]
167
- // }
168
- // specs key 取并集:第一单有 layout,第二单有 size → 都保留,缺失的填 null
169
-
170
- compress(orders, { trimTrailingNulls: true });
171
- // rows 变为:
172
- // [
173
- // ['A001', '张三', [
174
- // ['键盘', 299, ['黑色', '104键']],
175
- // ['鼠标', 99, ['白色', null, '4000']]
176
- // ]],
177
- // ['A002', '李四', [
178
- // ['显示器', 1999, ['银色']]
179
- // ]]
180
- // ]
181
-
182
- stringify(compress(orders, { trimTrailingNulls: true }));
183
- // {keys:[orderId,customer,{items:[name,price,{specs:[color,layout,dpi,size]}]}],rows:[[
184
- // A001,张三,[[键盘,299,[黑色,104键]],[鼠标,99,[白色,,4000]]]],[A002,李四,[[显示器,1999,[银色]]]]]}
185
- ```
186
-
187
- #### 单对象示例
188
- ```js
189
- compress({ name: 'Alice', age: 25 });
190
- // 等价于 compress([{ name: 'Alice', age: 25 }])
191
- // { keys: ['name', 'age'], rows: [['Alice', 25]] }
192
- ```
193
-
194
- ### `decompress(compressed)`
195
-
196
- 将 `{ keys, rows }` 还原为原始对象数组。缺失的尾部值会自动补回 `null`:
197
-
198
- ```js
199
- const restored = decompress(compressed);
200
- // deep-equal 原数组
201
- ```
202
-
203
- ### `stringify(compressed)`
204
-
205
- 将 compress 结果序列化为紧凑文本。相比 `JSON.stringify`,应用了以下优化规则:
206
-
207
- ```js
208
- const data = [
209
- { name: 'Alice', age: 25 },
210
- { name: 'Bob', age: 30 },
211
- ];
212
-
213
- const text = stringify(compress(data));
214
- // {keys:[name,age],rows:[[Alice,25],[Bob,30]]}
215
-
216
- JSON.stringify(compress(data));
217
- // {"keys":["name","age"],"rows":[["Alice",25],["Bob",30]]}
218
- ```
219
-
220
- #### 序列化规则一览
221
-
222
- | 值类型 | 序列化结果 | 说明 |
223
- |--------|-----------|------|
224
- | `null` / `undefined` | `null` | |
225
- | 有限数字 | `25` | 直接输出,无引号 |
226
- | `NaN` / `Infinity` | `null` | 非有限数统一输出 null |
227
- | `true` / `false` | `true` / `false` | — |
228
- | 安全字符串 | `Alice` | 省略引号(见下方规则) |
229
- | 非安全字符串 | `"hello world"` | 保留 JSON 引号和转义 |
230
- | 嵌套对象 `{k: v}` | `{k:v}` | key 同样区分安全/非安全 |
231
- | 数组 | 见下方 null 省略规则 | — |
232
-
233
- #### 安全字符串(可省略引号的条件)
234
-
235
- 满足以下**全部**条件的字符串可省略引号,否则保留 `JSON.stringify` 转义:
236
-
237
- 1. 非空字符串
238
- 2. 不是关键字字面量:`null`、`true`、`false`
239
- 3. 不匹配数字模式:`/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/`(如 `"123"`、`"-3.14"`、`"1e10"` 均保留引号)
240
- 4. 不以数字或减号 `-` 开头
241
- 5. 不含空白、`[`、`]`、`{`、`}`、`,`、`:`、`"` 等字符
242
-
243
- | 字符串 | 结果 | 原因 |
244
- |--------|------|------|
245
- | `"Alice"` | `Alice` | 安全,省略引号 |
246
- | `"hello world"` | `"hello world"` | 含空格 |
247
- | `"123"` | `"123"` | 看起来像数字 |
248
- | `"-3.14"` | `"-3.14"` | 看起来像数字 |
249
- | `"null"` | `"null"` | 关键字 |
250
- | `""` | `""` | 空字符串 |
251
- | `"-abc"` | `"-abc"` | 以减号开头 |
252
- | `"a:b"` | `"a:b"` | 含冒号 |
253
-
254
- #### 对象 key 引号规则
255
-
256
- `keys` 中的嵌套对象 key 同样适用安全字符串判断:
257
-
258
- ```js
259
- stringify({ keys: [{ profile: ['name', 'age'] }], rows: [...] });
260
- // {keys:[{profile:[name,age]}],rows:[...]} ← profile 是安全 key,省略引号
261
-
262
- stringify({ keys: [{ "my-key": ['name'] }], rows: [...] });
263
- // {keys:[{"my-key":[name]}],rows:[...]} ← my-key 含减号,保留引号
264
- ```
265
-
266
- #### 数组 null 省略规则
267
-
268
- 数组中的 `null` / `undefined` 被省略为逗号空槽,不占文字体积:
269
-
270
- | 原始数组 | 序列化结果 | 说明 |
271
- |----------------------|------------|------|
272
- | `["a", null, null]` | `[a,,]` | 尾部两个空槽 |
273
- | `[null, 1, null]` | `[,1,]` | 前后空槽 |
274
- | `[]` | `[]` | 空数组 |
275
- | `[null]` | `[null]` | **特殊**:`[,]` 代表 2 个 null,因此单 null 保留文字 |
276
-
277
- ### `parse(text)`
278
-
279
- 解析 `stringify` 产生的文本,将省略的 `null` 恢复:
280
-
281
- ```js
282
- const parsed = parse(text);
283
- // deep-equal compressed
284
- ```
285
-
286
- 支持完整 JSON 类型的解析(字符串、数字、布尔、null、嵌套对象/数组),兼容转义字符和 Unicode。
287
-
288
- ## 完整使用示例
289
-
290
- ```js
291
- import { compress, decompress, stringify, parse } from 'slimjson';
292
-
293
- const data = [
294
- { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
295
- { name: '李四', age: 35, profile: { avatar: 'b.jpg' } }, // 缺失 bio
296
- ];
297
-
298
- // 压缩 文本化 → 解析 → 还原
299
- const compressed = compress(data);
300
- const text = stringify(compressed);
301
- const parsed = parse(text);
302
- const restored = decompress(parsed);
303
-
304
- // restored 与 data 深度相等
305
-
306
- // 启用 trimTrailingNulls 进一步压缩
307
- const compressedTrim = compress(data, { trimTrailingNulls: true });
308
- const textTrim = stringify(compressedTrim);
309
- // textTrim 比 text 更短
310
- ```
311
-
312
- ### 压缩率计算
313
-
314
- ```js
315
- const originalSize = Buffer.byteLength(JSON.stringify(data));
316
- const compressedSize = Buffer.byteLength(stringify(compress(data)));
317
- const ratio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
318
- console.log(`压缩率: ${ratio}%`);
319
- ```
320
-
321
- ## 压缩效果
322
-
323
- 基于 `compress-test.js` 基准测试的实际数据(18 组测试,所有 roundtrip 解压正确):
324
-
325
- | 数据类型 | 对象数 | 原始大小 | trim | 压缩率 | trim | 压缩率 | 差值 |
326
- |---------|-------|---------|---------|-------|------|-------|------|
327
- | 简单用户 | 1,000 | 147.61 KB | 87.12 KB | 40.98% | 87.12 KB | 40.98% | — |
328
- | 简单用户 | 10,000 | 1.45 MB | 882.51 KB | 40.69% | 882.51 KB | 40.69% | — |
329
- | 嵌套用户(含 profile.social) | 1,000 | 235.70 KB | 153.56 KB | 34.85% | 153.27 KB | 34.97% | -294 B |
330
- | 订单(每单1-5商品) | 500 | 166.95 KB | 72.30 KB | 56.69% | 72.30 KB | 56.69% | |
331
- | 学校数据(6年级×4班×30生) | 24 | 214.86 KB | 88.88 KB | 58.63% | 88.53 KB | 58.80% | -365 B |
332
- | 稀疏字段(500条×30字段) | 500 | 144.61 KB | 45.40 KB | 68.60% | 45.13 KB | 68.79% | -276 B |
333
- | 稀疏字段(2000条×50字段) | 2,000 | 951.94 KB | 293.62 KB | 69.16% | 292.49 KB | 69.27% | -1.13 KB |
334
- | 深层嵌套(5层组织结构) | 5 | 634.60 KB | 289.02 KB | 54.46% | 289.02 KB | 54.46% | |
335
-
336
- **结论:**
337
- 1. 字段名越长、数量越多,压缩效果越好
338
- 2. 对象数组(订单条目、学生列表)压缩效果显著(55–59%)
339
- 3. 稀疏字段压缩率最高 — 缺失字段的 null 通过空槽省略(67–69%)
340
- 4. `trimTrailingNulls` 在数据有缺失尾部字段时额外节省体积(最高 1.48 KB / 5000 条)
341
- 5. 数据完整无缺失字段时,trim 无额外收益
342
- 6. 深层嵌套结构能获得更好的压缩效果
343
- 7. `stringify` 省略引号进一步减少文本体积
344
-
345
- ## 开发
346
-
347
- ```bash
348
- # 运行测试(192 个用例,100% 覆盖率)
349
- npm test
350
-
351
- # 运行压缩率基准测试(含 trim 对比)
352
- node compress-test.js
353
- ```
354
-
355
- ## GitHub
356
-
357
- [https://github.com/LastHeaven/slimjson](https://github.com/LastHeaven/slimjson)
358
-
359
- ## License
360
-
361
- MIT
1
+ # slimjson
2
+
3
+ 中文 | [English](./README_EN.md)
4
+
5
+ 轻量级对象数组压缩工具 — 将重复 key 的 JSON 对象数组转换为 `{ schema, data }` 紧凑格式,并支持序列化时省略 `null` 以进一步减小体积。
6
+
7
+ ## 适用场景
8
+
9
+ - **API 列表接口**:后端返回列表接口时,每个对象都携带相同的 key 名,大量冗余
10
+ - **异构字段**:不同对象可能拥有不同的字段(后端按需 omit null 字段)
11
+ - **网络传输压缩**:需要在网络传输中极致压缩 JSON 文本体积
12
+ - **大模型上下文压缩**:将大量结构化数据(如数据库查询结果、API 响应、知识库条目)压缩后送入 prompt,减少 token 消耗,降低调用成本
13
+ - **大模型工具调用**:function calling / tool_use 返回的结果往往是结构化的对象数组,压缩后再回传给模型,可显著减少上下文窗口占用,让模型在有限 token 内处理更复杂的数据
14
+ - **大模型识别友好**:压缩后的 `{ schema, data }` 格式将 schema(字段定义)与数据分离,key 只出现一次,模型能更准确地理解数据结构、按字段名提取信息,比重复 key 的原始 JSON 更不容易混淆
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ npm install slimjson
20
+ ```
21
+
22
+ ## API
23
+
24
+ ### `compress(source, opts?)`
25
+
26
+ 将对象数组压缩为 `{ schema, data }` 结构:
27
+
28
+ ```js
29
+ import { compress } from 'slimjson';
30
+
31
+ const users = [
32
+ { name: 'Alice', age: 25, city: 'NYC' },
33
+ { name: 'Bob', age: 30, city: 'LA' },
34
+ ];
35
+
36
+ const compressed = compress(users);
37
+ // {
38
+ // schema: [['name', 'age', 'city']],
39
+ // data: [['Alice', 25, 'NYC'], ['Bob', 30, 'LA']]
40
+ // }
41
+ ```
42
+
43
+ **参数:**
44
+
45
+ | 参数 | 类型 | 默认值 | 说明 |
46
+ |------|------|--------|------|
47
+ | `source` | `Object[]` 或 `Object` | — | 待压缩的对象数组(单个对象会自动包裹为数组) |
48
+ | `opts` | `Object` | | 可选配置 |
49
+ | `opts.trimTrailingNulls` | `boolean` | `false` | 是否去除行尾的 `null` 值 |
50
+
51
+ **特点:**
52
+ - `schema` 取所有对象的 key 并集,按首次出现顺序排列
53
+ - 某对象缺失某字段 → 对应 data 位置填充 `null`
54
+ - 嵌套对象递归处理:`schema` 中表示为 `{ "fieldName": [childKeys] }`
55
+ - 对象数组(如订单条目)同样递归压缩
56
+ - 当传入的是对象时,会当成数组中只有一个对象处理
57
+
58
+ #### 嵌套对象示例
59
+
60
+ ```js
61
+ const data = [
62
+ { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
63
+ { name: '李四', age: 35, profile: { avatar: 'b.jpg', file: null } },
64
+ { name: '王五' },
65
+ ];
66
+
67
+ compress(data);
68
+ // {
69
+ // schema: [['name', 'age', { profile: ['avatar', 'bio', 'file'] }]],
70
+ // data: [
71
+ // ['张三', 28, ['a.jpg', 'Hello', null]],
72
+ // ['李四', 35, ['b.jpg', null, null]],
73
+ // ['王五', null, null]
74
+ // ]
75
+ // }
76
+ ```
77
+
78
+ #### `trimTrailingNulls`:去除尾部 null
79
+
80
+ 启用后,每行及嵌套子行尾部的 `null` 会被去除,进一步压缩体积:
81
+
82
+ ```js
83
+ compress(data, { trimTrailingNulls: true });
84
+ // {
85
+ // schema: [['name', 'age', { profile: ['avatar', 'bio', 'file'] }]],
86
+ // data: [
87
+ // ['张三', 28, ['a.jpg', 'Hello']],
88
+ // ['李四', 35, ['b.jpg']],
89
+ // ['王五']
90
+ // ]
91
+ // }
92
+ ```
93
+
94
+ `decompress` 会自动将缺失的尾部值补回 `null`,roundtrip 还原结果一致:
95
+
96
+ ```js
97
+ decompress(compress(data, { trimTrailingNulls: true }));
98
+ // [
99
+ // { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
100
+ // { name: '李四', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
101
+ // { name: '王五', age: null, profile: null }
102
+ // ]
103
+ ```
104
+
105
+ #### 对象数组示例(订单场景)
106
+
107
+ ```js
108
+ const orders = [
109
+ { orderId: 'A001', items: [{ name: '键盘', price: 299 }, { name: '鼠标', price: 99 }] },
110
+ { orderId: 'A002', items: [{ name: '显示器', price: 1999 }] },
111
+ ];
112
+
113
+ compress(orders);
114
+ // {
115
+ // schema: [['orderId', { items: [['name', 'price']] }]],
116
+ // data: [['A001', [['键盘', 299], ['鼠标', 99]]], ['A002', [['显示器', 1999]]]]
117
+ // }
118
+
119
+ stringify(compress(orders));
120
+ // {schema:[[orderId,{items:[[name,price]]}]],data:[[A001,[[键盘,299],[鼠标,99]]],[A002,[[显示器,1999]]]]}
121
+ // ^^^^^ 嵌套对象 key 无引号 ^^^^ 安全字符串 value 无引号
122
+ ```
123
+
124
+ #### 三层嵌套示例(订单 → 商品 → 规格)
125
+
126
+ ```js
127
+ const orders = [
128
+ {
129
+ orderId: 'A001',
130
+ customer: '张三',
131
+ items: [
132
+ { name: '键盘', price: 299, specs: { color: '黑色', layout: '104键' } },
133
+ { name: '鼠标', price: 99, specs: { color: '白色', dpi: '4000' } },
134
+ ]
135
+ },
136
+ {
137
+ orderId: 'A002',
138
+ customer: '李四',
139
+ items: [
140
+ { name: '显示器', price: 1999, specs: { color: '银色', size: '27寸' } },
141
+ ]
142
+ },
143
+ ];
144
+
145
+ compress(orders);
146
+ // {
147
+ // schema: [[
148
+ // 'orderId',
149
+ // 'customer',
150
+ // { items: [['name', 'price', { specs: ['color', 'layout', 'dpi', 'size'] }]] }
151
+ // ]],
152
+ // data: [
153
+ // ['A001', '张三', [
154
+ // ['键盘', 299, ['黑色', '104键', null, null]],
155
+ // ['鼠标', 99, ['白色', null, '4000', null]]
156
+ // ]],
157
+ // ['A002', '李四', [
158
+ // ['显示器', 1999, ['银色', null, null, '27寸']]
159
+ // ]]
160
+ // ]
161
+ // }
162
+ // specs 的 key 取并集:第一单有 layout,第二单有 size → 都保留,缺失的填 null
163
+
164
+ compress(orders, { trimTrailingNulls: true });
165
+ // data 变为:
166
+ // [
167
+ // ['A001', '张三', [
168
+ // ['键盘', 299, ['黑色', '104键']],
169
+ // ['鼠标', 99, ['白色', null, '4000']]
170
+ // ]],
171
+ // ['A002', '李四', [
172
+ // ['显示器', 1999, ['银色', null, null, '27寸']]
173
+ // ]]
174
+ // ]
175
+
176
+ stringify(compress(orders, { trimTrailingNulls: true }));
177
+ // {schema:[[orderId,customer,{items:[[name,price,{specs:[color,layout,dpi,size]}]]}]],data:[[
178
+ // A001,张三,[[键盘,299,[黑色,"104键"]],[鼠标,99,[白色,,4000]]]],[A002,李四,[[显示器,1999,[银色,,,"27寸"]]]]]}
179
+ ```
180
+
181
+ #### 单对象示例
182
+ ```js
183
+ compress({ name: 'Alice', age: 25 });
184
+ // 等价于 compress([{ name: 'Alice', age: 25 }])
185
+ // { schema: ['name', 'age'], data: ['Alice', 25] }
186
+ ```
187
+
188
+ ### `decompress(compressed)`
189
+
190
+ `{ schema, data }` 还原为原始对象数组。缺失的尾部值会自动补回 `null`:
191
+
192
+ ```js
193
+ const restored = decompress(compressed);
194
+ // deep-equal 原数组
195
+ ```
196
+
197
+ ### `stringify(compressed)`
198
+
199
+ compress 结果序列化为紧凑文本。相比 `JSON.stringify`,应用了以下优化规则:
200
+
201
+ ```js
202
+ const data = [
203
+ { name: 'Alice', age: 25 },
204
+ { name: 'Bob', age: 30 },
205
+ ];
206
+
207
+ const text = stringify(compress(data));
208
+ // {schema:[[name,age]],data:[[Alice,25],[Bob,30]]}
209
+
210
+ JSON.stringify(compress(data));
211
+ // {"schema":[["name","age"]],"data":[["Alice",25],["Bob",30]]}
212
+ ```
213
+
214
+ #### 序列化规则一览
215
+
216
+ | 值类型 | 序列化结果 | 说明 |
217
+ |--------|-----------|------|
218
+ | `null` / `undefined` | `null` | — |
219
+ | 有限数字 | `25` | 直接输出,无引号 |
220
+ | `NaN` / `Infinity` | `null` | 非有限数统一输出 null |
221
+ | `true` / `false` | `true` / `false` | — |
222
+ | 安全字符串 | `Alice` | 省略引号(见下方规则) |
223
+ | 非安全字符串 | `"hello world"` | 保留 JSON 引号和转义 |
224
+ | 嵌套对象 `{k: v}` | `{k:v}` | key 同样区分安全/非安全 |
225
+ | 数组 | 见下方 null 省略规则 | |
226
+
227
+ #### 安全字符串(可省略引号的条件)
228
+
229
+ 满足以下**全部**条件的字符串可省略引号,否则保留 `JSON.stringify` 转义:
230
+
231
+ 1. 非空字符串
232
+ 2. 不是关键字字面量:`null`、`true`、`false`
233
+ 3. 不匹配数字模式:`/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/`(如 `"123"`、`"-3.14"`、`"1e10"` 均保留引号)
234
+ 4. 不以数字或减号 `-` 开头
235
+ 5. 不含空白、`[`、`]`、`{`、`}`、`,`、`:`、`"` 等字符
236
+
237
+ | 字符串 | 结果 | 原因 |
238
+ |--------|------|------|
239
+ | `"Alice"` | `Alice` | 安全,省略引号 |
240
+ | `"hello world"` | `"hello world"` | 含空格 |
241
+ | `"123"` | `"123"` | 看起来像数字 |
242
+ | `"-3.14"` | `"-3.14"` | 看起来像数字 |
243
+ | `"null"` | `"null"` | 关键字 |
244
+ | `""` | `""` | 空字符串 |
245
+ | `"-abc"` | `"-abc"` | 以减号开头 |
246
+ | `"a:b"` | `"a:b"` | 含冒号 |
247
+
248
+ #### 对象 key 引号规则
249
+
250
+ `schema` 中的嵌套对象 key 同样适用安全字符串判断:
251
+
252
+ ```js
253
+ stringify({ schema: [{ profile: ['name', 'age'] }], data: [...] });
254
+ // {schema:[{profile:[name,age]}],data:[...]} ← profile 是安全 key,省略引号
255
+
256
+ stringify({ schema: [{ "my key": ['name'] }], data: [...] });
257
+ // {schema:[{"my key":[name]}],data:[...]} ← my key 含空格,保留引号
258
+ ```
259
+
260
+ #### 数组 null 省略规则
261
+
262
+ 数组中的 `null` / `undefined` 被省略为逗号空槽,不占文字体积:
263
+
264
+ | 原始数组 | 序列化结果 | 说明 |
265
+ |----------------------|------------|------|
266
+ | `["a", null, null]` | `[a,,]` | 尾部两个空槽 |
267
+ | `[null, 1, null]` | `[,1,]` | 前后空槽 |
268
+ | `[]` | `[]` | 空数组 |
269
+ | `[null]` | `[null]` | **特殊**:`[,]` 代表 2 个 null,因此单 null 保留文字 |
270
+
271
+ ### `parse(text)`
272
+
273
+ 解析 `stringify` 产生的文本,将省略的 `null` 恢复:
274
+
275
+ ```js
276
+ const parsed = parse(text);
277
+ // deep-equal compressed
278
+ ```
279
+
280
+ 支持完整 JSON 类型的解析(字符串、数字、布尔、null、嵌套对象/数组),兼容转义字符和 Unicode。
281
+
282
+ ## 完整使用示例
283
+
284
+ ```js
285
+ import { compress, decompress, stringify, parse } from 'slimjson';
286
+
287
+ const data = [
288
+ { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
289
+ { name: '李四', age: 35, profile: { avatar: 'b.jpg' } }, // 缺失 bio
290
+ ];
291
+
292
+ // 压缩 → 文本化 → 解析 → 还原
293
+ const compressed = compress(data);
294
+ const text = stringify(compressed);
295
+ const parsed = parse(text);
296
+ const restored = decompress(parsed);
297
+
298
+ // restored data 深度相等
299
+
300
+ // 启用 trimTrailingNulls 进一步压缩
301
+ const compressedTrim = compress(data, { trimTrailingNulls: true });
302
+ const textTrim = stringify(compressedTrim);
303
+ // textTrim 比 text 更短
304
+ ```
305
+
306
+ ### 压缩率计算
307
+
308
+ ```js
309
+ const originalSize = Buffer.byteLength(JSON.stringify(data));
310
+ const compressedSize = Buffer.byteLength(stringify(compress(data)));
311
+ const ratio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
312
+ console.log(`压缩率: ${ratio}%`);
313
+ ```
314
+
315
+ ## 压缩效果
316
+
317
+ 基于 `compress-test.js` 基准测试的实际数据(18 组测试,所有 roundtrip 解压正确):
318
+
319
+ | 数据类型 | 对象数 | 原始大小 | 不 trim | 压缩率 | trim | 压缩率 | 差值 |
320
+ |---------|-------|---------|---------|-------|------|-------|------|
321
+ | 简单用户 | 100 | 14.69 KB | 8.69 KB | 40.82% | 8.69 KB | 40.82% | — |
322
+ | 简单用户 | 1,000 | 147.74 KB | 87.25 KB | 40.94% | 87.25 KB | 40.94% | — |
323
+ | 简单用户 | 10,000 | 1.45 MB | 881.58 KB | 40.71% | 881.58 KB | 40.71% | — |
324
+ | 嵌套用户(含 profile.social) | 100 | 23.41 KB | 15.28 KB | 34.74% | 15.24 KB | 34.87% | -33 B |
325
+ | 嵌套用户(含 profile.social) | 1,000 | 236.03 KB | 153.93 KB | 34.78% | 153.64 KB | 34.91% | -301 B |
326
+ | 嵌套用户(含 profile.social) | 5,000 | 1.16 MB | 777.89 KB | 34.58% | 776.42 KB | 34.70% | -1.47 KB |
327
+ | 订单(每单1-5商品) | 100 | 31.28 KB | 13.65 KB | 56.38% | 13.65 KB | 56.38% | — |
328
+ | 订单(每单1-5商品) | 500 | 163.18 KB | 70.83 KB | 56.59% | 70.83 KB | 56.59% | — |
329
+ | 订单(每单1-5商品) | 2,000 | 655.99 KB | 284.29 KB | 56.66% | 284.29 KB | 56.66% | |
330
+ | 学校数据(2年级×2班×10生) | 4 | 12.26 KB | 5.25 KB | 57.20% | 5.23 KB | 57.36% | -21 B |
331
+ | 学校数据(6年级×4班×30生) | 24 | 217.73 KB | 89.71 KB | 58.80% | 89.31 KB | 58.98% | -406 B |
332
+ | 学校数据(6年级×6班×50生) | 36 | 539.64 KB | 222.56 KB | 58.76% | 221.66 KB | 58.92% | -923 B |
333
+ | 稀疏字段(100条×20字段) | 100 | 19.50 KB | 6.34 KB | 67.46% | 6.28 KB | 67.78% | -64 B |
334
+ | 稀疏字段(500条×30字段) | 500 | 143.26 KB | 45.09 KB | 68.52% | 44.78 KB | 68.74% | -326 B |
335
+ | 稀疏字段(2000条×50字段) | 2,000 | 957.96 KB | 294.69 KB | 69.24% | 293.54 KB | 69.36% | -1.15 KB |
336
+ | 深层嵌套(小) | 2 | 17.47 KB | 8.08 KB | 53.73% | 8.08 KB | 53.73% | — |
337
+ | 深层嵌套(中) | 3 | 141.89 KB | 64.55 KB | 54.50% | 64.55 KB | 54.50% | — |
338
+ | 深层嵌套(大) | 5 | 629.42 KB | 286.40 KB | 54.50% | 286.40 KB | 54.50% | — |
339
+
340
+ **结论:**
341
+ 1. 字段名越长、数量越多,压缩效果越好
342
+ 2. 对象数组(订单条目、学生列表)压缩效果显著(55–59%)
343
+ 3. 稀疏字段压缩率最高 — 缺失字段的 null 通过空槽省略(67–69%)
344
+ 4. `trimTrailingNulls` 在数据有缺失尾部字段时额外节省体积(最高 1.48 KB / 5000 条)
345
+ 5. 数据完整无缺失字段时,trim 无额外收益
346
+ 6. 深层嵌套结构能获得更好的压缩效果
347
+ 7. `stringify` 省略引号进一步减少文本体积
348
+
349
+ ## Token 效率对比
350
+
351
+ 与其他格式的 token 消耗对比(基于 6 个数据集的实际测试)。
352
+
353
+ #### 混合结构赛道
354
+
355
+ 含嵌套或半均匀结构的数据集。CSV 无法表示此类结构,已排除。
356
+
357
+ ```
358
+ 🛒 电商订单(嵌套结构) ┊ 表格化程度: 33%
359
+
360
+ slimjson ████████░░░░░░░░░░░░ 46,233 tokens
361
+ ├─ vs JSON (−57.8%) 109,574 tokens
362
+ ├─ vs JSON compact (−33.5%) 69,528 tokens
363
+ ├─ vs TOON (−36.9%) 73,246 tokens
364
+ ├─ vs YAML (−45.9%) 85,451 tokens
365
+ └─ vs XML (−62.5%) 123,272 tokens
366
+
367
+ 📃 半均匀事件日志 ┊ 表格化程度: 50%
368
+
369
+ slimjson ██████████░░░░░░░░░░ 91,630 tokens
370
+ ├─ vs JSON (−49.4%) 181,141 tokens
371
+ ├─ vs JSON compact (−28.7%) 128,480 tokens
372
+ ├─ vs TOON (−40.5%) 154,032 tokens
373
+ ├─ vs YAML (−41.0%) 155,346 tokens
374
+ └─ vs XML (−55.5%) 205,796 tokens
375
+
376
+ 🧩 深层嵌套配置 ┊ 表格化程度: 0%
377
+
378
+ slimjson ████████████░░░░░░░░ 547 tokens
379
+ ├─ vs JSON (−39.6%) 905 tokens
380
+ ├─ vs JSON compact (−0.9%) 552 tokens
381
+ ├─ vs TOON (−11.5%) 618 tokens
382
+ ├─ vs YAML (−17.4%) 662 tokens
383
+ └─ vs XML (−45.1%) 997 tokens
384
+
385
+ ──────────────────────────────────── 合计 ────────────────────────────────────
386
+ slimjson █████████░░░░░░░░░░░ 138,410 tokens
387
+ ├─ vs JSON (−52.5%) 291,620 tokens
388
+ ├─ vs JSON compact (−30.3%) 198,560 tokens
389
+ ├─ vs TOON (−39.3%) 227,896 tokens
390
+ ├─ vs YAML (−42.7%) 241,459 tokens
391
+ └─ vs XML (−58.1%) 330,065 tokens
392
+ ```
393
+
394
+ #### 纯表格赛道
395
+
396
+ 扁平表格结构数据集,CSV 可适用。
397
+
398
+ ```
399
+ 👥 均匀员工记录 ┊ 表格化程度: 100%
400
+
401
+ CSV ████████████████████ 47,137 tokens
402
+ slimjson ████████████████████ 47,067 tokens (-0.1% vs CSV)
403
+ ├─ vs JSON (−63.0%) 127,050 tokens
404
+ ├─ vs JSON compact (−40.5%) 79,046 tokens
405
+ ├─ vs TOON (−5.8%) 49,966 tokens
406
+ ├─ vs YAML (−52.9%) 100,033 tokens
407
+ └─ vs XML (−67.9%) 146,596 tokens
408
+
409
+ 📈 时间序列分析数据 ┊ 表格化程度: 100%
410
+
411
+ CSV ███████████████████░ 8,392 tokens
412
+ slimjson ████████████████████ 8,767 tokens (+4.5% vs CSV)
413
+ ├─ vs JSON (−60.6%) 22,254 tokens
414
+ ├─ vs JSON compact (−38.3%) 14,220 tokens
415
+ ├─ vs TOON (−3.9%) 9,124 tokens
416
+ ├─ vs YAML (−50.9%) 17,867 tokens
417
+ └─ vs XML (−67.1%) 26,625 tokens
418
+
419
+ ⭐ Top 100 GitHub 仓库 ┊ 表格化程度: 100%
420
+
421
+ CSV ████████████████████ 8,512 tokens
422
+ slimjson ████████████████████ 8,550 tokens (+0.4% vs CSV)
423
+ ├─ vs JSON (−43.5%) 15,144 tokens
424
+ ├─ vs JSON compact (−25.4%) 11,454 tokens
425
+ ├─ vs TOON (−2.2%) 8,744 tokens
426
+ ├─ vs YAML (−34.9%) 13,128 tokens
427
+ └─ vs XML (−50.0%) 17,095 tokens
428
+
429
+ ──────────────────────────────────── 合计 ────────────────────────────────────
430
+ CSV ████████████████████ 64,041 tokens
431
+ slimjson ████████████████████ 64,384 tokens (+0.5% vs CSV)
432
+ ├─ vs JSON (−60.8%) 164,448 tokens
433
+ ├─ vs JSON compact (−38.5%) 104,720 tokens
434
+ ├─ vs TOON (−5.1%) 67,834 tokens
435
+ ├─ vs YAML (−50.9%) 131,028 tokens
436
+ └─ vs XML (−66.2%) 190,316 tokens
437
+ ```
438
+
439
+ > 在混合结构数据上,slimjson 比 JSON 节省 **52.5%** tokens;在纯表格数据上,与 CSV 基本持平(仅多 0.5%)。
440
+
441
+ ## LLM 数据检索准确率
442
+
443
+ 使用 209 道数据检索题测试不同格式下 LLM 的理解准确率。
444
+
445
+ #### 效率排名(每 1K tokens 的准确率)
446
+
447
+ ```
448
+ slimjson ████████████████████ 44.4 acc%/1K tok │ 94.7% acc │ 2,134 tokens
449
+ TOON ███████████████░░░░░ 34.0 acc%/1K tok │ 92.8% acc │ 2,734 tokens
450
+ JSON compact ██████████████░░░░░░ 31.0 acc%/1K tok │ 95.2% acc │ 3,072 tokens
451
+ YAML ███████████░░░░░░░░░ 25.4 acc%/1K tok │ 94.3% acc │ 3,716 tokens
452
+ JSON ██████████░░░░░░░░░░ 21.1 acc%/1K tok │ 95.7% acc │ 4,538 tokens
453
+ XML ████████░░░░░░░░░░░░ 18.5 acc%/1K tok │ 95.7% acc │ 5,162 tokens
454
+ ```
455
+
456
+ *效率分数 = (准确率% ÷ tokens) × 1,000,越高越好。*
457
+
458
+ > slimjson 准确率 **94.7%**(vs JSON 的 95.7%),同时节省 **53.0%** tokens。
459
+
460
+ #### 各模型准确率
461
+
462
+ ```
463
+ deepseek-v4-flash
464
+ JSON ███████████████████░ 95.7% (200/209)
465
+ XML ███████████████████░ 95.7% (200/209)
466
+ JSON compact ███████████████████░ 95.2% (199/209)
467
+ → slimjson ███████████████████░ 94.7% (198/209)
468
+ YAML ███████████████████░ 94.3% (197/209)
469
+ TOON ███████████████████░ 92.8% (194/209)
470
+ CSV ██████████████████░░ 91.7% (100/109)
471
+ ```
472
+
473
+ #### 按题型准确率
474
+
475
+ | 题型 | JSON | XML | JSON compact | slimjson | YAML | TOON | CSV |
476
+ |------|------|-----|-------------|----------|------|------|-----|
477
+ | 字段检索 | 98.5% | 97.1% | 98.5% | 95.6% | 97.1% | 91.2% | 96.9% |
478
+ | 聚合计算 | 98.4% | 96.8% | 95.2% | 95.2% | 93.7% | 95.2% | 86.2% |
479
+ | 条件筛选 | 97.9% | 97.9% | 100.0% | 100.0% | 100.0% | 100.0% | 96.3% |
480
+ | 结构感知 | 88.0% | 92.0% | 84.0% | 92.0% | 88.0% | 88.0% | 87.5% |
481
+ | 结构验证 | 40.0% | 60.0% | 60.0% | 40.0% | 40.0% | 40.0% | 80.0% |
482
+
483
+ #### 测试数据集
484
+
485
+ | 数据集 | 行数 | 结构类型 | CSV 支持 |
486
+ |--------|------|----------|----------|
487
+ | 均匀员工记录 | 100 | 均匀 | ✓ |
488
+ | 电商订单(嵌套结构) | 50 | 嵌套 | ✗ |
489
+ | 时间序列分析数据 | 60 | 均匀 | ✓ |
490
+ | Top 100 GitHub 仓库 | 100 | 均匀 | ✓ |
491
+ | 半均匀事件日志 | 75 | 半均匀 | ✗ |
492
+ | 深层嵌套配置 | 11 | 深层 | ✗ |
493
+
494
+ ## 开发
495
+
496
+ ```bash
497
+ # 运行测试(192 个用例,100% 覆盖率)
498
+ npm test
499
+
500
+ # 运行压缩率基准测试(含 trim 对比)
501
+ node compress-test.js
502
+ ```
503
+
504
+ ## GitHub
505
+
506
+ [https://github.com/LastHeaven/slimjson](https://github.com/LastHeaven/slimjson)
507
+
508
+ ## License
509
+
510
+ MIT