雜記 › 技術筆記 › 16 個鍵進、4 個欄出:JSON 轉表格的表頭聯集、點號展平與 CSV 引號規則

16 個鍵進、4 個欄出: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