如何使用 JQ 将对象列表展开为非规范化对象?

How to use JQ to unroll a list of objects into denormalized objects?

我有以下 JSON 行示例:

{"toplevel_key": "top value 1", "list": [{"key1": "value 1", "key2": "value 2"},{"key1": "value 3", "key2": "value 4"}]}
{"toplevel_key": "top value 2", "list": [{"key1": "value 5", "key2": "value 6"}]}

我想使用 JQ 转换它,将列表展开为固定数量的 "columns",最后得到一个平面 JSON 对象的列表,格式如下:

{
    "top-level-key": "top value 1",
    "list_0_key1": "value 1",
    "list_0_key2": "value 2",
    "list_1_key1": "value 3",
    "list_1_key2": "value 4",
}
{
    "top-level-key": "top value 2",
    "list_0_key1": "value 4",
    "list_0_key2": "value 5",
    "list_1_key1": "",
    "list_1_key2": "",
}

注意:实际上我希望每行一个,为了便于阅读而在此处设置格式。

我能够获得我想要的输出的唯一方法是写出我的 JQ 表达式中的所有列:

$ cat example.jsonl | jq -c '{toplevel_key, list_0_key1: .list[0].key1, list_0_key2: .list[0].key2, list_1_key1: .list[1].key1, list_1_key2: .list[1].key2}'

这得到了我想要的结果,但我必须手动编写所有固定的 "columns"(在生产中它会比这多得多)。

我知道我可以使用脚本生成该 JQ 代码,但我 对这样的解决方案感兴趣 -- 它不会解决我的问题,因为这是针对仅接受 JQ 的应用程序。

有没有办法在纯 JQ 中做到这一点?

这是我到目前为止能够得到的:

$ cat example.jsonl | jq -c '(.list | to_entries | map({("list_" + (.key | tostring)): .value})) | add'
{"list_0":{"key1":"value 1","key2":"value 2"},"list_1":{"key1":"value 3","key2":"value 4"}}
{"list_0":{"key1":"value 5","key2":"value 6"}}

以下是构建它的方法:

{ "top-level-key": .toplevel_key } + ([
    range(.list|length) as $i
        | .list[$i]
        | to_entries[]
        | .key = "list_\($i)_\(.key)"
    ] | from_entries)

这将为每个对应的列表条目映射。

{
  "top-level-key": "top value 1",
  "list_0_key1": "value 1",
  "list_0_key2": "value 2",
  "list_1_key1": "value 3",
  "list_1_key2": "value 4"
}
{
  "top-level-key": "top value 2",
  "list_0_key1": "value 5",
  "list_0_key2": "value 6"
}

如果您需要将其填充,则必须仔细分析结果以确定实际需要多少并添加填充。但我暂时保留它。

只要知道具体按键的名称,Jeff 的回答就很好。这是一个没有对特定键名进行硬编码的答案,也就是说,它适用于任何结构和嵌套级别的对象:

[leaf_paths as $path | {
    "key": $path | map(tostring) | join("_"),
    "value": getpath($path)
}] | from_entries

解释:paths 是一个内置函数,它递归地输出一个数组,表示您传递给它的输入的每个元素的位置:所述数组中的元素是有序的键名称和索引导致请求的数组元素。 leaf_paths 是它的一个版本,它只获取到 "leaf" 元素的路径,即不包含其他元素的元素。

澄清一下,给定输入 [[1, 2]]paths 将输出 [0], [0, 0], [0, 1](即 [1, 2]1 和 [= 的路径19=], 分别) 而 leaf_paths 只会输出 [0, 0], [0, 1].

这是最难的部分。之后,我们将每个路径作为 $path(形式为 ["list", 1, "key2"])将其每个元素转换为使用 map(tostring) 的字符串表示形式(这给了我们 ["list", "1", "key2"])和 join 它们带有下划线。我们将其作为我们要创建的对象中 "entry" 的键:作为值,我们在给定的 $path 处获得原始对象的值。

最后,我们使用 from_entries 将键值对数组转换为 JSON 对象。这将为我们提供类似于 Jeff 答案的输出:即,其中仅出现具有值的键。

但是,您的原始问题要求出现在任何输入对象上的值出现在所有输出中,当输入中缺少相应的值时,相应的值设置为空字符串。这是一个执行此操作的 jq 程序:正如 Jeff 在他的回答中所说,您需要 slurp (-s) 所有输入值才能使其成为可能:

(map(leaf_paths) | unique) as $paths |
map([$paths[] as $path | {
    "key": $path | map(tostring) | join("_"),
    "value": (getpath($path) // "")
}] | from_entries)[]

你会注意到它与第一个程序非常相似:主要区别在于我们将 slurped 对象中的所有唯一路径作为 $paths,并且对于每个对象,我们都经过这些路径而不是继续通过该对象的路径。我们还使用替代运算符 (//) 将缺失值设置为空字符串。

希望对您有所帮助!

如果您想将 toplevel_key 与列表作为单独行上的字符串连接,您可以使用以下内容:

jq -r '"\(.toplevel_key) - " as $i | [.list | to_entries[] | "\(.value | .key1), \(.value | .key2)"] | join(", ") as $j | $i + $j' toplevel.json

这将提供以下结果:

top value 1 - value 1, value 2, value 3, value 4
top value 2 - value 5, value 6