TL;DR — 按用途 10 秒選工具
- 現在就要轉成表格:FormatArc JSON to CSV。瀏覽器內完成,不上傳資料,巢狀物件展開為點記法欄位。
- 一筆記錄裡面有物件陣列(訂單與明細):先決定「一行 = 一筆訂單」還是「一行 = 一筆明細」。參考下方「各種 JSON 形狀的處理方式」。
- 陣列每個元素要各占一行:用
mlr --ijson --ocsv cat(Miller) 或pandas.json_normalize(data, record_path=...)。 - ID 超過 15 位:不要用 JavaScript 系的轉換器。參考「超過 2^53 的整數」。
- 之後要在 Excel 打開:雙擊之前先讀「在試算表中不破壞資料地開啟」。
| 方法 | 安裝 | 巢狀物件 | 記錄內的陣列 | 瀏覽器完成(不上傳) |
|---|---|---|---|---|
| FormatArc | 不需要 | 點記法欄位 | 以 JSON 文字留在一個儲存格 | 是 |
| json-2-csv (npm) | npm i json-2-csv | 點記法欄位 | 以 JSON 文字留在一個儲存格 | 否(Node) |
Miller (mlr) | brew install miller | 點記法欄位 | 展開為 items.1.sku、items.2.sku… | 否(CLI) |
pandas json_normalize | pip install pandas | 點記法欄位 | Python repr 放一個儲存格,或用 record_path 展開成行 | 否(Python) |
jq | brew install jq | 自己寫對應 | 自己寫對應 | 否(CLI) |
上表內容全部是實測值。輸入資料、量測腳本、原始輸出都放在倉庫的 scripts/benchmarks/json-to-csv-guide/(2026-07-26 量測),下面引用的輸出都是那次執行的結果。
30 秒完成轉換
打開 JSON to CSV,貼上物件陣列,按執行。以下輸入:
[
{ "name": "Mika", "email": "mika@example.com", "role": "admin", "address": { "city": "Tokyo" } },
{ "name": "Noah", "email": "noah@example.com", "role": "viewer", "address": { "city": "Osaka" } }
]
產出的 CSV:
name,email,role,address.city
Mika,mika@example.com,admin,Tokyo
Noah,noah@example.com,viewer,Osaka


