【问题标题】:Open source command line tool for Linux to diff XML files ignoring element order用于 Linux 的开源命令行工具,用于区分 XML 文件,忽略元素顺序
【发布时间】:2012-08-16 19:45:26
【问题描述】:

是否有开源命令行工具(适用于 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>

但问题是元素&lt;tag name="BBB"&gt; 没有排序。它们只是按照输入的顺序输出。

我已经看过diffXmlxDiffXMLUnitxmlstarlet,但这些都不能解决问题;差异输出应该是人类可读的,例如就像使用diff时一样。

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

【问题讨论】:

  • 我知道它不是命令行工具,但如果你可以使用 X ;然后Meld 把手指放在鼻子里了 :) 否则 diff 也能发挥作用,不是吗?我不会复制 diff 的输出,但看起来还可以。
  • xmllint 可以规范化您的 xml,然后您可以使用普通 diff。
  • 嗯。在寻找 xmllint 的确切参数时,Google 向我发送了这个重复的问题:superuser.com/questions/79920/how-can-i-diff-two-xml-files
  • 感谢您的建议,我也已经看到并尝试了 xmllint,但对于我的示例,它似乎不起作用。规范化我的两个输入,输出结果和输入一​​样。

标签: xml sorting command-line open-source diff


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    首先,您的 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>
    

    您可以通过合并具有相同名称属性的兄弟并按标签名称和值排序来创建比较的规范形式。

    为了合并具有相同名称的兄弟元素,您必须忽略名称与前面的兄弟元素相同的元素并取剩余的元素。这可以通过以下 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>
    

    【讨论】:

      【解决方案3】:

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

      许多人会发现将 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 并使用常规差异。

      【讨论】:

        【解决方案4】:

        您必须编写自己的解释器来进行预处理。 XSLT 是一种方法……也许吧;我不是 XSLT 方面的专家,我不确定您是否可以使用它进行排序。

        这是一个快速而肮脏的 perl 脚本,它可以做你想做的事。请注意,使用真正的 XML 解析器要明智得多。我对任何一个都不熟悉,所以我让你接触我自己写它们的糟糕做法。注意 cmets;您已被警告。

        #!/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 技巧来运行每个输入的预处理器命令。它们将按照您指定的顺序排列。请注意,由于此问题要求的排序,内容可能位于意外位置。)

        为了满足您对“开源”“命令行工具”的要求,我特此在Beerware 许可证(BSD 2-clause,如果您认为值得,欢迎您购买我一个啤酒)。

        【讨论】:

          【解决方案5】:

          我也遇到过类似的问题,最终发现:https://superuser.com/questions/79920/how-can-i-diff-two-xml-files

          该帖子建议先进行规范的 xml 排序,然后再进行 diff。因为你在 linux 上,所以这应该很适合你。它在我的 mac 上对我有用,如果他们安装了 cygwin 之类的东西,它应该适用于 windows 上的人:

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

          【讨论】:

            【解决方案6】:

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2023-03-21
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-02-08
              • 2021-07-14
              • 2011-02-10
              • 1970-01-01
              相关资源
              最近更新 更多