如何使用jq將一個任意物件的 JSON 陣列轉換為 CSV,而該陣列中的物件是嵌套的?
StackOverflow 有大量的問題/答案,其中參考了特定的輸入或輸出欄位,但我希望有一個通用的解決方案,
- 包括一個頭檔案。
- 包括一個標題行,
- 適用于任何JSON輸入,包括嵌套陣列 物件, 。
- 允許在其他記錄中存在鍵值缺失的記錄 。
- 不對任何欄位名進行硬編碼, 。
- 如果需要的話,允許將CSV轉換回嵌套的JSON結構,并且
- 使用關鍵的路徑作為關鍵的資料。
- 使用關鍵路徑作為標題名稱(見以下描述)。
點狀符號
許多使用 JSON 的產品(如 CouchDB, MongoDB, ... )和庫(如 Lodash, ...)使用不同的語法,允許通過用一個字符(通常是一個點)連接關鍵片段來訪問嵌套的屬性值/子欄位('點符號法')。像這樣的關鍵路徑的例子是"a.b.0.c"來參考這個JSON片段中的深度嵌套屬性:
{}。
"a"/span>: {
"b": [
{>
"c": 123,
}
]
}
}
Caveat。對于大多數情況來說,使用這種方法是一種務實的解決方案,但這意味著要么在屬性名稱中禁止使用點字符,要么必須發明一種更復雜的(而且肯定不會使用的屬性名稱),用于轉義屬性名稱中的點/訪問嵌套欄位。MongoDB只是禁止在檔案中使用".",直到v5.0,一些庫對欄位訪問有變通方法(Lodash例子)。
盡管如此,為了簡單起見,解決方案應該在CSV輸出的標題中使用所描述的點語法來處理嵌套屬性。如果有一個解決方案的變體可以解決這個問題,例如使用JSONPath,則可獲得獎勵。
作為輸入的JSON陣列示例
[/span>
{>
"a"/span>: {
"b": [
{>
"c"/span>: 123
}
]
}
},
{>
"a": {
"b": [
{>
"c": "foo " bar",
"d": "qux"
}
]
z
},
{>
"a": {
"b": [
{>
"d": 456
}
]
}
}
]
示例CSV輸出
輸出應該有一個包含所有欄位的標題(即使第一個陣列的物件沒有為所有現有的關鍵路徑定義值)。為了使輸出能夠直觀地被人類編輯,每一行應該代表輸入陣列中的一個物件。
預期的輸出應該是這樣的:
。"a.b.0.c"/span>,"a.b.0.d"/span>。
123。
"foo""bar","qux".
,456。
命令列
這就是我所需要的:這就是我所需要的。
cat example. json | jq <MISSING CODE Here>
uj5u.com熱心網友回復:
下面的tocsv和fromcsv函式為所述問題提供了一個解決方案,除了關于頭檔案的要求(6)的一個復雜情況。 從本質上講,通過添加一個矩陣轉置步驟,可以使用這里給出的函式來滿足這一要求。
無論是否添加轉置步驟,這里采取的方法的優點是對JSON鍵或值沒有任何限制。特別是,它們可以
在這個例子中,有一個JSON鍵或值沒有限制,特別是它們可以包含句號(點)、換行符和/或NUL字符。
在這個例子中,我們給出了一個物件陣列,但事實上,任何有效的JSON檔案流都可以作為tocsv的輸入;由于jq的魔力,原始流將被fromcsv重新創建(從逐個物體平等的角度來看)。
當然,由于沒有CSV標準,由
tocsv函式產生的CSV可能不被所有的CSV處理器所理解。 特別是
特別是,請注意這里定義的tocsv函式映射了
嵌入JSON字串或鍵名中的換行符到兩個字符的
字串"
" (即一個字面的反斜杠,后面是字母 "n")。
反向操作執行反向翻譯以滿足
"
(使用tail只是為了簡化表述;修改解決方案以滿足 "往返 "的要求將是非常簡單的。
使用tail只是為了簡化演示;修改該解決方案以使其成為一個唯一的jq解決方案是微不足道的。
CSV的生成是基于這樣的假設,即任何值都可以被包含在一個欄位中,只要()。 只要(a)該欄位有引號,并且(b)該欄位內的雙引號是 欄位內的雙引號是雙倍的。
任何支持 "round-trips "的通用解決方案都必然是
某種程度上是復雜的。 這里介紹的解決方案之所以比人們想象的要復雜,主要原因是
比人們想象的更復雜的主要原因是加入了第三列
增加了第三列,部分原因是為了便于區分整數和
整數和整數值的字串,但主要是因為它使人們容易區分
區分大小為1的陣列和大小為2的陣列,由jq的
--stream選項產生的陣列。 毋庸置疑,還有其他方法
這些問題可以得到解決;對jq的呼叫數量也可以
該解決方案以一個測驗腳本的形式呈現,該腳本檢查了一個告訴測驗案例的往返要求:
該解決方案以一個測驗腳本的形式呈現,該腳本檢查了一個告訴測驗案例的往返要求。
#!/bin/bash
function json {
cat<<EOF。
[
{
"a"/span>: 1,
"b": [
1,
2,
"1".
],
"c"。"d",ef",
"embed "ed": "quote",
"null": null。
"string": "null",
"控制字符"。"au0000c",
"換行"。"a
b"/span>
},
{
"x": 1: "x".
}
]
EOF ]
}
function tocsv {
jq -ncr -流 '
(["path", "value", "stringp"],
(inputs | . [.[1]|type="string"]))
| map( tostring|gsub("";"""") | gsub("
"; "
"))
| ""(.[0])","(.[1])",(.[2])"
'
}
function fromcsv {
tail -n 2 | #先重復反斜線,再重復雙引號
jq -rR '"[(gsub("\";"") | gsub("""";"") ]" |
jq -c '. [2] as $s
| .[0] |= fromjson
| .[1] |= 如果$s則.否則fromjson結束
|如果$s == null,則[.[0]] 否則 .[:-1] 結束
# 處理新行
| map(if type == "string" then gsub("\n";"
") else . end)' | |
jq -n 'fromstream(inputs)'。
}
# 檢查 roundtrip。
json | tocsv | fromcsv | jq -s '. [0] == . [1]' - <(json)
下面是由json | tocsv產生的CSV,只是SO似乎不允許字面的NUL,所以我用代替了它:
"path"/span>,"value"/span>,stringp
"[0,""a""]","1",false。
"[0,""b"",0]","1",false。
"[0,""b"",1]","2",false。
"[0,""b"",2]","1",true。
"[0,""b"",2]","假",空。
"[0,""c""] "。 "d"",ef",true。
"[0,""embed""ed"]","quote",true
"[0,"null""]" 。 "null",false
"[0,"string"]" 。 "null",true
"[0,"控制字符"]", "ac",true
"[0,"newline"]","a
b",true
"[0,"newline"]" 。 "false",null
"[1,"x"]","1",false
"[1,"x"]。 "false",null
"[1]","false",null
uj5u.com熱心網友回復:
解決方案1,使用點符號
。下面是將你的嵌套JSON物件陣列轉換為CSV的jq呼叫:
jq -r '(. | map(leaf_paths) | unique) as $cols | map(. as $row | ($cols | map(. as $col | $row | getpath($col)))) as $rows | ([($cols | map(. | map(tostring) | join(". "))] $rows) | map(@csv) | . []
嘗試這個解決方案的最快方法是使用JQPlay。
。CSV輸出將有一個標題行。它將包含所有存在于輸入物件中任何地方的屬性,包括嵌套的屬性,以點符號表示。每個輸入陣列元素將被表示為一個單行,缺少的屬性將被表示為空的 CSV 欄位。
在bash或類似的shell中使用解決方案1
。
創建JSON輸入檔案...
echo ' [{"a": {"b": [{"c": 123}]}},{"a": {"b": [{"c": "foo " bar", "d": "qux"}]},{"a": {"b": [{"d": 456}]}' > example.json然后使用這個jq命令在標準輸出上輸出CSV:
cat example.json | jq -r '( . | map(leaf_paths) | unique) as $cols | map(. as $row | ($cols | map(. as $col | $row | getpath($col)))) as $rows | ([($cols | map(. | map(tostring) | join(". "))] $rows) | map(@csv) | . []...或者把輸出寫到
example.csv:cat example.json | jq -r '( . | map(leaf_paths) | unique) as $cols | map(. as $row | ($cols | map(. as $col | $row | getpath($col)))) as $rows | ([($cols | map(. | map(tostring) | join(". "))] $rows) | map(@csv) | . [] > example.csv
創建一個新的CSV欄位。
將方案1中的資料轉換回JSON
這里有一個Node.js 示例,您可以在 RunKit 上試用。它將使用解決方案 1 中的方法生成的 CSV 轉換為嵌套 JSON 物件的陣列。解決方案 1 的解釋
。下面是一個較長的注釋版本的jq過濾器。
# 1) 找到輸入陣列中所有物件的所有唯一葉子屬性名。每個嵌套的屬性名都是一個陣列,包含其關鍵路徑的組成部分,例如["a", 0, "b"].
(. | map(leaf_paths) | unique) as $cols |
# 2) 使用找到的關鍵路徑來確定給定輸入記錄中的所有(嵌套)屬性值。
map(. as $row | ($cols | map(. as $col | $row | getpath($col)))) as $rows !
# 3) 創建原始輸出的行陣列。每一行都被表示為一個值陣列,每一個現有的列有一個元素。
(
# 3.1) 這代表標題行。關鍵路徑在此生成。
[($cols | map(. | map(tostring) | join("."/span>))]
# 3.2) 將標題行與所有其他行連接起來。
$rows
)
# 4) 將每一行轉換為轉義的CSV字串。
| map(@csv)
# 5) 直接輸出每個陣列元素。如果不這樣做,結果將是一個CSV字串的JSON陣列.。
| .[]
解決方案2:對于確實在屬性名中有圓點的輸入
。如果你確實需要支持屬性名中的點字符,你可以為關鍵路徑語法使用不同的分隔符(用其他東西替換"."中的點),或者用tostring替換map(tostring) | join(".")部分 - 這產生一個字串的JSON陣列,你可以用作關鍵路徑 - 無需使用點符號。這里有一個JQPlay的解決方案變數。
完整的jq命令:
jq -r (. | map(leaf_paths) | unique) as $cols | map(. as $row | ($cols | map(. as $col | $row | getpath($col)))) as $rows | [($cols | map(. | tostring)] $rows) | map(@csv) | .[]
那么變體的輸出CSV就會是這樣的--它的可讀性較差,對于你希望人類能直觀地理解CSV的標題的情況來說,并沒有什么用處:
"[""a"" 。 ""b"",0,""c""】"。 "[""a"" 。 ""b"",0,""d""] "
123。
"foo"" bar","qux".
,456。
請看下文,了解如何將這種格式轉換回你的編程語言中的表述。
獎勵:將生成的 CSV 轉換回 JSON
。如果輸入的嵌套屬性不包含".",那么將CSV轉換回JSON很簡單,例如使用支持點符號的庫,或者使用JSONPath。
- JavaScript。使用Lodash的_.set() 。
- 其他語言。找到一個實作JSONPath的包/庫,并使用
$.a.b.0.c或$['a']['b'][0]['c'>等選擇器來設定每個記錄的每個嵌套屬性。
解決方案2(用JSON陣列作為頭檔案)允許你將頭檔案解釋為JSON陣列字串。然后你可以從每個頭資訊中生成一個JSON路徑,并重新創建所有的記錄/物件:
→ → → 我寫了一個Node.js腳本的例子,將這樣的CSV轉換回JSON。你可以在RunKit中嘗試解決方案2。
標籤:"[""""""""""""""""""。
"[""a"",""b"",0,""c""]" (CSV)["a", "b",0, "c"] (取消escaping并決議為JSON后的關鍵路徑組件陣列)$.["a"]["b"][0]["c"] (JSONPath){ a: { b: [{c: ... }] } } (Nested regenerated object)
