Инструмент командной строки с открытым исходным кодом для Linux, чтобы различать XML-файлы, игнорируя порядок элементов

Существует ли инструмент командной строки с открытым исходным кодом (для Linux) для сравнения файлов XML, который игнорирует порядок элементов?

Пример входного файла a.xml:

<tag name="AAA">
  <attr name="b" value="1"/>
  <attr name="c" value="2"/>
  <attr name="a" value="3"/>
</tag>

<tag name="BBB">
  <attr name="x" value="111"/>
  <attr name="z" value="222"/>
</tag>
<tag name="BBB">
  <attr name="x" value="333"/>
  <attr name="z" value="444"/>
</tag>

b.xml:

<tag name="AAA">
  <attr name="a" value="3"/>
  <attr name="b" value="1"/>
  <attr name="c" value="2"/>
</tag>

<tag name="BBB">
  <attr name="z" value="444"/>
  <attr name="x" value="333"/>
</tag>
<tag name="BBB">
  <attr name="x" value="111"/>
  <attr name="z" value="222"/>
</tag>

Таким образом, сравнение этих двух файлов не должно давать никаких различий. Сначала я попытался отсортировать файлы с помощью XSLT:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" encoding="WINDOWS-1252" omit-xml-declaration="no" indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*">
        <xsl:sort select="@*" />
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Но проблема в том, что для элементов <tag name="BBB"> сортировки нет Они просто выводят порядок, в котором они вводятся.

Я уже посмотрел на diffXml, xDiff, XMLUnit, xmlstarlet но ни один из них не решает проблему; вывод diff должен быть удобочитаемым, например, как при использовании diff,

Любые подсказки о том, как можно решить сортировку или игнорирование diff-порядка элементов? Спасибо!

6 ответов

У меня была похожая проблема, и я в итоге обнаружил: https://superuser.com/questions/79920/how-can-i-diff-two-xml-files

В этом посте предлагается выполнить каноническую сортировку по XML, а затем выполнить diff. Поскольку вы работаете в Linux, это должно работать для вас чисто. Он работал для меня на моем Mac и должен работать для людей на Windows, если у них установлено что-то вроде Cygwin:

$ xmllint --c14n a.xml > sortedA.xml
$ xmllint --c14n b.xml > sortedB.xml
$ diff sortedA.xml sortedB.xml

Вы должны написать свой собственный интерпретатор для предварительной обработки. XSLT - это один из способов сделать это... возможно; Я не эксперт в XSLT, и я не уверен, что вы можете разобраться с этим.

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

#!/usr/bin/perl

use strict;
use warnings;

# NOTE: general wisdom - do not use simple homebrewed XML parsers like this one!
#
# This makes sweeping assumptions that are not production grade.  Including:
#   1. Assumption of one XML tag per line
#   2. Assumption that no XML tag contains a greater-than character
#      like <foo bar="<oops>" />
#   3. Assumes the XML is well-formed, nothing like <foo><bar>baz</foo></bar>

# recursive function to parse each tag.
sub parse_tag {
  my $tag_name = shift;
  my @level = (); # LOCAL: each recursive call has its OWN distinct @level
  while(<>) {
    chomp;

    # new open tag:  match new tag name, parse in recursive call
    if (m"<\s*([^\s/>]+)[^/>]*>") {
      push (@level, "$_\n" . parse_tag($1) );

    # close tag, verified by name, or else last line of input
    } elsif (m"<\s*/\s*$tag_name[\s>]"i or eof()) {
      # return all children, sorted and concatenated, then the end tag
      return join("\n", sort @level) . "\n$_";

    } else {
      push (@level, $_);
    }
  }
  return join("\n", sort @level);
}

# start with an impossible tag in case there is no root
print parse_tag("<root>");

Сохранить как xml_diff_prep.pl и затем запустите это:

$ diff -sq <(perl xml_diff_prep.pl a.xml) <(perl xml_diff_prep.pl b.xml)
Files /proc/self/fd/11 and /proc/self/fd/12 are identical

(Я использовал -s а также -q флаги должны быть явными. Вы можете использовать gvimdiff или любую другую утилиту или флаги, которые вам нравятся. Обратите внимание, что он идентифицирует файлы по дескриптору файла; это потому, что я использовал трюк bash для запуска команды препроцессора на каждом входе. Они будут в том же порядке, который вы указали. Обратите внимание, что содержимое может находиться в неожиданных местах из-за сортировки, запрошенной этим вопросом.)

