問題的本質
工作表是矩形,JSON 是樹。把前者變成後者,意味著要回答一個沒有普適答案的問題:一個分支該變成什麼?
JSON 裡只有兩種分支,而它們的行為截然不同。
物件是一組固定的具名欄位。 {"city": "Berlin", "zip": "10115"} 對這筆記錄來說永遠就是這兩個鍵。固定的名字集合,能乾淨地對應到固定的欄集合。
陣列是數量未知的一串元素。 ["pro", "beta"] 這筆記錄裡有兩個,下一筆可能有九個。沒有哪個固定的欄數裝得下它。
這個區別決定了本文剩下的全部內容:物件可以展平,陣列不行。
物件是怎麼變成欄的
展平會走訪這棵樹,為每個葉節點值收集一條路徑:
{
"id": 1024,
"name": "Alice Chen",
"address": {
"city": "Berlin",
"zip": "10115"
}
}
找到的路徑:id、name、address.city、address.zip。每條各成一欄。
各家工具的差別在標題上。用完整點號路徑精確,但讀起來很不友善:
| id | name | address.city | address.zip |
|---|
用最後一段則可讀,也更符合試算表的實際用途:
| Id | Name | City | Zip |
|---|
取最後一段的做法還會順帶規範命名:底線和連字號變成空格,駝峰拆成單字,首字母大寫。於是 created_at 變成 Created at,orderId 變成 Order Id。
完整路徑並沒有消失——它作為這一欄的來源路徑保留下來,也正是你想讓這一欄取別的值時要改的東西。可讀的標題和精確的來源,兩者同時具備。
唯一會咬人的情況
兩個不同分支可能以同一個詞結尾:
{
"user": { "name": "Alice" },
"company": { "name": "Northwind" }
}
路徑是 user.name 和 company.name,標題都是 Name。
欄裡的值是對的,來源路徑也仍然可區分,但一張有兩個同名欄的試算表會讓查閱函式出錯,也會讓開啟它的人困惑。匯出前改掉一個標題。每次轉換不熟悉的結構時都值得檢查這一項——這是展平過程中最常見的意外。
深度與欄數膨脹
物件展平是無損的,但寬度漲得很快。一筆記錄裡有四個各含五個欄位的巢狀物件,還沒算最上層欄位就已經二十欄了。
Excel 單張工作表上限 16,384 欄,聽起來很寬裕,直到你去轉一份巢狀很深的設定資料。而可讀性失效得比這早得多。如果展平後超過幾十欄,這是個訊號:這份 JSON 同時在描述好幾種東西,它更應該變成幾張工作表,而不是一張很寬的表。
陣列是怎麼變成儲存格的
基本型別的陣列——字串、數字、布林——在單個儲存格裡有三種合理表示。以 tags: ["pro", "beta"] 為例:
| 模式 | 結果 | 什麼時候用 |
|---|---|---|
| 保留 JSON | ["pro","beta"] |
後續還有程式會再解析這個儲存格 |
| 串接 | pro, beta |
這張表是給人看的 |
| 計數 | 2 |
你要按數量排序、加總或做樞紐分析 |
選之前值得先知道邊界情況。空陣列 [] 在保留 JSON 時是文字 [],串接時是空儲存格,計數時是數字 0。只有計數模式給出的是能直接參與算術的值。
最容易讓人失望的是串接模式,因為它看起來沒問題,直到陣列裡裝的是物件而不是標籤:
{"sku":"A-1","qty":2}, {"sku":"B-7","qty":1}
這是一次正確的串接,也是一個沒法用的結果。於是就引出了真正的解法。
物件陣列怎麼變成第二張工作表
物件陣列是一對多關係。用關聯式的話說,它是一張子表,而它在試算表裡的正確表現形式是第二張工作表,不是一個擠爆的儲存格。
拿兩個帶訂單的客戶舉例:
{
"data": [
{
"id": 1024,
"name": "Alice Chen",
"orders": [
{ "sku": "A-1", "qty": 2 },
{ "sku": "B-7", "qty": 1 }
]
},
{
"id": 1025,
"name": "Bob Miller",
"orders": [{ "sku": "C-3", "qty": 5 }]
}
]
}
主表一個客戶一列。orders 陣列額外產生一張以其路徑命名的工作表:
工作表:Orders
| Parent row | Item index | Parent id | Parent name | Sku | Qty |
|---|---|---|---|---|---|
| 1 | 1 | 1024 | Alice Chen | A-1 | 2 |
| 1 | 2 | 1024 | Alice Chen | B-7 | 1 |
| 2 | 1 | 1025 | Bob Miller | C-3 | 5 |
其中三欄的存在,純粹是為了保住原本由樹狀結構編碼的那層關係:
- Parent row —— 這個元素來自主表的第幾列。
- Item index —— 元素在其陣列中的位置,讓原始順序不丟。
- Parent id / Parent name —— 從父記錄裡複製下來的識別欄位。
這些識別欄取自父記錄中可辨識的欄位名:id、uuid、uid、key、code、name、email、orderId、order_id、createdAt、created_at。父記錄裡存在哪些,就複製哪些到子列。
有了這張表,「按客戶彙總 Qty」就是兩分鐘的樞紐分析表。而同樣的資料擠在一個儲存格裡當 JSON 文字,就是一份手工重打的工作。
直接試試: 我們的 JSON 轉 Excel 工具在瀏覽器裡就能做這件事。開啟「巢狀陣列匯出為工作表」,JSON 裡的每個物件陣列都會變成獨立工作表,父層識別欄已經填好——不上傳、免註冊。
需要特別處理的值
展平決定的是形狀。另外有幾類具體的值也需要留意,否則試算表軟體會把它們改掉。
null 變成空儲存格,而不是文字 null。這對彙總很關鍵:AVERAGE 會忽略空儲存格,卻會把 0 算進去,所以用空儲存格表示缺失,是「平均值正確」和「平均值錯誤」之間的差別。
布林值保持布林型別,顯示為 TRUE 和 FALSE。如果轉換器把它們寫成字串 "true"/"false",篩選和設定格式化的條件就都失效了。
數字保持數值型別,包括 91.5 這樣的小數。
16 位及以上的整數會被刻意寫成文字。超過約 15 到 16 位有效數字後,無論 JavaScript 的數字型別還是 Excel 的顯示都無法精確保存,9007199254740993123 會變成 9007199254740993000。寫成文字能保住每一位。更短的數字仍是數值型別,不影響正常計算。
最後這一條,在任何含訂單編號、雪花 ID、帳號的資料集上都值得檢查一遍。它的破壞是無聲的——不報錯、不警告,只是末尾幾位錯了。
動手前該知道的上限
| 上限 | 數值 |
|---|---|
| 單張工作表列數 | 1,048,576 |
| 單張工作表欄數 | 16,384 |
| 儲存格字元數 | 32,767 |
巢狀 JSON 真正會撞上的是儲存格字元上限。把一個大陣列作為原始 JSON 文字塞進一個儲存格,很容易超過 32,767 個字元,超出部分會被截斷。陣列比較大就匯出成工作表,別放儲存格裡。
瀏覽器端轉換還額外受可用記憶體限制,具體多少取決於裝置和瀏覽器,無法給出一個統一數字。在觸及硬上限之前,資料量大到一定程度就會出現實際的效能轉折——大約 5 萬列或 500 欄之後,匯出會開始明顯變慢。
判斷你的結構需要什麼
按順序過一遍:
- 記錄陣列在哪? 其餘都是中繼資訊。把根節點設成它。
- 有欄位是巢狀物件嗎? 它們會自動展平。展平後檢查是否出現重複標題。
- 有欄位是基本型別陣列嗎? 根據讀者是誰,在保留 JSON、串接、計數之間選一個。
- 有欄位是物件陣列嗎? 把它們匯出成獨立工作表。
- 有超過 15 位的編號嗎? 確認它們是以文字形式、每一位完整地過來了。
- 結果有多寬? 超過幾十欄通常意味著這份資料該拆開。
用 Python 實作
做可重複使用的資料管線,pandas 能覆蓋同樣的場景:
import json
import pandas as pd
with open("response.json", encoding="utf-8") as f:
payload = json.load(f)
# 物件展平成點號命名的欄
main = pd.json_normalize(payload["data"], sep=".")
# 物件陣列單獨成表,並帶上父層識別
orders = pd.json_normalize(
payload["data"],
record_path="orders",
meta=["id", "name"],
meta_prefix="parent_",
)
with pd.ExcelWriter("output.xlsx") as writer:
main.drop(columns=["orders"]).to_excel(writer, sheet_name="Customers", index=False)
orders.to_excel(writer, sheet_name="Orders", index=False)
與瀏覽器工具有兩點差異值得注意。json_normalize 會把完整點號路徑作為欄名,所以你拿到的是 address.city 而不是 City,如果這張表要給人看,用 df.rename(columns=...) 改一下。另外 pandas 會把長整數讀成浮點並四捨五入,編號欄要傳 dtype=str,或者在寫出前先轉換。
常見問題
JSON 的物件陣列怎麼匯出到 Excel?
給它一張獨立的工作表。物件陣列是一對多關係,在試算表裡的正確表現形式是第二張表:一個元素一列,再加上識別每個元素屬於哪一筆父記錄的欄。把它塞進單個儲存格當成 JSON 文字或串接文字,技術上可行,但產出的東西沒法篩選、也沒法做樞紐分析。
三種陣列匯出模式有什麼差別?
保留原始 JSON 文字什麼都不丟,適合後面還有程式會再解析這個儲存格的場景。串接得到「pro, beta」這類便於閱讀的文字,適合裝簡單標籤、給人看的表。計數把陣列換成元素個數,是唯一能直接參與加總與排序的模式。空陣列在三種模式下表現不同,分別是方括號文字、空儲存格和數字 0。
同一筆記錄裡的巢狀物件會變成什麼?
會展平成獨立的欄,這個過程是無損的。每個葉節點值保有自己的路徑,所以位於 address.city 的值會單獨成欄。預設標題取該路徑最後一段並做人性化處理,因此 address.city 產生的標題顯示為 City,而完整點號路徑會作為這一欄的來源路徑保留下來。
兩個巢狀欄位同名會怎樣?
你會得到兩個標題相同的欄。因為預設標題取路徑的最後一段,user.name 和 company.name 都會產生標題為 Name 的欄。它們的完整來源路徑仍然不同,並且在對應面板裡可見,所以分享檔案之前先改掉其中一個標題。重複標題會讓 VLOOKUP 和樞紐分析表以很難排查的方式出錯。
Excel 的列數、欄數和儲存格上限是多少?
單張工作表最多 1,048,576 列、16,384 欄,單個儲存格最多 32,767 個字元。陣列真正會撞上的是儲存格字元上限,因為把一個大陣列當成原始 JSON 文字塞進一個儲存格很容易超過它並被截斷。欄數也會隨巢狀深度快速成長,而表格在遠未觸及 16,384 這個上限時就已經難以閱讀了。
匯出後的陣列列怎麼和父記錄關聯起來?
靠寫進子工作表的識別欄。每一筆子列都會帶上父記錄的列號和它在陣列中的位置,再加上父記錄裡可辨識的識別欄位,例如 id、name、email 或訂單編號。匯出之後,正是這些欄讓你能用查閱函式或樞紐分析表把關係重新拼回來。
延伸閱讀
- JSON 轉 Excel 完整指南 —— 完整流程,以及什麼時候該選線上工具而不是 Power Query 或 Python。
- JSON 轉 CSV 指南 —— CSV 為什麼表達不了多個工作表,以及這對巢狀資料意味著什麼代價。
小結
物件展平成欄,不丟任何東西。真正需要決策的是陣列:基本型別可以在儲存格裡以 JSON、串接文字或計數的形式存在,但物件陣列屬於獨立工作表,並用識別欄連回父列。記得檢查重複標題、確認長編號以文字形式完整保留;如果結果寬達幾十欄,那是資料在告訴你,它不該只是一張表。
開啟 JSON 轉 Excel 工具 → —— 按陣列選擇匯出方式、巢狀陣列匯出為工作表,全部在瀏覽器裡完成。