12

是否有开源命令行工具(适用于 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, XMLUnitxmlstarlet但是这些都不能解决问题;diff 输出应该是人类可读的,例如使用diff.

关于如何解决排序或忽略元素顺序差异的任何提示?谢谢!

4

6 回答 6

1

我有一个类似的问题,我最终发现:https ://superuser.com/questions/79920/how-can-i-diff-two-xml-files

那篇文章建议做一个规范的 xml 排序然后做一个差异。因为你在 linux 上,所以这应该很适合你。它在我的 mac 上对我有用,如果他们安装了 cygwin 之类的东西,它应该适用于 windows 上的人:

$ xmllint --c14n a.xml > sortedA.xml
$ xmllint --c14n b.xml > sortedB.xml
$ diff sortedA.xml sortedB.xml
于 2016-12-02T17:20:59.313 回答
0

First your XML examples are not valid, because they lack a root element. I added a root element. This is 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>

And this is 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>

You can create a canonical form for the comparison by merging the siblings with the same name attribute and sorting by the tag name and the value.

In order to merge the sibling elements with the same name you have to ignore the elements which name is the same like a preceding sibling and take the remaining. This can be done on the second element level by the following Xpath:

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

You have to take the name of those elements in order to select all the child elements which have a parent with this name. After that you have to sort by name and value. This makes it possible to transform both files into this canonical form:

<?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>

This will do the transformation:

<?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>
于 2012-09-25T16:51:56.453 回答
0

您必须编写自己的解释器来进行预处理。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

(我使用-sand-q标志是明确的。您可以使用 gvimdiff 或您喜欢的任何其他实用程序或标志。注意它通过文件描述符识别文件;那是因为我使用 bash 技巧在每个输入上运行预处理器命令。他们'将按照您指定的顺序。请注意,由于此问题要求的排序,内容可能位于意外位置。)

为了满足您对“开源”“命令行工具”的要求,我特此根据Beerware许可证(BSD 2-clause,如果您认为值得,欢迎您给我买啤酒)将此代码作为开源发布。

于 2014-02-14T02:58:53.570 回答
0

您正在请求根据正在排序的元素中的属性序列进行排序。但是您在这里的顶级tag元素只有一个属性:name. 如果您希望多个tag元素name="BBB"以不同的方式排序,则需要为它们提供不同的排序键。

在你的例子中,我会尝试类似的东西select="concat(name(), @name, name(*[1]), *[1]/@name)"——但这是一个非常浅的键。它使用输入中第一个孩子的值,但孩子可能会在此过程中移动位置。您可能能够(比我更了解您的数据)在一次传递中为每个元素计算一个好的键,或者您可能只需要几次传递。

于 2012-08-21T18:44:12.520 回答
0

如果你想把它带到任意程度,你可以实现一些东西,将两棵树一起走,并决定两个文档之间哪些元素“匹配”。这会让你以任何你想要的方式实现匹配逻辑。这是 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>
于 2017-01-18T19:07:43.063 回答
-1

从您的示例来看,您似乎只关心元素内的重新排序元素,而不关心元素本身的重新排序。如果是这样,那么(正如之前的受访者所说)您需要使用排序,但在元素上,而不是元素或属性上。

许多人会发现将 XML 元素命名为“tag”和/或“attr”会让人感到困惑,因为这些术语已经在 XML 中具有特定含义——这可能有助于尝试按“@*”而不是对元素进行排序?

如果你的结构真的和你的例子一样,一个更“XML-ish”的表示将是:

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

更加紧凑,避免了术语冲突,并根据定义使属性与顺序无关——这意味着任何现成的 XML diff 实用程序都将获得您想要的效果,或者您可以转换为规范的 XML 和使用常规差异。

于 2013-11-27T14:47:34.580 回答