Чтобы удовлетворить ваш запрос "инструмента с открытым исходным кодом" "инструмента командной строки", настоящим я выпускаю этот код как Open Source под лицензией Beerware (пункт 2 BSD, если вы считаете, что оно того стоит, вы можете купить мне пиво).

Если вы хотите принять это в произвольной степени, вы можете реализовать что-то, что объединяет два дерева и решает, какие элементы "совпадают" между двумя документами. это позволило бы вам реализовать подходящую логику так, как вы хотите. Вот пример в xslt 2.0:

<xsl:stylesheet version="2.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"

                xmlns:set="http://exslt.org/sets"

                xmlns:primary="primary"
                xmlns:control="control"

                xmlns:util="util"

                exclude-result-prefixes="xsl xs set primary control">

    <xsl:output method="text"/>

    <xsl:strip-space elements="*"/>

    <xsl:template match="/">
        <xsl:call-template name="compare">
            <xsl:with-param name="primary" select="*/*[1]"/><!-- first child of root element, for example -->
            <xsl:with-param name="control" select="*/*[2]"/><!-- second child of root element, for example --> 
        </xsl:call-template>
    </xsl:template>

    <!-- YOUR SPECIFIC OVERRIDES -->

    <xsl:template match="attr" mode="find-match" as="element()?">
        <xsl:param name="candidates" as="element()*"/>
        <!-- attr matches by @name and @value -->
        <xsl:sequence select="$candidates[@name = current()/@name][@value = current()/@value][1]"/>
    </xsl:template>

    <xsl:template match="tag" mode="find-match" as="element()?">
        <xsl:param name="candidates" as="element()*"/>
        <xsl:variable name="attrs" select="attr"/>
        <!-- tag matches if @name matches and attr counts (matched and unmatched) match -->
        <xsl:sequence select="$candidates[@name = current()/@name]
                                         [count($attrs) = count(util:find-match($attrs, attr))]
                                         [count($attrs) = count(attr)][1]"/>
    </xsl:template>

    <xsl:function name="util:find-match">
        <xsl:param name="this"/>
        <xsl:param name="candidates"/>
        <xsl:apply-templates select="$this" mode="find-match">
            <xsl:with-param name="candidates" select="$candidates"/>
        </xsl:apply-templates>
    </xsl:function>

    <!-- END SPECIFIC OVERRIDES -->

    <!-- compare "primary" and "control" elements -->
    <xsl:template name="compare">
        <xsl:param name="primary"/>
        <xsl:param name="control"/>

        <xsl:variable name="diff">
            <xsl:call-template name="match-children">
                <xsl:with-param name="primary" select="$primary"/>
                <xsl:with-param name="control" select="$control"/>
            </xsl:call-template>
        </xsl:variable>

        <xsl:choose>
            <xsl:when test="$diff//*[self::primary:* | self::control:*]">
                <xsl:text>FAIL</xsl:text><!-- or do something more sophisticated with $diff... -->
            </xsl:when>
            <xsl:otherwise>
                <xsl:text>PASS</xsl:text>
            </xsl:otherwise>
        </xsl:choose>

    </xsl:template>

    <!-- default matching template for elements

         for context node (from "primary"), choose from among $candidates (from "control") which one matches

         (for "complex" elements, name has to match, for "simple" elements, name and value do)

         (override with more specific match pattern if desired)
         -->
    <xsl:template match="*" mode="find-match" as="element()?">
        <xsl:param name="candidates" as="element()*"/>
        <xsl:choose>
            <xsl:when test="text() and count(node()) = 1">
                <xsl:sequence select="$candidates[node-name(.) = node-name(current())][text() and count(node()) = 1][. = current()][1]"/>
            </xsl:when>
            <xsl:when test="not(node())">
                <xsl:sequence select="$candidates[node-name(.) = node-name(current())][not(node())][1]"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:sequence select="$candidates[node-name(.) = node-name(current())][1]"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <!-- default matching template for attributes

         for context attr (from "primary"), choose from among $candidates (from "control") which one matches

         (name and value have to match)

         (override with more specific match pattern if desired)
         -->
    <xsl:template match="@*" mode="find-match" as="attribute()?">
        <xsl:param name="candidates" as="attribute()*"/>
        <xsl:sequence select="$candidates[. = current()][node-name(.) = node-name(current())][1]"/>
    </xsl:template>

    <!-- default primary-only template (override with more specific match pattern if desired) -->
    <xsl:template match="@* | *" mode="primary-only">
        <xsl:apply-templates select="." mode="illegal-primary-only"/>
    </xsl:template>

    <!-- write out a primary-only diff -->
    <xsl:template match="@* | *" mode="illegal-primary-only">
        <primary:only>
            <xsl:copy-of select="."/>
        </primary:only>
    </xsl:template>

    <!-- default control-only template (override with more specific match pattern if desired) -->
    <xsl:template match="@* | *" mode="control-only">
        <xsl:apply-templates select="." mode="illegal-control-only"/>
    </xsl:template>

    <!-- write out a control-only diff -->
    <xsl:template match="@* | *" mode="illegal-control-only">
        <control:only>
            <xsl:copy-of select="."/>
        </control:only>
    </xsl:template>

    <!-- assume primary (context) element and control element match, so render the "common" element and recurse -->
    <xsl:template match="*" mode="common">
        <xsl:param name="control"/>

        <xsl:copy>
            <xsl:call-template name="match-attributes">
                <xsl:with-param name="primary" select="@*"/>
                <xsl:with-param name="control" select="$control/@*"/>
            </xsl:call-template>

            <xsl:choose>
                <xsl:when test="text() and count(node()) = 1">
                    <xsl:value-of select="."/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:call-template name="match-children">
                        <xsl:with-param name="primary" select="*"/>
                        <xsl:with-param name="control" select="$control/*"/>
                    </xsl:call-template>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:copy>

    </xsl:template>

    <!-- find matches between collections of attributes in primary vs control -->
    <xsl:template name="match-attributes">
        <xsl:param name="primary" as="attribute()*"/>
        <xsl:param name="control" as="attribute()*"/>
        <xsl:param name="primaryCollecting" as="attribute()*"/>

        <xsl:choose>
            <xsl:when test="$primary and $control">
                <xsl:variable name="this" select="$primary[1]"/>
                <xsl:variable name="match" as="attribute()?">
                    <xsl:apply-templates select="$this" mode="find-match">
                        <xsl:with-param name="candidates" select="$control"/>
                    </xsl:apply-templates>
                </xsl:variable>

                <xsl:choose>
                    <xsl:when test="$match">
                        <xsl:copy-of select="$this"/>
                        <xsl:call-template name="match-attributes">
                            <xsl:with-param name="primary" select="subsequence($primary, 2)"/>
                            <xsl:with-param name="control" select="remove($control, 1 + count(set:leading($control, $match)))"/>
                            <xsl:with-param name="primaryCollecting" select="$primaryCollecting"/>
                        </xsl:call-template>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:call-template name="match-attributes">
                            <xsl:with-param name="primary" select="subsequence($primary, 2)"/>
                            <xsl:with-param name="control" select="$control"/>
                            <xsl:with-param name="primaryCollecting" select="$primaryCollecting | $this"/>
                        </xsl:call-template>
                    </xsl:otherwise>
                </xsl:choose>

            </xsl:when>
            <xsl:otherwise>
                <xsl:if test="$primaryCollecting | $primary">
                    <xsl:apply-templates select="$primaryCollecting | $primary" mode="primary-only"/>
                </xsl:if>
                <xsl:if test="$control">
                    <xsl:apply-templates select="$control" mode="control-only"/>
                </xsl:if>
            </xsl:otherwise>
        </xsl:choose>

    </xsl:template>

    <!-- find matches between collections of elements in primary vs control -->
    <xsl:template name="match-children">
        <xsl:param name="primary" as="node()*"/>
        <xsl:param name="control" as="element()*"/>

        <xsl:variable name="this" select="$primary[1]" as="node()?"/>

        <xsl:choose>
            <xsl:when test="$primary and $control">
                <xsl:variable name="match" as="element()?">
                    <xsl:apply-templates select="$this" mode="find-match">
                        <xsl:with-param name="candidates" select="$control"/>
                    </xsl:apply-templates>
                </xsl:variable>

                <xsl:choose>
                    <xsl:when test="$match">
                        <xsl:apply-templates select="$this" mode="common">
                            <xsl:with-param name="control" select="$match"/>
                        </xsl:apply-templates>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:apply-templates select="$this" mode="primary-only"/>
                    </xsl:otherwise>
                </xsl:choose>
                <xsl:call-template name="match-children">
                    <xsl:with-param name="primary" select="subsequence($primary, 2)"/>
                    <xsl:with-param name="control" select="if (not($match)) then $control else remove($control, 1 + count(set:leading($control, $match)))"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:when test="$primary">
                <xsl:apply-templates select="$primary" mode="primary-only"/>
            </xsl:when>
            <xsl:when test="$control">
                <xsl:apply-templates select="$control" mode="control-only"/>
            </xsl:when>
        </xsl:choose>

    </xsl:template>