轉換完全在瀏覽器頁面上完成。JSON 資料不會傳送到任何伺服器。當你要貼的是產線 API 的回應、裡面有客戶資料時,這一點就很重要了。哪些被保護、哪些不被保護,見線上轉換工具是否安全。
輸入不是合法 JSON 的話,會跟 JSON 整理器一樣回傳帶行號的錯誤訊息。常見原因與修法見JSON 解析錯誤的原因與修法。
同一組 JSON 跑 4 款轉換器,結果各不一樣
JSON 是樹狀結構,CSV 是矩形表格。每款轉換器都有自己的規則來把樹壓扁成矩形,而這些規則互不相同。我們用 15 筆輸入跑過 4 款轉換器,逐一記錄輸出。
執行環境(取自 results.json):macOS arm64 上的 Node v26.3.1、FormatArc 用 lib/tooling.ts(PapaParse 5.5.2)、json-2-csv 5.5.11、Miller 6.19.0、pandas 3.0.5。四款都是預設設定,沒有任何選項。目的就是看「什麼都沒設定的時候會怎樣」。
四款一致的部分
巢狀物件會被展開成點記法。對 {"name":"Mika","address":{"city":"Tokyo","zip":"150-0001"}},四款都輸出:
name,address.city,address.zip
Mika,Tokyo,150-0001
三層巢狀(meta.created.by.name)也一樣。未被陣列包起來的單一物件,四款都變成一行 CSV。含逗號、雙引號、換行值的引號處理也一致:
who,quote,note
"Smith, John","She said ""hi""","line1
line2"
這跟 RFC 4180在新分頁中開啟 第 2 節的引號規則完全吻合:含換行、雙引號、逗號的欄位用雙引號包起來,內部的雙引號用兩個來轉義。補一點方便比對檔案的人:記錄分隔符 FormatArc 用 CRLF(PapaParse 的預設值,也是 RFC 4180 規定的),json-2-csv、Miller、pandas 用 LF。
陣列分三種處理
輸入:
[{"name":"Mika","tags":["admin","billing"]},{"name":"Noah","tags":["viewer"]}]
| 轉換器 | 輸出 |
|---|---|
| FormatArc | name,tags / Mika,"[""admin"",""billing""]" / Noah,"[""viewer""]" |
| json-2-csv | 與 FormatArc 相同 |
| Miller | name,tags.1,tags.2 / Mika,admin,billing / Noah,viewer, |
| pandas | name,tags / Mika,"['admin', 'billing']" / Noah,['viewer'] |
三種完全不同的產物。FormatArc 和 json-2-csv 把陣列當「合法 JSON 文字」放在一個儲存格裡,之後可以逐格再解析。Miller 把表格拉寬——很方便,但如果某筆記錄有 40 個 tag,欄位數就會爆到 40。pandas 輸出的是帶單引號的 Python repr,這不是合法 JSON,後段如果 JSON.parse 那個儲存格就會失敗。
物件陣列也一樣。{"order":"A-1","items":[{"sku":"X1","qty":2},{"sku":"X2","qty":1}]} 在 FormatArc 和 json-2-csv 裡變成一個儲存格的 JSON 文字,在 Miller 裡變成 items.1.sku, items.1.qty, items.2.sku, items.2.qty。
缺鍵:空儲存格、"undefined" 字串、還是報錯
實際 API 回應裡常有選填欄位。輸入:
[{"id":1,"name":"Mika"},{"id":2,"name":"Noah","nickname":"No"},{"id":3,"phone":"02-1234-5678"}]
FormatArc 和 pandas 輸出空儲存格:
id,name,nickname,phone
1,Mika,,
2,Noah,No,
3,,,02-1234-5678
json-2-csv 在缺失處寫 undefined 這個字串:
id,name,nickname,phone
1,Mika,undefined,undefined
2,Noah,No,undefined
3,undefined,undefined,02-1234-5678
Miller 直接拒絕整個檔案:mlr: CSV schema change: first keys "id,name"; current keys "id,phone"。對串流處理工具來說這是合理的設計,但意思是說多了一個選填欄位,昨天還跑得通的管線就停了。
FormatArc 會收集所有記錄的鍵之並集(按首次出現順序),在寫第一行之前就確定欄位數。行不會跑偏。
null:空儲存格還是 n-u-l-l 四個字
[{"id":1,"deleted_at":null},{"id":2,"deleted_at":"2026-07-01"}]
FormatArc 和 pandas 輸出空儲存格,json-2-csv 和 Miller 寫入 null 四個字。把這個 CSV 直接匯入資料庫的話,前者是真正的 NULL,後者是一個長得像 NULL 的字串。從 JSON 轉 CSV 之後「WHERE 條件對不上」的問題,大部分都出在這裡。
空物件會產生四種不同的表格
[{"id":1,"meta":{}},{"id":2","meta":{"source":"api"}}]:
| 轉換器 | 輸出 |
|---|---|
| FormatArc | id,meta,meta.source — 因為空的 {},所有行都留一個空的 meta 欄 |
| json-2-csv | 表頭相同,但把 {} 和 {"source":"api"} 原樣寫進儲存格,值重疊 |
| Miller | 報錯:CSV schema change: first keys "id,meta"; current keys "id,meta.source" |
| pandas | meta 欄整個消失:id,meta.source |
FormatArc 的行為誠實但不美觀。多出一個空欄的話,就是 payload 某個地方藏著空物件 {}。
鍵名含逗點時,三款工具會覆寫欄位
這是會丟資料的情況,四款裡面三款會丟。輸入:
[{"a.b":1,"a":{"b":2}}]
這筆記錄有兩個不同的值:字面上叫 a.b 的鍵的值 1,以及巢狀 a → b 路徑的值 2。FormatArc、Miller、pandas 都輸出:
a.b
2
1 不見了。只有 json-2-csv 分得出來,對字面鍵做轉義:
a\.b,a.b
1,2
如果 JSON 用的是含逗點的鍵名(分析事件名 page.view.count、MongoDB 文件、Prometheus 標籤),轉之前先確認有沒有衝突,或用 json-2-csv 來轉。
超過 2^53 的整數在 JavaScript 系會被四捨五入
[{"id":9007199254740993,"order_no":12345678901234567890}]
| 轉換器 | 輸出 |
|---|---|
| FormatArc | 9007199254740992,12345678901234567000 |
| json-2-csv | 9007199254740992,12345678901234567000 |
| Miller | 9007199254740993,12345678901234567890 |
| pandas | 9007199254740993,12345678901234567890 |
這不是 CSV 的問題,也不是那兩款 JavaScript 工具的 bug。JSON.parse 把所有數字轉成雙精確度浮點數,所以 2^53 以上的整數無法精確表示。值在傳到 CSV 寫入器之前就已經變了。Snowflake ID、X(Twitter)ID、某些付款序號、64-bit 資料庫主鍵都落在這個範圍。要處理長 ID 的話,在 JSON 端用字串包起來,或用保留位數的 Miller、pandas、jq。
爛輸入:明確報錯還是靜默空檔案
| 輸入 | FormatArc | json-2-csv | Miller | pandas |
|---|---|---|---|---|
["a","b","c"] | 錯誤:「陣列的每個元素必須是物件」 | 三行空白、無錯誤 | 例外 | 例外 |
[] | 錯誤:「JSON 陣列為空」 | 一行空白、無錯誤 | 空輸出、無錯誤 | 空輸出、無錯誤 |
三種結果裡最糟的是「回傳碼 0 但輸出空檔案」。cron 批次工作會毫不猶豫地用空檔覆蓋昨天的正常檔案。
FormatArc 的轉換規則(實作原樣)
以下是摘要不是重點整理,是 lib/tooling.ts 的 convertJsonToCsv 函式的實際規則,跟上面的實測結果完全一致。
- 最上層必須是物件陣列或單一物件。單一物件變成一行 CSV。原始型別陣列、空陣列、裸字串或數字都會被拒絕並附說明。
- 巢狀物件遞迴展開為點記法欄位(
address.city、meta.created.by.name)。沒有深度限制。 - 陣列不展開。用
JSON.stringify序列化後放在一個儲存格,所以["admin","billing"]保持可解析狀態。 - 欄位是所有記錄鍵的並集,按首次出現順序排列。只在最後一筆記錄出現的鍵也會有欄位,之前的行填空儲存格。行不會跑偏。
null和undefined變空儲存格。空物件{}也變空儲存格,但保留自己的欄位。- 引號處理跟隨 PapaParse 的
unparse:含逗號、引號、換行的欄位加引號,內部引號翻倍,記錄分隔為 CRLF。 - 不合法的 JSON 會帶行號報錯(跟 JSON 整理器同一條錯誤路徑)。
資料不會離開瀏覽器分頁。沒有上傳步驟,伺服器上沒有暫存檔,就是在已載入的頁面裡執行一個函式。
各種 JSON 形狀:怎麼轉
扁平物件陣列
沒有要判斷的。貼上、執行,完事。
含巢狀物件的記錄
點記法是各工具都採用的預設答案,人眼看也追得到來源(address.city 從哪來一目了然)。事先確認兩件事就好:已有的鍵名裡有沒有逗點(前面提到的衝突),以及讀取 CSV 的程式能不能處理表頭裡的逗點。有些 SQL loader 需要表頭加引號,Google 試算表則沒問題。
含標量陣列的記錄
三個選項:
- 保持 JSON 文字放在一個儲存格(FormatArc 預設)。適合 CSV 當中間檔案、後段腳本要再讀的情況。
- 用 Miller 展開成
tags.1、tags.2欄位。適合元素數量少且固定(座標對、RGB 三個值)的情況。 - 給人看的欄位就在轉換前用分隔符號串起來:
jq '.[] |= (.tags |= join(";"))' data.json跑完再轉。分隔用;而非逗號,儲存格就不用加引號了。
含物件陣列的記錄(最棘手的形狀)
在碰轉換器之前,先定義「CSV 的一行代表什麼」。
- 一行 = 父層(一筆訂單):陣列保持 JSON 文字放在一個儲存格。FormatArc 預設就是這樣。明細不會壞,之後可以用腳本解析那個儲存格。
- 一行 = 子層(一筆明細):這是另一張表,不是同一張表的另一種格式。用
pandas.json_normalize(data, record_path="items", meta=["order"])可以把父層欄位重複展開到每個子行。 - 拆成兩個檔案:父層欄位轉成
orders.csv,子層用jq '[.[] | .order as $o | .items[] | {order:$o} + .]' data.json抽出再轉成items.csv。這是正規化的答案,CSV 要進資料庫的話選這個。
要避免的是:用 Miller 預設行為把可變長陣列拉成 items.1.sku, items.2.sku, …。欄位數由「剛好子元素最多的那筆記錄」決定,資料一變 schema 就變。
鍵不齊整的記錄
FormatArc 和 pandas 不用設定就能處理。Miller 加 unsparsify 就不會中斷、會補上缺失值:
mlr --ijson --ocsv unsparsify data.json
或者用 jq '[.[] | {id, name, nickname, phone}]' 先讓所有記錄都帶齊所有鍵。
在 Excel 與 Google 試算表中不破壞資料地開啟
破壞資料的通常不是轉換器,是試算表軟體本身。
- 編碼。CSV 格式本身沒有編碼欄位。RFC 4180在新分頁中開啟 也只提到 US-ASCII 以外的字元集用 MIME
charset參數表示,本機檔案沒有這個參數。FormatArc 下載的是沒有 BOM 的 UTF-8。Windows 的 Excel 沒有 BOM 時可能把非 ASCII 文字當成 Windows-1252 讀,字就亂了。不要雙擊檔案,用 Excel 的「資料」分頁 >「從文字/CSV」,把編碼選成「65001: Unicode (UTF-8)」再匯入。Google 試算表會自動正確判讀 UTF-8。 - 15 位以上的數字。Excel 的官方規格寫明數字精度為 15 位在新分頁中開啟,超過 15 位的數字會被替換為 0。加上前面的 2^53 四捨五入,長 ID 可能兩次受傷。該欄位匯入時要指定為文字型別。
- 前導零。
007或電話02-1234-5678,如果不把欄位當文字匯入,就會變成數字7或日期。 - 看起來像日期的字串。
2026-07-01當然會,1-2這種版本號也會被試算表自動轉成日期格式。匯入時指定欄位型別為文字最安全。 - 以
=、+、-、@開頭的儲存格。這次比對的四款工具都沒有轉義(=1+1原樣輸出)。要不要當成公式來算是試算表決定的,這就是 CSV Injection(公式注入攻擊)在新分頁中開啟 的基本型。如果 CSV 含使用者輸入且會有別人打開,在前頭加單引號(')或把該欄匯入為文字。
分隔符號:逗號、分號、還是 Tab
FormatArc 永遠用逗號(,)。輸出經過 PapaParse 的 unparse 函式,沒有分隔符號設定選項(lib/tooling.ts 的 convertJsonToCsv)。RFC 4180在新分頁中開啟 也把分隔符定義為逗號。
這個預設值會一直撐到有人雙擊檔案為止。Microsoft 的文件說 Excel 把活頁簿存成 .csv 時「預設清單分隔符號是逗號,可以用 Windows 區域及語言設定改成其他字元」(匯入或匯出文字檔案在新分頁中開啟)。同一頁還寫到:小數點符號用逗號的地區,「Excel 會改用分號作為清單分隔符號」。歐洲大陸和大部分中南美洲都是這個設定。在那種環境下雙擊逗號分隔的檔案,所有內容可能全擠在 A 欄。
兩個有效解法,一個無效:
- 不要雙擊,用匯入功能。「資料」>「從文字/CSV」,分隔符號選逗號、編碼選 UTF-8。跟上面編碼那條同一條路徑,不會多步驟。
- 如果一定要讓對方雙擊打開,用 Miller 產分號分隔的檔案。上面比較用的 Miller 6.19.0 實測,
mlr --ijson --ocsv --ofs ';' cat data.json會把表頭a,b變成a;b。 - 不要把逗號批量替換成分號。原本含逗號的欄位已經被引號包住了,替換會把那個欄位從中間切開,資料就壞了。
瀏覽器不適合的時候
curl 抓到的 API 回應貼上去確認,瀏覽器轉換器最快也最安全。但 2GB 的匯出檔、或夜間批次工作的一個步驟,瀏覽器就不適合了。
# Miller: 物件展開為點記法、陣列展開為索引欄位、串流處理
mlr --ijson --ocsv cat data.json > out.csv
# jq: 自己寫對應,沒有猜測空間
jq -r '(.[0] | keys_unsorted), (.[] | [.[]]) | @csv' data.json > out.csv
# pandas: 點記法展開,加上 record_path 可逐元素展開為行
python3 -c "import pandas,json; print(pandas.json_normalize(json.load(open('data.json'))).to_csv(index=False))" > out.csv
這三款不能互相替換。json_normalize 把字典展開成點記法欄位,但要逐元素出行的話必須指定 record_path。jq 的 @csv 需要輸入已經是標量陣列,巢狀物件傳進去不會展開、會直接報錯。Miller 預設會展開物件和陣列(陣列索引從 1 開始),但記錄 schema 不一致時會中斷(前面實測過)。詳細見 Miller flatten 文件在新分頁中開啟、jq 手冊在新分頁中開啟、pandas.json_normalize 參考在新分頁中開啟。
從 CSV 轉回 JSON 回不到原狀
往返回不去。拿第一個例子的 CSV 再用 CSV to JSON 工具轉回來:
[
{
"name": "Mika",
"email": "mika@example.com",
"role": "admin",
"address.city": "Tokyo"
}
]
address.city 回來的是一個含逗點的扁平鍵名,不是巢狀的 address 物件。沒有工具能自動把點記法重新巢狀化,因為 address.city 本身就是一個完全合法的 JSON 鍵名,轉換器無法判斷你原本的意思是哪個。跟前面鍵名衝突是同一個原因。
所以 JSON 轉 CSV 就當它是「試算表分析用的單向匯出」,正本永遠是 JSON。需要巢狀形狀回來的話,用 jq 'map(reduce (to_entries[]) as {$key,$value} ({}; setpath($key | split("."); $value)))' 這樣明確地重新巢狀化,然後檢查結果。反方向(CSV 轉 JSON)的比較見CSV 轉 JSON 比較指南。
常見問題
為什麼 CSV 只有一欄,裡面是整塊 JSON?
最上層是 {"data":[...]} 這種「只含一個陣列的單一物件」。先把陣列抽出來(只貼 data 的值,或 jq '.data' response.json 跑一下),再轉。FormatArc 會把外層物件展開成 data 欄,不會把裡面的陣列自動當成行。
巢狀陣列的每個元素能各占一行嗎?
瀏覽器轉換器做不到,這是故意的。行數會隨資料變,父層欄位也會被無聲地複製。用 pandas.json_normalize(data, record_path="items", meta=[...]) 或先用 jq 整形,如前所述。
欄位內容是 [{"sku":"X1"...}] 這種形式
物件陣列被保持為 JSON 文字放在一個儲存格。這是預期行為,資料沒有丟,之後可以還原。想改的話有三種方式,見「含物件陣列的記錄」。
數字會變嗎?
只有在 JavaScript 自身規定的範圍內才會。超過 2^53 的整數在解析時會被四捨五入(前面實測過)。字串完全不動。ID 超過 15 位的話,在 JSON 端用字串包起來,或用非 JavaScript 的工具。
資料會上傳到哪裡嗎?
不會。轉換在頁面內執行,JSON 不會離開分頁,連承載它的請求都不存在。
JSON 不合法會怎樣?
不會輸出壞掉的 CSV,而是回傳帶行號和內容的錯誤訊息。常見原因(結尾逗號、單引號等)的處理方式可參考 JSON 整理器的說明。整形後維持可讀性的技巧見JSON 排版實務指南。
正本該用哪個格式保存?
用能保存結構的那個。CSV 是沒有型別的矩形表格,巢狀、陣列、null 與空字串 "" 的區別全部消失。以 JSON 為正本,CSV 當其中一個檢視(view)。CSV 的基本結構與限制見CSV 是什麼?。