如何使用 jq 将任意嵌套 JSON 转换为 CSV – 以便可以将其转换回来?

opy*_*pyh 7 csv json google-sheets mongodb jq

如何使用jq将任意JSON 对象数组转换为 CSV,而该数组中的对象是嵌套的

\n

StackOverflow 有大量的问题/答案,其中引用了特定的输入或输出字段,但我希望有一个通用的解决方案

\n
    \n
  1. 包括标题行,
  2. \n
  3. 适用于任何 JSON 输入,包括嵌套数组 + 对象,
  4. \n
  5. 允许记录缺少其他记录中存在的键值
  6. \n
  7. 不硬编码任何字段名称,
  8. \n
  9. 如果需要,允许将 CSV 转换回嵌套 JSON 结构,并且
  10. \n
  11. 使用关键路径作为标头名称(请参阅以下描述)。
  12. \n
\n

点符号

\n

许多使用 JSON 的产品(如CouchDBMongoDB、 \xe2\x80\xa6 )和库(如Lodash、 \xe2\x80\xa6 )使用语法变体,允许通过将关键片段与字符,通常是一个点(\xe2\x80\x98dot 表示法\xe2\x80\x99)。

\n

像这样的关键路径的一个示例是"a.b.0.c"引用此 JSON 片段中的深度嵌套属性:

