slimjson 1.0.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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node compress-test.js)",
5
+ "Bash(node *)",
6
+ "Bash(npx jest *)"
7
+ ]
8
+ }
9
+ }
package/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # slimjson
2
+
3
+ 轻量级对象数组压缩工具 — 将重复 key 的 JSON 对象数组转换为 `{ keys, rows }` 紧凑格式,并支持序列化时省略 `null` 以进一步减小体积。
4
+
5
+ ## 适用场景
6
+
7
+ - 后端返回列表接口时,每个对象都携带相同的 key 名,大量冗余
8
+ - 不同对象可能拥有不同的字段(后端按需 omit null 字段)
9
+ - 需要在网络传输中极致压缩 JSON 文本体积
10
+ - **大模型上下文压缩**:将大量结构化数据(如数据库查询结果、API 响应、知识库条目)压缩后送入 prompt,减少 token 消耗,降低调用成本
11
+ - **大模型工具调用**:function calling / tool_use 返回的结果往往是结构化的对象数组,压缩后再回传给模型,可显著减少上下文窗口占用,让模型在有限 token 内处理更复杂的数据
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ npm install slimjson
17
+ ```
18
+
19
+ ## API
20
+
21
+ ### `compress(source)`
22
+
23
+ 将对象数组压缩为 `{ keys, rows }` 结构:
24
+
25
+ ```js
26
+ const { compress } = require('slimjson');
27
+
28
+ const users = [
29
+ { name: 'Alice', age: 25, city: 'NYC' },
30
+ { name: 'Bob', age: 30, city: 'LA' },
31
+ ];
32
+
33
+ const compressed = compress(users);
34
+ // {
35
+ // keys: ['name', 'age', 'city'],
36
+ // rows: [
37
+ // ['Alice', 25, 'NYC'],
38
+ // ['Bob', 30, 'LA' ]
39
+ // ]
40
+ // }
41
+ ```
42
+
43
+ **特点:**
44
+ - `keys` 取所有对象的 key 并集,按首次出现顺序排列
45
+ - 某对象缺失某字段 → 对应 row 位置填充 `null`
46
+ - 嵌套对象递归处理:`keys` 中表示为 `{ "fieldName": [childKeys] }`
47
+ - 对象数组(如订单条目)同样递归压缩
48
+ - 当传入的是对象时,会当成数组中只有一个对象处理
49
+
50
+ #### 嵌套对象示例
51
+
52
+ ```js
53
+ const data = [
54
+ { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
55
+ { name: '李四', age: 35, profile: { avatar: 'b.jpg' } }, // 缺失 bio
56
+ ];
57
+
58
+ compress(data);
59
+ // {
60
+ // keys: ['name', 'age', { profile: ['avatar', 'bio'] }],
61
+ // rows: [
62
+ // ['张三', 28, ['a.jpg', 'Hello']],
63
+ // ['李四', 35, ['b.jpg', null ]]
64
+ // ]
65
+ // }
66
+
67
+ stringify(compress(data));
68
+ // {keys:[name,age,{profile:[avatar,bio]}],rows:[[张三,28,[a.jpg,Hello]],[李四,35,[b.jpg,]]]}
69
+ // ^^ 省略 null,保留逗号
70
+ ```
71
+
72
+ #### 对象数组示例(订单场景)
73
+
74
+ ```js
75
+ const orders = [
76
+ { orderId: 'A001', items: [{ name: '键盘', price: 299 }, { name: '鼠标', price: 99 }] },
77
+ { orderId: 'A002', items: [{ name: '显示器', price: 1999 }] },
78
+ ];
79
+
80
+ compress(orders);
81
+ // {
82
+ // keys: ['orderId', { items: ['name', 'price'] }],
83
+ // rows: [
84
+ // ['A001', [['键盘', 299], ['鼠标', 99]]],
85
+ // ['A002', [['显示器', 1999]]]
86
+ // ]
87
+ // }
88
+
89
+ stringify(compress(orders));
90
+ // {keys:[orderId,{items:[name,price]}],rows:[[A001,[[键盘,299],[鼠标,99]]],[A002,[[显示器,1999]]]]}
91
+ // ^^^^^ 嵌套对象 key 无引号 ^^^^ 安全字符串 value 无引号
92
+ ```
93
+
94
+ #### 三层嵌套示例(订单 → 商品 → 规格)
95
+
96
+ ```js
97
+ const orders = [
98
+ {
99
+ orderId: 'A001',
100
+ customer: '张三',
101
+ items: [
102
+ { name: '键盘', price: 299, specs: { color: '黑色', layout: '104键' } },
103
+ { name: '鼠标', price: 99, specs: { color: '白色', dpi: '4000' } },
104
+ ]
105
+ },
106
+ {
107
+ orderId: 'A002',
108
+ customer: '李四',
109
+ items: [
110
+ { name: '显示器', price: 1999, specs: { color: '银色', size: '27寸' } },
111
+ ]
112
+ },
113
+ ];
114
+
115
+ compress(orders);
116
+ // {
117
+ // keys: [
118
+ // 'orderId',
119
+ // 'customer',
120
+ // { items: ['name', 'price', { specs: ['color', 'layout', 'dpi', 'size'] }] }
121
+ // ],
122
+ // rows: [
123
+ // ['A001', '张三', [
124
+ // ['键盘', 299, ['黑色', '104键', null, null]],
125
+ // ['鼠标', 99, ['白色', null, '4000', null]]
126
+ // ]],
127
+ // ['A002', '李四', [
128
+ // ['显示器', 1999, ['银色', null, null, '27寸']]
129
+ // ]]
130
+ // ]
131
+ // }
132
+ // specs 的 key 取并集:第一单有 layout,第二单有 size → 都保留,缺失的填 null
133
+
134
+ stringify(compress(orders));
135
+ // {keys:[orderId,customer,{items:[name,price,{specs:[color,layout,dpi,size]}]}],rows:[[
136
+ // A001,张三,[[键盘,299,[黑色,104键,,]],[鼠标,99,[白色,,4000,]]]],[A002,李四,[[显示器,1999,[银色,,,27寸]]]]]}
137
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ specs 中缺失字段用空槽省略 null ^^^^^^^^^^^^^^^^^^^^^^^^
138
+ ```
139
+
140
+ ### `decompress(compressed)`
141
+
142
+ 将 `{ keys, rows }` 还原为原始对象数组:
143
+
144
+ ```js
145
+ const restored = decompress(compressed);
146
+ // deep-equal 原数组
147
+ ```
148
+
149
+ ### `stringify(compressed)`
150
+
151
+ 将 compress 结果序列化为文本,数组中 `null` 值被省略(保留逗号占位),安全的字符串省略引号:
152
+
153
+ ```js
154
+ const data = [
155
+ { name: 'Alice', age: 25 },
156
+ { name: 'Bob', age: 30 },
157
+ ];
158
+
159
+ const text = stringify(compress(data));
160
+ // {keys:[name,age],rows:[[Alice,25],[Bob,30]]}
161
+ ```
162
+
163
+ 对比 JSON.stringify:
164
+ ```js
165
+ JSON.stringify(compress(data));
166
+ // {"keys":["name","age"],"rows":[["Alice",25],["Bob",30]]}
167
+ // ↑ 引号 ↑ ↑ 引号 ↑
168
+ ```
169
+
170
+ 数组 null 省略规则:
171
+
172
+ | 原始数组 | 序列化结果 | 说明 |
173
+ |----------------------|------------|------|
174
+ | `["a", null, null]` | `[a,,]` | 尾部两个空槽 |
175
+ | `[null, 1, null]` | `[,1,]` | 前后空槽 |
176
+ | `[null, "1", null]` | `[,"1",]` | 前后空槽 |
177
+ | `[null, "1a", null]` | `[,"1a",]` | 前后空槽 |
178
+ | `[]` | `[]` | 空数组 |
179
+ | `[null]` | `[null]` | **特殊**:`[,]` 代表 2 个 null,因此单 null 保留文字 |
180
+
181
+ ### `parse(text)`
182
+
183
+ 解析 `stringify` 产生的文本,将省略的 `null` 恢复:
184
+
185
+ ```js
186
+ const parsed = parse(text);
187
+ // deep-equal compressed
188
+ ```
189
+
190
+ 支持完整 JSON 类型的解析(字符串、数字、布尔、null、嵌套对象/数组),兼容转义字符和 Unicode。
191
+
192
+ ## 完整使用示例
193
+
194
+ ```js
195
+ const { compress, decompress, stringify, parse } = require('slimjson');
196
+
197
+ const data = [
198
+ { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
199
+ { name: '李四', age: 35, profile: { avatar: 'b.jpg' } }, // 缺失 bio
200
+ ];
201
+
202
+ // 压缩 → 文本化 → 解析 → 还原
203
+ const compressed = compress(data);
204
+ const text = stringify(compressed);
205
+ const parsed = parse(text);
206
+ const restored = decompress(parsed);
207
+
208
+ // restored 与 data 深度相等
209
+ ```
210
+
211
+ ### 压缩率计算
212
+
213
+ ```js
214
+ const originalSize = Buffer.byteLength(JSON.stringify(data));
215
+ const compressedSize = Buffer.byteLength(stringify(compress(data)));
216
+ const ratio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
217
+ console.log(`压缩率: ${ratio}%`);
218
+ ```
219
+
220
+ ## 压缩效果
221
+
222
+ 基于 `compress-test.js` 基准测试的实际数据(18 组测试,平均压缩率 **52.20%**,所有 roundtrip 解压正确):
223
+
224
+ | 数据类型 | 对象数 | 原始大小 | 压缩后 | 压缩率 |
225
+ |---------|-------|---------|-------|-------|
226
+ | 简单用户 | 1,000 | 147.85 KB | 87.36 KB | **40.91%** |
227
+ | 简单用户 | 10,000 | 1.45 MB | 882.34 KB | **40.69%** |
228
+ | 嵌套用户(含 profile.social) | 1,000 | 235.28 KB | 153.03 KB | **34.96%** |
229
+ | 订单(每单1-5商品) | 500 | 166.34 KB | 72.10 KB | **56.66%** |
230
+ | 学校数据(6年级×4班×30生) | 24 | 215.47 KB | 88.39 KB | **58.98%** |
231
+ | 稀疏字段(500条×30字段) | 500 | 144.68 KB | 45.39 KB | **68.62%** |
232
+ | 稀疏字段(2000条×50字段) | 2,000 | 947.88 KB | 292.74 KB | **69.12%** |
233
+ | 深层嵌套(5层组织结构) | 5 | 634.65 KB | 288.75 KB | **54.50%** |
234
+
235
+ **结论:**
236
+ 1. 字段名越长、数量越多,压缩效果越好
237
+ 2. 对象数组(订单条目、学生列表)压缩效果显著(55–59%)
238
+ 3. 稀疏字段压缩率最高 — 缺失字段的 null 通过空槽省略(67–69%)
239
+ 4. 深层嵌套结构能获得更好的压缩效果
240
+ 5. `stringify` 省略引号进一步减少文本体积
241
+
242
+ ## 开发
243
+
244
+ ```bash
245
+ # 运行测试
246
+ npm test
247
+
248
+ # 运行压缩率基准测试
249
+ node compress-test.js
250
+ ```
251
+
252
+ ## License
253
+
254
+ ISC
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 用法: node compress-file.js <输入.json> [输出]
3
+ *
4
+ * 默认输出文件名: <输入名>.json.slim
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { compress, stringify } = require('./compress');
9
+
10
+ const input = process.argv[2];
11
+ if (!input) {
12
+ console.error('用法: node compress-file.js <输入.json> [输出]');
13
+ process.exit(1);
14
+ }
15
+ if (!fs.existsSync(input)) {
16
+ console.error(`文件不存在: ${input}`);
17
+ process.exit(1);
18
+ }
19
+
20
+ const output = (process.argv[3] || input).replace(/\.json$/i, '') + '.json.slim';
21
+
22
+ let data;
23
+ try {
24
+ data = JSON.parse(fs.readFileSync(input, 'utf8'));
25
+ } catch (e) {
26
+ console.error(`JSON 解析失败: ${e.message}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const compressed = compress(data);
31
+ const text = stringify(compressed);
32
+
33
+ fs.writeFileSync(output, text, 'utf8');
34
+
35
+ const originalSize = Buffer.byteLength(JSON.stringify(data), 'utf8');
36
+ const newSize = Buffer.byteLength(text, 'utf8');
37
+ const ratio = ((originalSize - newSize) / originalSize * 100).toFixed(2);
38
+
39
+ console.log(`输入: ${path.basename(input)} (${(originalSize / 1024).toFixed(2)} KB)`);
40
+ console.log(`输出: ${path.basename(output)} (${(newSize / 1024).toFixed(2)} KB)`);
41
+ console.log(`压缩率: ${ratio}%`);
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 用法: node compress-ratio.js <json文件路径>
3
+ *
4
+ * 读取 JSON 文件压缩,输出压缩率。
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { compress, stringify } = require('./compress');
9
+
10
+ function getByteSize(obj) {
11
+ return Buffer.byteLength(JSON.stringify(obj), 'utf8');
12
+ }
13
+
14
+ function formatBytes(bytes) {
15
+ if (bytes < 1024) return `${bytes} B`;
16
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
17
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
18
+ }
19
+
20
+ // ---------- 参数解析 ----------
21
+ const filePath = process.argv[2];
22
+ if (!filePath) {
23
+ console.error('用法: node compress-ratio.js <json文件>');
24
+ process.exit(1);
25
+ }
26
+ if (!fs.existsSync(filePath)) {
27
+ console.error(`文件不存在: ${filePath}`);
28
+ process.exit(1);
29
+ }
30
+
31
+ // ---------- 读取并解析 ----------
32
+ let raw;
33
+ try {
34
+ raw = fs.readFileSync(filePath, 'utf8');
35
+ } catch (e) {
36
+ console.error(`读取文件失败: ${e.message}`);
37
+ process.exit(1);
38
+ }
39
+
40
+ let data;
41
+ try {
42
+ data = JSON.parse(raw);
43
+ } catch (e) {
44
+ console.error(`JSON 解析失败: ${e.message}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ const originalSize = Buffer.byteLength(JSON.stringify(data), 'utf8');
49
+
50
+ // ---------- 压缩 ----------
51
+ const compressed = compress(data);
52
+ const compressedStr = stringify(compressed);
53
+
54
+ const compressedSize = Buffer.byteLength(compressedStr, 'utf8');
55
+
56
+ const savings = originalSize - compressedSize;
57
+ const ratio = originalSize === 0 ? 0 : (savings / originalSize * 100);
58
+
59
+ // ---------- 输出 ----------
60
+ const fileName = path.basename(filePath);
61
+
62
+ console.log(`\n文件: ${fileName}`);
63
+ console.log(`原始大小: ${formatBytes(originalSize)}`);
64
+ console.log(`压缩后: ${formatBytes(compressedSize)}`);
65
+ console.log(`节省: ${formatBytes(savings)} (${ratio.toFixed(2)}%)`);
66
+
67
+ // 如果是数组,额外输出元素数量
68
+ if (Array.isArray(data)) {
69
+ console.log(`元素数量: ${data.length}`);
70
+ }