Слияние двух сложных объектов JSON

#python #json #merge

#python #json #слияние

Вопрос:

Я хочу объединить два объекта JSON в новый. Я пытаюсь использовать jsonmerge с полной схемой json, но я не знаю, как правильно настроить стратегии слияния. Я почти уверен, что это можно сделать.

Код:

 import json
from jsonmerge import Merger
from jsonschema import validate
full_build = {
         "captures": [
            {
               "compiler": "gnu",
               "executable": "gcc",
               "cmd": ["gcc", "options", "file1.cpp"],
               "cwd": ".",
               "env": ["A=1", "B=2"],
            },
            {
               "compiler": "gnu",
               "executable": "gcc",
               "cmd": ["gcc", "options", "file2.cpp"],
               "cwd": ".",
               "env": ["A=1", "B=2"],
            }
         ]
}
incremental_build = {
         "captures": [
            {
               "compiler": "gnu",
               "executable": "gcc",
               "cmd": ["gcc", "new options", "file2.cpp"],
               "cwd": ".",
               "env": ["A=1", "NEW=2"],
            },
            {
               "compiler": "gnu",
               "executable": "gcc",
               "cmd": ["gcc", "options", "file3.cpp"],
               "cwd": ".",
               "env": ["A=1", "B=2"],
            }
         ]
}
schema = {
   "type" : "object",
   "properties" : {
      "captures": {
         "type" : "array",
         "items" : {
            "type" : "object",
            "properties" : {
               "cmd" : {
                  "type" : "array",
                  "items" : {"type" : "string"},
               },
               "compiler" : {"type" : "string"},
               "cwd" : {"type" : "string"},
               "env" : {
                  "type" : "array",
                  "items" : {"type" : "string"},
               },
               "executable" : {"type" : "string"},
            }
         }
      }
   }
}
validate(instance=full_build, schema=schema)

mergeSchema = schema
merger = Merger(mergeSchema)
result = merger.merge(full_build, incremental_build)
print(json.dumps(result, indent=3))
  

Результат:

 {
   "captures": [
      {
         "compiler": "gnu",
         "executable": "gcc",
         "cmd": [
            "gcc",
            "options",
            "file3.cpp"
         ],
         "cwd": ".",
         "env": [
            "A=1",
            "B=2"
         ]
      }
   ]
}
  

Ожидаемый результат:

 {
   "captures": [
      {
         "compiler": "gnu",
         "executable": "gcc",
         "cmd": [
            "gcc",
            "options",
            "file1.cpp"
         ],
         "cwd": ".",
         "env": [
            "A=1",
            "B=2"
         ]
      },
      {
         "compiler": "gnu",
         "executable": "gcc",
         "cmd": [
            "gcc",
            "new options",
            "file2.cpp"
         ],
         "cwd": ".",
         "env": [
            "A=1",
            "NEW=2"
         ]
      },
      {
         "compiler": "gnu",
         "executable": "gcc",
         "cmd": [
            "gcc",
            "options",
            "file3.cpp"
         ],
         "cwd": ".",
         "env": [
            "A=1",
            "B=2"
         ]
      }
   ]
}
  

Есть еще много вещей, которые следует учитывать (например, наличие большего или меньшего количества параметров / переменных среды, чем раньше), но я думаю, что с небольшой подсказкой мне удастся выполнить задачу.
Я действительно не хочу это жестко кодировать.

И нет, я не могу изменить структуру json :(.

Предыстория: я хочу объединить выходные данные оболочки SonarQube build, потому что я не хочу выполнять полную сборку, чтобы получить все файлы в выходных данных оболочки.

Ответ №1:

У вас есть два массива объектов JSON, и на их основе вы хотите создать единый массив.

В вашем примере кажется, что иногда вы хотите, чтобы объекты из incremental_build перезаписывали объекты из full_build (есть только один объект, который упоминается file2.cpp в конечном массиве), но иногда вы этого не делаете (объект с file3.cpp не перезаписывает объект с file1.cpp ).).

Вы не указали, каким именно должно быть правило для этого, но я предполагаю, что имя файла — это то, с чем вы хотите сопоставить. Я также предполагаю, что вы хотите рассматривать сами элементы массива как неизменяемые и не хотите в дальнейшем объединять их вместе, когда имя файла совпадает.

Для достижения этого вы можете использовать следующую схему:

 schema = {
   "properties" : {
      "captures": {
         "mergeStrategy": "arrayMergeById",
         "mergeOptions": {
            "idRef": "/cmd/2"
         },
         "items": {
            "mergeStrategy": "overwrite"
         }
      }
   }
}

merger = Merger(schema)
result = merger.merge(full_build, incremental_build)
  

Вам не нужна полная схема, если вы не хотите также проверять свой JSON. jsonmerge сам по себе заботится только об информации о стратегии слияния.

Приведенная выше схема указывает, что массив под свойством captures в объекте верхнего уровня должен быть объединен с использованием arrayMergeById стратегии. Эта стратегия объединяет элементы массива на основе значения, на которое указывает idRef ссылка. В вашем примере имя файла является третьим элементом cmd свойства (указатели JSON используют индексацию на основе нуля).

arrayMergeById объединяет соответствующие элементы массива на основе их собственных схем. По умолчанию они будут объединены с использованием objectMerge стратегии. Это привело бы к неправильному результату в случае, когда у элемента в incremental_build будет отсутствовать свойство, которое присутствует в соответствующем full_build элементе. Следовательно, приведенная выше схема также определяет стратегию перезаписи для всех элементов captures массива.

Комментарии:

1. Спасибо avian. Это работает, но имя файла не всегда находится в индексе 2 :(. Я думаю, что ответ wouter bolsterlee работает лучше в этом случае использования.

Ответ №2:

Кажется, вам вообще не нужна какая-либо сложная операция слияния. По сути, вы хотите объединить списки ‘захватов’ из обеих структур в новую структуру, которая содержит их все. Этого можно достичь, сделав копию и просто расширив список впоследствии:

 full_build = ...
incremental_build = ...
combined = copy.deepcopy(full_build)
combined['captures'].extend(incremental_build['captures'])
  

Если вы хотите ‘дедуплицировать’ на основе какого-либо атрибута, например, имени файла, вы можете использовать что-то вроде этого:

 def get_filename_from_capture(cmd):
    return cmd["cmd"][-1]


all_captures = full_build["captures"]   incremental_build["captures"]
captures_by_filename = {
    get_filename_from_capture(capture): capture for capture in all_captures
}

combined = copy.deepcopy(full_build)
combined["captures"] = list(captures_by_filename.values())
  

Комментарии:

1. В результате получается 4 captures элемента массива с "file2.cpp" удвоением. "file2.cpp" Должно быть только один раз и быть одним из incremental_build

2. расширил ответ до ‘dedup’ на основе некоторого произвольного значения

3. Спасибо. Мне нужно было бы отредактировать get_filename_from_capture для лучшего распознавания имени файла (оно может быть где угодно), но это должно сработать!

4. да, я подумал, что вам понадобится что-то более умное, поэтому я извлек это в помощник. удачи. (и отметьте ответ как принятый!)