\n
{\n  "a": {\n    "b": [\n      {\n        "c": 123,\n      }\n    ]\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

警告:在大多数情况下,使用此方法是一种实用的解决方案,但这意味着必须在属性名称中禁止点字符,或者必须发明更复杂的(绝对从未使用过的属性名称)来转义属性名称中的点/访问嵌套字段。MongoDB在 v5.0 之前就禁止使用"."in 文档,一些库有字段访问的解决方法(Lodash 示例)。

\n

尽管如此,为了简单起见,解决方案应在 CSV 输出\xe2\x80\x99s 标头中使用所描述的点语法来表示嵌套属性。如果有一个解决方案变体可以解决这个问题,例如使用JSONPath ,那就太好了。

\n

作为输入的示例 JSON 数组

\n
[\n    {\n        "a": {\n            "b": [\n                {\n                    "c": 123\n                }\n            ]\n        }\n    },\n    {\n        "a": {\n            "b": [\n                {\n                    "c": "foo \\" bar",\n                    "d": "qux"\n                }\n            ]\n        }\n    },\n    {\n        "a": {\n            "b": [\n                {\n                    "d": 456\n                }\n            ]\n        }\n    }\n]\n
Run Code Online (Sandbox Code Playgroud)\n

CSV 输出示例

\n

输出应该有一个包含所有字段的标头(即使第一个数组中的对象没有为所有现有键路径定义值)。

\n

为了使输出可供人类直观地编辑,每一行应代表输入数组中的一个对象。

\n

预期输出应如下所示:

\n
"a.b.0.c","a.b.0.d"\n123,\n"foo "" bar","qux"\n,456\n
Run Code Online (Sandbox Code Playgroud)\n

命令行

\n

这就是我需要的:

\n
cat example.json | jq <MISSING CODE HERE>\n
Run Code Online (Sandbox Code Playgroud)\n

opy*_*pyh 7

解决方案1,使用点表示法

\n

以下是jq将嵌套 JSON 对象数组转换为 CSV 的调用:

\n
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) | .[]\n
Run Code Online (Sandbox Code Playgroud)\n

尝试此解决方案的最快方法是使用 JQPlay

\n

CSV 输出将有一个标题行。它将包含输入对象中任何位置存在的所有属性,包括点表示法中的嵌套属性。每个输入数组元素将表示为单行,缺少的属性将表示为空 CSV 字段。

\n

bash在或类似的 shell中使用解决方案 1

\n
    \n
  • 创建 JSON 输入文件\xe2\x80\xa6

    \n
    echo \'[{"a": {"b": [{"c": 123}]}},{"a": {"b": [{"c": "foo \\" bar","d": "qux"}]}},{"a": {"b": [{"d": 456}]}}]\' > example.json\n
    Run Code Online (Sandbox Code Playgroud)\n
  • \n
  • 然后使用此 jq 命令在标准输出上输出 CSV:

    \n
    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) | .[]\'\n
    Run Code Online (Sandbox Code Playgroud)\n
  • \n
  • \xe2\x80\xa6 或将输出写入example.csv

    \n
    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\n
    Run Code Online (Sandbox Code Playgroud)\n
  • \n
\n

将解决方案 1 中的数据转换回 JSON

\n

这是一个Node.js 示例,您可以在 RunKit 上尝试。它将使用解决方案 1 中的方法生成的 CSV 转换回嵌套 JSON 对象的数组。

\n

解决方案1的说明

\n

这是过滤器的更长、带注释的版本jq

\n
# 1) Find all unique leaf property names of all objects in the input array. Each nested property name is an array with the components of its key path, for example ["a", 0, "b"].\n(. | map(leaf_paths) | unique) as $cols |\n\n# 2) Use the found key paths to determine all (nested) property values in the given input records.\nmap (. as $row | ($cols | map(. as $col | $row | getpath($col)))) as $rows |\n\n  # 3) Create the raw output array of rows. Each row is represented as an array of values, one element per existing column.\n  (\n\n    # 3.1) This represents the header row. Key paths are generated here.\n    [($cols | map(. | map(tostring) | join(".")))]\n\n    + # 3.2) concatenate the header row with all other rows\n    $rows\n\n  )\n\n  # 4) Convert each row to a escaped CSV string.\n  | map(@csv)\n\n  # 5) output each array element directly. Without this, the result would be a JSON array of CSV strings.\n  | .[]\n
Run Code Online (Sandbox Code Playgroud)\n

解决方案 2:对于属性名称中包含点的输入

\n

如果您确实需要支持属性名称中的点字符,您可以为键路径语法使用不同的分隔符字符串(将点替换为"."其他内容),或者将map(tostring) | join(".")部分替换为tostring- 这会生成一个 JSON 字符串数组,您可以将其替换为字符串。可以用作关键路径 - 不需要点符号。这是具有此解决方案变体的JQPlay

\n

完整jq命令:

\n
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) | .[]\n
Run Code Online (Sandbox Code Playgroud)\n

该变体的输出 CSV 看起来像这样 \xe2\x80\x93 it\xe2\x80\x99s 可读性较差,并且对于您希望人们直观地理解 CSV\xe2\x80\x99s 标头的情况没有用处:

\n
"[""a"",""b"",0,""c""]","[""a"",""b"",0,""d""]"\n123,\n"foo "" bar","qux"\n,456\n
Run Code Online (Sandbox Code Playgroud)\n

请参阅下文,了解如何将此格式转换回您的编程语言的表示形式。

\n

奖励:将生成的 CSV 转换回 JSON

\n

如果输入的嵌套属性不包含 no ".",则将 CSV 转换回 JSON 很简单,例如使用支持点表示法的库或使用JSONPath

\n
    \n
  • JavaScript:使用Lodash 的 _.set()
  • \n
  • 其他语言:找到一个实现JSONPath 的包/库,并使用像$.a.b.0.c或 这样的选择器$[\'a\'][\'b\'][0][\'c\']来设置每个记录的每个嵌套属性。
  • \n
\n

解决方案 2(使用 JSON 数组作为标头)允许您将标头解释为 JSON 数组字符串。然后,您可以从每个标头生成 JSON 路径,并重新创建所有记录/对象:

\n

"[""a"",""b"",0,""c""]"(CSV)

\n

\xe2\x86\x92 ["a","b",0,"c"](取消转义并解析为 JSON 后的键路径组件数组)

\n

\xe2\x86\x92 $.["a"]["b"][0]["c"](JSONPath)

\n

\xe2\x86\x92 { a: { b: [{c: \xe2\x80\xa6 }] } }(嵌套重新生成的对象)

\n

我编写了一个示例 Node.js 脚本来将这样的 CSV 转换回 JSON。您可以在 RunKit 中尝试解决方案 2

\n