Алгоритм (JS или псевдокод), чтобы получить разницу между двумя XML деревьями - PullRequest
0 голосов
/ 02 марта 2020

Итак, я пытаюсь найти способ получить разницу между двумя XML деревьями (примеры ниже), но ничего не могу придумать. Мне нужно, чтобы результат представлял собой массив различий: каждый элемент массива содержит измененный узел, способ его изменения (добавления, удаления) и путь к узлу.

Редактировать: Забыл упомянуть, порядок XML не должен иметь значения. Я попытался использовать npm / dom-Compare, но он не дает желаемого результата (с примерами ниже), потому что он не ожидает увидеть новый тег (dir photos), но не дает информации о нем после того, как он нашел неожиданный тег.

1.

<dir name="rootDir">
    <dir name="childDir">
        <file name="hello.jpg"/>
    </dir>
    <file name="linux.txt"/>
    <file name="img.png"/>
</dir>

2.

<dir name="rootDir">
    <dir name="childDir">
        <file name="hello.jpg"/>
        <file name="interesting.brain"/>
    </dir>
    <dir name="photos">
        <file name="me.dng"/>
    </dir>
    <file name="img.png"/>
</dir>

Мои XML источники будут содержать только теги и теги.

Например, в двух XML документах, приведенных выше, сравнение (1, 2) должно привести к следующему: (Для моих целей нет изменений «изменено», например, если имя файла изменилось, то это новый файл и старый обрабатывается так, как если бы он был удален, а не перемещен, и каталоги не включаются, если их файлы изменяются).

[
    {node: '<file name="interesting.brain"/>', path: '/rootDir/childDir' change: 'added'},
    {node: '<dir name="photos">', path: '/rootDir', change: 'added'}
    {node: '<file name="linux.txt"/>', path: '/rootDir', change: 'deleted'}
]

Моей первой мыслью было сначала проанализировать строки XML в JS объектах, используя fast- xml -parser, что приводит к следующим объектам:

1.

{ dir: [
    {
        name: 'rootDir',
        dir: [
            {
                name: 'childDir',
                file: [
                    { name: 'hello.jpg' }
                ]
            }
        ],
        file: [
            { name: 'linux.txt' },
            { name: 'img.png' }
        ]
    }
] }

2.

{ dir: [
    {
        name: 'rootDir',
        dir: [
            {
                name: 'childDir',
                file: [
                    { name: 'hello.jpg' },
                    { name: 'interesting.brain' }
                ]
            },
            {
                name: 'photos',
                file: [
                    { name: 'me.dng' }
                ]
            }
        ],
        file: [
            { name: 'img.png' }
        ]
    }
] }

Однако это приводит к дополнительным осложнениям, поскольку Формат использует как массивы, так и объекты, что, по крайней мере, увеличивает рабочая нагрузка в выяснении, как различать оба. Это также, вероятно, немного медленнее, так как, очевидно, вам сначала нужно проанализировать строку XML, не говоря уже о добавлении в стороннюю библиотеку.

Поиск любого совета или алгоритма псевдокода, который я могу использовать для решить эту проблему. Следует отметить, что я использую Typescript и нацеливаюсь на ES6 / Node.js.

Cheers.

Ответы [ 2 ]

1 голос
/ 02 марта 2020

Есть компания под названием Delta XML, вся бизнес-модель которой основана на решении этой проблемы. Я просто упоминаю, что вы понимаете, что решаете нетривиальную проблему.

Например, вы говорите: Забыл упомянуть, порядок XML не должен иметь значения.

Это иллюстрирует тот факт, что люди хотят, чтобы сравнение отражало семантику их конкретного XML словаря, а не только сам синтаксис XML. Очевидно, что существует множество XML словарей, в которых изменение порядка элементов является существенным изменением.