</xsl:stylesheet>

Применительно к этому документу (на основе вашего теста), результат PASS:

<test>
  <root>
    <tag name="AAA">
      <attr name="b" value="1"/>
      <attr name="c" value="2"/>
      <attr name="a" value="3"/>
    </tag>
    <tag name="BBB">
      <attr name="x" value="111"/>
      <attr name="z" value="222"/>
    </tag>
    <tag name="BBB">
      <attr name="x" value="333"/>
      <attr name="z" value="444"/>
    </tag>
  </root>
  <root>
    <tag name="AAA">
      <attr name="a" value="3"/>
      <attr name="b" value="1"/>
      <attr name="c" value="2"/>
    </tag>
    <tag name="BBB">
      <attr name="z" value="444"/>
      <attr name="x" value="333"/>
    </tag>
    <tag name="BBB">
      <attr name="x" value="111"/>
      <attr name="z" value="222"/>
    </tag>
  </root>
</test>

Во-первых, ваши примеры XML недопустимы, поскольку в них отсутствует корневой элемент. Я добавил корневой элемент. Это a.xml:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <tag name="AAA">
        <attr name="b" value="1"/>
        <attr name="c" value="2"/>
        <attr name="a" value="3"/>
    </tag>
    <tag name="BBB">
        <attr name="x" value="111"/>
        <attr name="z" value="222"/>
    </tag>
    <tag name="BBB">
        <attr name="x" value="333"/>
        <attr name="z" value="444"/>
    </tag>
