转置/旋转/翻转 JSON 元素

3
我想要“转置”(不确定这是否是正确的词)JSON元素。
例如,我有一个像这样的JSON文件:
{
  "name": {
    "0": "fred",
    "1": "barney"
  },
  "loudness": {
    "0": "extreme",
    "1": "not so loud"
  }
}

...并且我想生成这样的JSON数组:
[
  {
    "name": "fred",
    "loudness": "extreme"
  },
  {
    "name": "barney",
    "loudness": "not so loud"
  }
]

我的原始JSON有很多比“name”和“loudness”更多的一级元素,也有更多的名称、特性等。
对于这个简单的例子,我可以完全指定转换如下:
$ echo '{"name":{"0":"fred","1":"barney"},"loudness":{"0":"extreme","1":"not so loud"}}'| \
> jq '[{"name":.name."0", "loudness":.loudness."0"},{"name":.name."1", "loudness":.loudness."1"}]'

[
  {
    "name": "fred",
    "loudness": "extreme"
  },
  {
    "name": "barney",
    "loudness": "not so loud"
  }
]

"...但对于原始的JSON数据来说,这是不可行的。
在我的更大的JSON文件中,如何使用jq创建所需的输出而又不依赖于关键字?"
3个回答

3

是的,transpose 是一个恰当的词,因为下面的内容明确表述了。

下面这个通用的辅助函数提供了一种简单的解决方案,它完全不关心包含对象和内部对象的键名:

# Input: an array of values
def objectify($keys):
  . as $in | reduce range(0;length) as $i ({}; .[$keys[$i]] = $in[$i]);

假设内部键的顺序一致

假设内部对象中的键名按照一致的顺序给出,现在可以按照以下方式获得解决方案:

keys_unsorted as $keys
| [.[] | [.[]]] | transpose
| map(objectify($keys))

没有假定内部键的顺序一致性

如果不能假定内部键的顺序是一致的,那么可以使用通用辅助函数对它们进行排序,例如:

def reorder($keys):
  . as $in | reduce $keys[] as $k ({}; .[$k] = $in[$k]);

或者,如果你更喜欢无reduce的定义:

def reorder($keys): [$keys[] as $k | {($k): .[$k]}] | add;

可以按照以下方式修改上面的“main”程序:

keys_unsorted as $keys
| (.[$keys[0]]|keys_unsorted) as $inner
| map_values(reorder($inner))
| [.[] | [.[]]] | transpose
| map(objectify($keys))

前面的解决方案仅考虑了第一个内部对象中的键名。


你认为使用 with_entries(.key = $keys[.key]) 简化 objectify 函数怎么样? - luciole75w
我喜欢它,但现在处理键比值多的情况已经不再那么明显了。 - peak
好的,这里键数组的索引方式与缩减方法相同,只要有足够的键(或更多),我就看不出有什么问题了。或者我误解了你的意思? - luciole75w

2
Peak's solution的基础上,这里提供了一种替代方案,它基于group_by来处理任意顺序的内部键。
keys_unsorted as $keys
| map(to_entries[])
| group_by(.key)
| map(with_entries(.key = $keys[.key] | .value |= .value))

使用路径是一个好主意,正如Hobbs所指出的(译者注:原文中链接未提供)。你也可以像这样做:
[ path(.[][]) as $p | { key: $p[0], value: getpath($p), id: $p[1] } ]
| group_by(.id)
| map(from_entries)

太棒了!我也在想是否可以在这里使用INDEXJOIN - rickhg12hs
1
@rickhg12hs 有趣的想法。这些内置函数肯定可以在这里提供帮助,但我并不认为解决方案像上面那样简单(还请注意它们需要 jq 1.6+)。但如果您发现了什么,请随时在此帖子中做出贡献 ^^ - luciole75w
哦,这很好。我正在寻找使用 from_entries 的方法。 - hobbs
这里的所有答案都非常好,而这个答案似乎是其他答案的完善。最佳答案! - rickhg12hs
很高兴你们都觉得这篇文章有用 :) - luciole75w

2
这有点复杂,但它可以运行:
. as $data | 
reduce paths(scalars) as $p (
  [];
  setpath(
    [ $p[1] | tonumber, $p[0] ];
    ( $data | getpath($p) ) 
  ) 
)

首先,将顶层内容存储为$data,因为在reduce块中.即将获得新值。

然后,调用paths(scalars),它会给出输入中所有叶节点的键路径。例如,对于您的示例,它将给出["name", "0"],然后是["name", "1"],然后是["loudness", "0"],然后是["loudness", "1"]

对每个路径运行一个reduce,以空数组开始减少。

对于每个路径,构造一个相反顺序的新路径,其中数字字符串转换为实际可用作数组索引的数字,例如["name", "0"]变成[0, "name"]

然后使用getpath获取$data中旧路径处的值,并使用setpath.中设置新路径处的值,并将其作为下一个.返回给reduce

最终结果将是:

[
  {
    "name": "fred",
    "loudness": "extreme"
  },
  {
    "name": "barney",
    "loudness": "not so loud"
  }
]

如果您的实际数据结构可能是两个层次,则需要用更合适的表达式替换[ $p[1] | tonumber,$p[0] ]来转换路径。或者,您的一些“值”可能是对象/数组,您希望保留它们不变,这种情况下您可能需要用paths | select(length == 2)之类的内容来替换paths(scalars)

太棒了!如果一天内没有更好的答案发布,我会接受它。 - rickhg12hs

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