Даже с простым текстом или символьными строками существует целая научная литература c, касающаяся различий. Например, прочитайте статью Дэвида Бирнбаума (David Birnbaum) в XML Прага 2020 (https://archive.xmlprague.cz/2020/files/xmlprague-2020-proceedings.pdf#page = 57 ) о реализации алгоритма Нидлмана-Вунша в XSLT 3.0.

Конечно, Вы можете изобрести алгоритм, соответствующий вашим конкретным потребностям, без тщательного исследования области. Но, по крайней мере, чтобы получить достойный ответ на этот вопрос, вам нужно более точно определить ваши требования. Один простой пример не является спецификацией.

Конкретная характеристика c этой проблемы заключается в том, что оптимальные алгоритмы (идентифицирующие минимальное количество различий) могут быть очень трудоемкими (возможно, O (n ^ 3)) так что вам, возможно, придется пойти на компромисс между качеством ответа и временем, затраченным на его доставку.

1 голос
/ 02 марта 2020

Я создал простое решение, основанное на вашем описании проблемы. Это может быть не совсем оптимально, но это делает работу (надеюсь). Посмотрите, нужно ли это вам.

Мы будем использовать xml -parse для обработки XML.

TL; DR: Получите полный код здесь .

Итак, чтобы решить эту проблему, мы будем go выполнить два шага.

ШАГ 1: Создать карты XML файлов

Давайте определим структуру данных под названием «карта» (должна была выбрать более описательное имя, но не могла придумать одно). Эта карта будет словарь .

Наша карта состоит из пар ключ-значение.

  • Ключ - это путь. Наша карта будет содержать все существующие пути в структуре XML.
  • Значение - это другой словарь:
    • Ключ - это имя элемента.
    • Значение тег элемента.

Таким образом, карты двух приведенных вами примеров XML структур будут выглядеть так:

Старая карта:

{
   "/rootDir":{
      "childDir":"dir",
      "linux.txt":"file",
      "img.png":"file"
   },
   "/rootDir/childDir":{
      "hello.jpg":"file"
   }
}

Новая карта:

{
   "/rootDir":{
      "childDir":"dir",
      "photos":"dir",
      "img.png":"file"
   },
   "/rootDir/childDir":{
      "hello.jpg":"file",
      "interesting.brain":"file"
   },
   "/rootDir/photos":{
      "me.dng":"file"
   }
}

Рекурсивная функция для построения карты из структуры XML будет выглядеть следующим образом:

// recursive function to build map
function buildMap(element, path, map) {
  map[path] = {}
  // const childElements = element.childNodes.filter(childNode => childNode.type === 'element');
  for (const childNode of element.childNodes) {
    // skip text (because the xml-parse package also returns the unnecessary texts in an XML structure, e.g. line breaks)
    if (childNode.type === 'text') continue;

    // process child element
    // add child element's name to indicate that this path has a child with this name
    // use child element's type (dir/file) as the value
    map[path][childNode.attributes.name] = childNode.tagName;

    // if child element is dir, process it recursively
    if (childNode.tagName === 'dir') buildMap(childNode, `${path}/${childNode.attributes.name}`, map);
  }
}

STEP 2: Получите различия между двумя картами

Теперь мы выведем изменения из карт.

По сути, мы сделаем обход по путям старого map, получите набор дочерних элементов в каждом пути (из обеих карт) и сравните два набора дочерних элементов, чтобы получить необходимые нам изменения.

Функции для этого шага следующие:

// function to get the differences between two maps
function diffMaps(oldMap, newMap) {
  const changes = [];
  // traverse each path of the old map
  for (const key of Object.keys(oldMap)) {
    // get children in this path for both old map and new map
    const oldChildren = oldMap[key];
    const newChildren = newMap[key];
    changes.push(...diffChildren(key, oldChildren, newChildren));
  }
  return changes;
}

// function to get the differences between the children of two maps
function diffChildren(path, oldChildren, newChildren) {
  const changes = [];
  // traverse each child of the old children
  for (const key of Object.keys(oldChildren)) {
    // if new children also have that child ==> no change ==> remove that child from new children and continue
    if (newChildren[key]) {
      // the reason for deleting is that after we have deleted all the keys that are present in old children, the remaining keys in new children will be the newly added ones.
      delete newChildren[key];
      continue;
    }

    // new children don't have that child ==> deleted ==> add to changes
    const type = oldChildren[key];
    changes.push({
      node: type === 'dir' ? `<dir name="${key}">` : `<file name="${key}"/>`,
      path: path,
      change: 'deleted'
    });
  }

  // traverse each child of the new children and add them to changes
  for (const key of Object.keys(newChildren)) {
    const type = newChildren[key];
    changes.push({
      node: type === 'dir' ? `<dir name="${key}">` : `<file name="${key}"/>`,
      path: path,
      change: 'added'
    });
  }

  return changes;
}

ЗАКЛЮЧИТЕЛЬНО: Тестирование

Теперь, когда у нас есть необходимые функции, просто подключите наши данные и запустите:)

const oldXmlString = String.raw`
<dir name="rootDir">
    <dir name="childDir">
        <file name="hello.jpg"/>
    </dir>
    <file name="linux.txt"/>
    <file name="img.png"/>
</dir>
`.trim();

const newXmlString = String.raw`
<dir name="rootDir">
    <dir name="childDir">
        <file name="hello.jpg"/>
        <file name="interesting.brain"/>
    </dir>
    <dir name="photos">
        <file name="me.dng"/>
    </dir>
    <file name="img.png"/>
</dir>
`.trim();

const oldXml = xml.parse(oldXmlString);
const newXml = xml.parse(newXmlString);

const oldRoot = oldXml[0];
const newRoot = newXml[0];

// maps with path as key and child nodes' names as value
const oldMap = {};
const newMap = {};

buildMap(oldRoot, `/${oldRoot.attributes.name}`, oldMap);
buildMap(newRoot, `/${newRoot.attributes.name}`, newMap);

const diffs = diffMaps(oldMap, newMap);
console.log(diffs);

Выход:

[ { node: '<file name="linux.txt"/>',
    path: '/rootDir',
    change: 'deleted' },
  { node: '<dir name="photos">',
    path: '/rootDir',
    change: 'added' },
  { node: '<file name="interesting.brain"/>',
    path: '/rootDir/childDir',
    change: 'added' } ]
...