</root>

А это b.xml:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <tag name="AAA">
        <attr name="a" value="3"/>
        <attr name="b" value="1"/>
        <attr name="c" value="2"/>
    </tag>
    <tag name="BBB">
        <attr name="z" value="444"/>
        <attr name="x" value="333"/>
    </tag>
    <tag name="BBB">
        <attr name="x" value="111"/>
        <attr name="z" value="222"/>
    </tag>
</root>

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

Чтобы объединить элементы одного и того же имени, вы должны игнорировать элементы, имя которых совпадает с именем предыдущего элемента, и взять оставшиеся. Это можно сделать на втором уровне элемента следующим Xpath:

*[not(@name = preceding-sibling::*/@name)]

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

<?xml version="1.0" encoding="WINDOWS-1252"?>
<root>
    <tag name="AAA">
        <attr name="a" value="3"/>
        <attr name="b" value="1"/>
        <attr name="c" value="2"/>
    </tag>
    <tag name="BBB">
        <attr name="x" value="111"/>
        <attr name="x" value="333"/>
        <attr name="z" value="222"/>
        <attr name="z" value="444"/>
    </tag>
</root>

Это сделает преобразование:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" encoding="WINDOWS-1252" omit-xml-declaration="no" indent="yes"/>
    <xsl:strip-space elements="*"/>
    <xsl:template match="/root">
        <xsl:copy>
                <xsl:copy-of select="@*"/>
                <xsl:for-each select="*[not(@name = preceding-sibling::*/@name)]">
                    <xsl:variable name="name" select="@name"/>
                    <xsl:copy>
                        <xsl:copy-of select="@*"/>
                        <xsl:for-each select="../*[@name = $name]/*">
                            <xsl:sort select="@name"/>
                            <xsl:sort select="@value"/>
                            <xsl:copy>
                                <xsl:copy-of select="@*"/>
                            </xsl:copy>
                        </xsl:for-each>
                    </xsl:copy>
                </xsl:for-each>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Вы запрашиваете сортировку на основе последовательности атрибутов в сортируемых элементах. Но ваш верхний уровень tag элементы здесь имеют только один атрибут: name, Если вы хотите несколько tag элементы с name="BBB" чтобы сортировать по-другому, вам нужно дать им разные ключи сортировки.

В вашем примере я бы попробовал что-то вроде select="concat(name(), @name, name(*[1]), *[1]/@name)" - но это очень мелкий ключ. Он использует значения от первого дочернего элемента во входных данных, но дочерние элементы могут смещать позицию во время процесса. Возможно, вы сможете (зная ваши данные лучше, чем я), рассчитать хороший ключ для каждого элемента за один проход, или вам может понадобиться всего несколько проходов.

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

Многим было бы странно иметь элементы XML с именами "tag" и / или "attr", поскольку это термины с конкретными значениями, уже содержащимися в XML - возможно, это способствовало попытке сортировки по "@*" вместо сортировки элементов?

Если ваша структура действительно похожа на ваш пример, гораздо более "XML-представление" будет выглядеть так:

<AAA b="1" c="2" a="3" />
<BBB x="111" z="222" />
<BBB x="333" z="444" />

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

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