杂记 › 技术笔记 › JSON 转表格的表头到底从哪来:键并集、点号展平与 CSV 引号的真实规则

JSON 转表格的表头到底从哪来:键并集、点号展平与 CSV 引号的真实规则

表头取全部记录键的并集,展平用点号不拍下标,csvCell 只认逗号、双引号、CR、LF;实测 16 个原型同名键进 4 列出,下载比复制多 25 字节。

三个对不上的数

一段 JSON 贴进去,状态栏列数、下载文件字节数、剪贴板文本字节数经常互相解释不了:多出来的键进不进表头、下载为什么比复制多 25 字节、16 个键为什么只剩 4 列。我把模板里的 generateCSV、csvCell、copyTable 逐字抽出来,喂给 Node 与 headless Chrome 跑了一遍。

表头是全部记录键的并集

第 138–144 行逐条收集键并去重,只在没见过时追加。输入 [{"a":1,"b":2},{"a":3,"c":4}] 出 3 列、allFields=["a","b","c"]、csv 19 字节;按第一条记录的键算只有 2 列,c 会整列消失。50 条互不相交的键出 50 列。

本篇样例 3 条记录、并集 24 个键,勾不勾展平都是 24 列:addr 拆成两个键多 1 列,empty:{} 少 1 列。缺键的单元格走第 159、200 行,预览是空 <td></td>,CSV 里是两个逗号之间的空串。

50 条互不相交的键50 列本篇样例的键并集24 列两条各 2 键3 列只看第一条记录2 列16 键含 12 个原型名4 列按第 138–144 行的并集统计

图 1:并集列数与两种误算口径。

点号展平,数组不拍下标

第 113 行 prefix + '.' + key 写死点号:{"u":{"id":{"v":1}}} 出 u.id.v,N 层同名键的键长 = 2N − 1,1000 层实测 1999 字符。数组走第 116–117 行整段 JSON.stringify,{"a":[1,2,3]} 的键仍是 a,值被加引号成 "[1,2,3]",一个下标列都不会有。

递归没有深度上限参数:1000 层与 3000 层正常出表,6000 层稳定 RangeError: Maximum call stack size exceeded;同一份输入压到 100000 层,JSON.parse 仍成功,爆的是模板自己的递归,红框标题照样写「JSON 解析错误」。

加引号只认 4 个字符

第 208 行 /[",\r\n]/ 是唯一条件:双引号、逗号、CR、LF,命中就首尾各加 1 个引号,值里的双引号按第 209 行翻倍,1 字节的 " 出 4 字节。表头也过第 195 行的 csvCell,键 a,b 的首行是 "a,b"。

首尾带空格的 前置空格 、分号 甲;乙;丙、制表符 甲\t乙,以及 =1+1、+86138、-123、@mention,全部原样不加引号。

三条通路三种字节

BOM 不在 generateCSV 里,第 193 行从空串起手,只在第 219 行 new Blob(['\uFEFF' + csv]) 补上:下载字节 = csv 字节 + 3,12 组实测无一例外。CSV 行尾是 CRLF(第 195、201 行两处 + '\r\n'),复制是单个 LF 加制表符(第 241、246 行)且不调 csvCell,末尾还有第 248 行 text.trim() 削首尾。

通路字节分隔与行尾过不过 csvCell
generateCSV()435逗号 + CRLF过
下载的 Blob438再加 3 字节 BOM过
copyTable()413制表符 + LF不过

三个数出自本篇 3 条记录、24 列、表头开且不展平的样例:25 字节的差 = 3 字节 BOM + 4 个行终止 CR + 18 个引号字符 − 被 trim 削掉的行尾字符。

不转义的后果:输入 [{"a":"x","b":"甲\t乙","c":"d"},{"a":"p\nq","b":"r","c":"s"}],csv 31 字节、列数守住,复制文本每行制表符数却是 2,3,0,2、共 4 个物理行。而 [{"a":" x "}] 关表头时 csv 7 字节保住两侧空格,复制只剩 1 个字节。在 JSON转Excel 里走复制之前,我一般先确认值里没有制表符和换行。

csv 字符串435 字节下载文件438 字节复制文本413 字节同一条 24 列样例差 25 字节

图 2:一份输入、三条出口。

16 个键进、4 个列出

第 139、142 行用 {} 做去重集合,查的是原型链,于是与 Object.prototype 同名的键一进来就是真值、永远进不了 fields。实测吃掉 12 个,含 constructor、toString、valueOf、__proto__;把 length、name、value、x 这四个正常键一起塞进同一条记录,16 键进、列: 4。

并集被吃空后第 191 行退化成只取第一条记录的键:输入 [{"constructor":1},{"toString":2},{"valueOf":3}],预览 0 列,csv 却有 90 字节,两次出现 33 字符的 function Object() { [native code] },出自第 200 行 String(v)。第 111 行又假定 hasOwnProperty 一定存在,[{"hasOwnProperty":1,"a":2}] 勾展平就抛 obj.hasOwnProperty is not a function。

不勾展平时值变成了什么

第 21 行的 j2e-flatten 默认不勾,第 134 行因此不做展平,嵌套值被 String(v) 写成 15 字符的 [object Object]:本篇 addr 列两行都是它,北京和 100000 在导出里找不到;数组变成加引号的 "1,2"。

空对象更隐蔽:{"a":{},"b":1} 勾展平后 2 键进 1 列出,整列消失;null 只把值写空、列保留。而 null、空串 "" 和「这一行没这个键」在 CSV 里都是 a,b\r\n1,\r\n,字节完全相同。

这一页产的是 CSV

下载文件前 8 字节实测 ef bb bf 6e 61 6d 65 2c,BOM 之后直接是 name,;ZIP 签名 50 4b 03 04 与 OLE 签名 d0 cf 11 e0 都没命中,type 是 23 字符的 text/csv;charset=utf-8;。粘进表格软件那一侧要防什么,json-to-excel 与 CSV 那篇写过。

escHtml(第 176–178 行只动 &、<、> 和双引号这 4 个字符)只被第 151 行的 <th> 与第 159 行的 <td> 调用,导出通路 0 次,所以它只污染预览,text.trim() 只影响复制文本,下载字节里两样都不掺。

问答

Q:列数为什么比记录里见过的键少?

两种:键名撞上 Object.prototype 的那 12 个成员,整列被吃掉;或者值是空对象 {},勾展平后整列消失。

Q:下载为什么比复制多 25 字节?

差在 3 字节 BOM、4 个行终止 CR 和 18 个引号字符上,再扣掉 trim 削去的行尾字符;但下载 = csv + 3 这条不变。

Q:能让分号或以 = 开头的值也加引号吗?

不能,第 208 行那个字符集没有开关,页面也不是全量转义。

📎 相关文章

16 组开关实测 HTML 去标签:报 19 个标签,实际删掉 148 个字符 2026-10-30 SVG 转 PNG 后外链图丢了一半:同一份 594 B 文件预览有 3 张图、成品只剩 1 张 2026-10-30 二进制转 Base64 这一页:前缀只有 5 档,换行能解、连字符和下划线直接报错 2026-10-30 给应用做商店介绍图,点导出拿到的却是 1242×2688:8 档画布预设的像素账、噪点 24.83 倍体积与默认被裁掉 134.93 px 的手机框 2026-10-30 取消勾选「保留注释」后,format-js 把 24 字符 URL 压成 13 字符残缺码 2026-10-30