Как объединить одинаково структурированные, вложенные файлы 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 в один большой массив.

Другие вопросы по тегам