Как объединить одинаково структурированные, вложенные файлы json, используя jq
Мне нужно объединить массив в серию одинаково структурированных, вложенных файлов JSON, которые используют одни и те же ключи более высокого уровня.
Цель состоит в том, чтобы создать объединенный файл, сохранив при этом все существующие ключи и значения более высокого уровня.
Файл 1:
{
"account": "123456789012",
"regions": [
{
"region": "one",
"services": [
{
"groups": [
{
"GroupId": "123456",
"GroupName": "foo"
},
{
"GroupId": "234567",
"GroupName": "bar"
}
]
}
]
}
]
}
Файл 2:
{
"account": "123456789012",
"regions": [
{
"region": "one",
"services": [
{
"group_policies": [
{
"GroupName": "foo",
"PolicyNames": [
"all_foo",
"all_bar"
]
},
{
"GroupName": "bar",
"PolicyNames": [
"all_bar"
]
}
]
}
]
}
]
}
Ожидаемый результат:
{
"account": "123456789012",
"regions": [
{
"region": "one",
"services": [
{
"groups": [
{
"GroupId": "123456",
"GroupName": "foo"
},
{
"GroupId": "234567",
"GroupName": "bar"
}
]
},
{
"group_policies": [
{
"GroupName": "foo",
"PolicyNames": [
"all_foo",
"all_bar"
]
},
{
"GroupName": "bar",
"PolicyNames": [
"all_bar"
]
}
]
}
]
}
]
}
Я попробовал следующее на основе ответов на другие вопросы этого типа безуспешно:
jq -s '.[0] * .[1]' test1.json test2.json
jq -s add test1.json test2.json
jq -n '[inputs[]]' test{1,2}.json
Следующее успешно объединяет массив, но ему не хватает ключей и значений более высокого уровня в результатах.
jq -s '.[0].regions[0].services[0] * .[1].regions[0].services[0]' test1.json test2.json
Я предполагаю, что есть простое решение jq для этого, которое ускользает от моих поисков. Если нет, любая комбинация jq и bash будет работать для решения.
2 ответа
Вот решение, которое преобразует массивы в объекты до уровня услуг, сливается с *
и преобразует обратно в форму массива. Если file1
а также file2
содержит пример данных, а затем эту команду:
$ jq -Mn --argfile file1 file1 --argfile file2 file2 '
def merge: # merge function
($file1, $file2) # process $file1 then $file2
| .account as $a # save .account in $a
| .regions[] # for each element of .regions
| .region as $r # save .region in $r
| .services[] as $s # save each element of .services in $s
| {($a): {($r): $s}} # generate object for each account,region,service
# | debug # uncomment debug here to see stream
;
reduce merge as $x ({}; . * $x) # use '*' to recombine all the objects from merge
# | debug # uncomment debug here to see combined object
| keys[] as $a # for each key (account) of combined object
| {account:$a, regions:[ # construct object with {account, regions array}
.[$a] # for each account
| keys[] as $r # for each key (region) of account object
| {region:$r, services:[ # constuct object with {region, services array}
.[$r] # for each region
| keys[] as $s # for each service
| {($s): .[$s]} # generate service object
]} # add service objects to service array
]}' # add region object ot regions array
производит
{
"account": "123456789012",
"regions": [
{
"region": "one",
"services": [
{
"group_policies": [
{
"GroupName": "foo",
"PolicyNames": [
"all_foo",
"all_bar"
]
},
{
"GroupName": "bar",
"PolicyNames": [
"all_bar"
]
}
]
},
{
"groups": [
{
"GroupId": "123456",
"GroupName": "foo"
},
{
"GroupId": "234567",
"GroupName": "bar"
}
]
}
]
}
]
}
расширенное объяснение
Сборка этого шаг за шагом дает лучшую картину того, как это работает. Начните только с этого фильтра
def merge: # merge function
($file1, $file2) # process $file1 then $file2
| .account as $a # save .account in $a
| $a
;
merge
так как есть два объекта (один из файла file1 и один из файла file2), это выводит .account
с каждого:
"123456789012"
"123456789012"
Обратите внимание, что .account as $a
не меняет текущее значение .
, Переменные позволяют нам "углубляться" в подобъекты без потери контекста более высокого уровня. Рассмотрим этот фильтр:
def merge: # merge function
($file1, $file2) # process $file1 then $file2
| .account as $a # save .account in $a
| .regions[] # for each element of .regions
| .region as $r # save .region in $r
| [$a, $r]
;
merge
какие выходы (учетная запись, регион) пары:
["123456789012","one"]
["123456789012","one"]
Теперь мы можем продолжать углубляться в услуги:
def merge: # merge function
($file1, $file2) # process $file1 then $file2
| .account as $a # save .account in $a
| .regions[] # for each element of .regions
| .region as $r # save .region in $r
| .services[]
| [$a, $r, .]
;
merge
Третий элемент массива (.
) в этот момент относится к каждой последующей службе в .services
массив, так что этот фильтр генерирует
["123456789012","one",{"groups":[{"GroupId":"123456","GroupName":"foo"},
{"GroupId":"234567","GroupName":"bar"}]}]
["123456789012","one",{"group_policies":[{"GroupName":"foo","PolicyNames":["all_foo","all_bar"]},
{"GroupName":"bar","PolicyNames":["all_bar"]}]}]
Эта (полная) функция слияния:
def merge: # merge function
($file1, $file2) # process $file1 then $file2
| .account as $a # save .account in $a
| .regions[] # for each element of .regions
| .region as $r # save .region in $r
| .services[] as $s # save each element of .services in $s
| {($a): {($r): $s}} # generate object for each account,region,service
;
merge
производит поток
{"123456789012":{"one":{"groups":[{"GroupId":"123456","GroupName":"foo"},
{"GroupId":"234567","GroupName":"bar"}]}}}
{"123456789012":{"one":{"group_policies":[{"GroupName":"foo","PolicyNames":["all_foo","all_bar"]},
{"GroupName":"bar","PolicyNames":["all_bar"]}]}}}
Важно отметить, что это объекты, которые можно легко объединить с *
на шаг уменьшения:
def merge: # merge function
($file1, $file2) # process $file1 then $file2
| .account as $a # save .account in $a
| .regions[] # for each element of .regions
| .region as $r # save .region in $r
| .services[] as $s # save each element of .services in $s
| {($a): {($r): $s}} # generate object for each account,region,service
;
reduce merge as $x ({}; . * $x) # use '*' to recombine all the objects from merge
Reduce инициализирует свое локальное состояние (.
) чтобы {}
а затем вычисляет новое состояние для каждого результата из функции слияния путем оценки . * $x
, рекурсивно комбинируя объекты слияния, построенные из $ file1 и $ file:
{"123456789012":{"one":{"groups":[{"GroupId":"123456","GroupName":"foo"},
{"GroupId":"234567","GroupName":"bar"}],
"group_policies":[{"GroupName":"foo","PolicyNames":["all_foo","all_bar"]},
{"GroupName":"bar","PolicyNames":["all_bar"]}]}}}
Обратите внимание, что *
прекратили слияние объектов массива в ключах 'groups' и 'group_policies'. Если бы мы хотели продолжить слияние, мы могли бы создать больше объектов в функции слияния. например, рассмотрим это расширение:
def merge: # merge function
($file1, $file2) # process $file1 then $file2
| .account as $a # save .account in $a
| .regions[] # for each element of .regions
| .region as $r # save .region in $r
| .services[] as $s # save each element of .services in $s
| (
$s.groups[]? as $g
| {($a): {($r): {groups: {($g.GroupId): $g}}}}
), (
$s.group_policies[]? as $p
| {($a): {($r): {group_policies: {($p.GroupName): $p}}}}
)
;
merge
Это слияние идет глубже предыдущего, производя
{"123456789012":{"one":{"groups":{"123456":{"GroupId":"123456","GroupName":"foo"}}}}}
{"123456789012":{"one":{"groups":{"234567":{"GroupId":"234567","GroupName":"bar"}}}}}
{"123456789012":{"one":{"group_policies":{"foo":{"GroupName":"foo","PolicyNames":["all_foo","all_bar"]}}}}}
{"123456789012":{"one":{"group_policies":{"bar":{"GroupName":"bar","PolicyNames":["all_bar"]}}}}}
Здесь важно, чтобы ключи "groups" и "group_policies" содержали объекты, что означает в этом фильтре
def merge: # merge function
($file1, $file2) # process $file1 then $file2
| .account as $a # save .account in $a
| .regions[] # for each element of .regions
| .region as $r # save .region in $r
| .services[] as $s # save each element of .services in $s
| (
$s.groups[]? as $g
| {($a): {($r): {groups: {($g.GroupId): $g}}}}
), (
$s.group_policies[]? as $p
| {($a): {($r): {group_policies: {($p.GroupName): $p}}}}
)
;
reduce merge as $x ({}; . * $x)
снижение *
объединит группы и групповые политики вместо их перезаписи, генерируя:
{"123456789012":{"one":{"groups":{"123456":{"GroupId":"123456","GroupName":"foo"},
"234567":{"GroupId":"234567","GroupName":"bar"}},
"group_policies":{"foo":{"GroupName":"foo","PolicyNames":["all_foo","all_bar"]},
"bar":{"GroupName":"bar","PolicyNames":["all_bar"]}}}}}
Возвращение этого в исходную форму потребует немного больше работы, но не много:
def merge: # merge function
($file1, $file2) # process $file1 then $file2
| .account as $a # save .account in $a
| .regions[] # for each element of .regions
| .region as $r # save .region in $r
| .services[] as $s # save each element of .services in $s
| (
$s.groups[]? as $g
| {($a): {($r): {groups: {($g.GroupId): $g}}}}
), (
$s.group_policies[]? as $p
| {($a): {($r): {group_policies: {($p.GroupName): $p}}}}
)
;
reduce merge as $x ({}; . * $x)
| keys[] as $a # for each key (account) of combined object
| {account:$a, regions:[ # construct object with {account, regions array}
.[$a] # for each account
| keys[] as $r # for each key (region) of account object
| {region:$r, services:[ # constuct object with {region, services array}
.[$r] # for each region
| {groups: [.groups[]]} # add groups to service
, {group_policies: [.group_policies[]]} # add group_policies to service
]}
]}
Теперь с этой версией предположим, что наш file2 содержит группу, а также group_policies. например
{
"account": "123456789012",
"regions": [
{
"region": "one",
"services": [
{
"groups": [
{
"GroupId": "999",
"GroupName": "baz"
}
]
},
{
"group_policies": [
{
"GroupName": "foo",
"PolicyNames": [
"all_foo",
"all_bar"
]
},
{
"GroupName": "bar",
"PolicyNames": [
"all_bar"
]
}
]
}
]
}
]
}
Где первая версия этого решения производится
{
"account": "123456789012",
"regions": [
{
"region": "one",
"services": [
{
"group_policies": [
{
"GroupName": "foo",
"PolicyNames": [
"all_foo",
"all_bar"
]
},
{
"GroupName": "bar",
"PolicyNames": [
"all_bar"
]
}
]
},
{
"groups": [
{
"GroupId": "999",
"GroupName": "baz"
}
]
}
]
}
]
}
Эта пересмотренная версия производит
{
"account": "123456789012",
"regions": [
{
"region": "one",
"services": [
{
"groups": [
{
"GroupId": "123456",
"GroupName": "foo"
},
{
"GroupId": "234567",
"GroupName": "bar"
},
{
"GroupId": "999",
"GroupName": "baz"
}
]
},
{
"group_policies": [
{
"GroupName": "foo",
"PolicyNames": [
"all_foo",
"all_bar"
]
},
{
"GroupName": "bar",
"PolicyNames": [
"all_bar"
]
}
]
}
]
}
]
}
Объединение jq add
и JQ дает нам:
jq '.hits.hits' logs.*.json | jq -s add
это объединит все массивы hit.hits во всех файлах logs.*.json в один большой массив.