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

4

我有以下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将其转换,将列表展开为固定数量的“列”,最终得到一个扁平的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}'

这个方法可以得到我想要的结果,但是我必须手动编写所有固定的“列”(在生产中将会有更多)。
我知道我可以使用脚本来生成那段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"}}
3个回答

9
只要你知道特定键的名称,Jeff给出的答案非常好。这里提供一种不硬编码特定键名的答案,即它适用于任何结构和嵌套级别的对象:
[leaf_paths as $path | {
    "key": $path | map(tostring) | join("_"),
    "value": getpath($path)
}] | from_entries

解释: paths 是一个内置函数,它输出表示传递给它的输入每个元素位置的数组,递归地:该数组中的元素是导致请求的数组元素的有序键名和索引。leaf_paths 是它的一种版本,只获取到“叶”元素的路径,也就是不包含其他元素的元素。
为了澄清,给定输入[[1, 2]]paths 将输出[0],[0, 0],[0, 1](即[1, 2],1 和 2 的路径),而leaf_paths 只会输出[0, 0],[0, 1]
那就是最困难的部分。之后,我们将每个路径作为 $path(形式为 ["list", 1, "key2"])获取,使用 map(tostring) 将其每个元素转换为其字符串表示形式(这样我们就可以得到 ["list", "1", "key2"]),然后使用下划线 _ 连接它们。我们将此作为我们要创建的对象中“条目”的键:作为值,我们获取给定$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)[]

你会发现这个程序与第一个程序非常相似:主要区别是我们在所读取的对象中获取所有唯一路径作为$paths,然后对每个对象,我们遍历这些路径而不是该对象的路径。我们还使用替代运算符(//)将缺失的值设置为空字符串。
希望这可以帮到你!

哇,伙计,你刚刚让我大吃一惊! :) 我偶然发现了那个路径函数,觉得它可能有用,但不知道如何拼凑起来。 很高兴知道那个//运算符,真是有用的东西! 谢谢! - Elias Dorneles
2
这些路径...真的很难习惯使用它们。不错,我得把它们融入我的技能库中。 - Jeff Mercado

4
以下是您可以建立的方法:

这里是如何构建它的:

{ "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"
}

如果需要填充它,你需要先读取结果以确定实际需要多少填充,然后再添加填充。但是我建议暂时保持现状。


谢谢,这太棒了! :) - Elias Dorneles

0
如果您想将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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接