比较Java中2个XML文档的最佳方法


198

我正在尝试编写一个应用程序的自动化测试,该测试基本上将自定义消息格式转换为XML消息并将其发送到另一端。我有一组很好的输入/输出消息对,所以我要做的就是发送输入消息并侦听另一端的XML消息。

当需要将实际输出与预期输出进行比较时,我遇到了一些问题。我的第一个想法只是对预期消息和实际消息进行字符串比较。这不能很好地工作,因为我们拥有的示例数据并不总是保持一致的格式,并且常常为XML名称空间使用不同的别名(有时根本不使用名称空间)。

我知道我可以解析两个字符串,然后遍历每个元素,然后自己比较它们,这并不是很难做到的,但是我感觉有一种更好的方法或可以利用的库。

因此,归根结底,问题是:

给定两个都包含有效XML的Java字符串,您将如何确定它们在语义上是否等效?如果您有办法确定差异是多少,您将获得加分。

Answers:


197

听起来像XMLUnit的工作

例:

public class SomeTest extends XMLTestCase {
  @Test
  public void test() {
    String xml1 = ...
    String xml2 = ...

    XMLUnit.setIgnoreWhitespace(true); // ignore whitespace differences

    // can also compare xml Documents, InputSources, Readers, Diffs
    assertXMLEqual(xml1, xml2);  // assertXMLEquals comes from XMLTestCase
  }
}

1
过去,我在使用XMLUNit时遇到了问题,它在使用XML API版本时过于烦躁,并且没有被证明是可靠的。自从我放弃XOM以来已经有一段时间了,所以也许从那时起它就被改进了。
skaffman's

63
对于XMLUnit的初学者,请注意,默认情况下,如果控件和测试文档的缩进/换行符不同,则myDiff.similar()将返回false。我希望从myDiff.identical()而不是从myDiff.similar()获得这种行为。包括XMLUnit.setIgnoreWhitespace(true); 在setUp方法中更改测试类中所有测试的行为,或在单个测试方法中使用它更改仅该测试的行为。
2012年

1
@Stew感谢您的评论,仅从XMLUnit开始,肯定会遇到此问题。+1
周杰伦

2
如果您要在github上使用XMLUnit 2尝试此操作,请完全重写2版本,所以此示例适用于SourceForge上的XMLUnit 1。另外,sourceforge页面上指出“仍将维护Java 1.x的XMLUnit”。
Yngvar Kristiansen

1
方法是assertXMLEqual与XMLAssert.java中的一样
user2818782 '18

36

下面将使用标准JDK库检查文档是否相等。

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setCoalescing(true);
dbf.setIgnoringElementContentWhitespace(true);
dbf.setIgnoringComments(true);
DocumentBuilder db = dbf.newDocumentBuilder();

文档doc1 = db.parse(new File(“ file1.xml”));
doc1.normalizeDocument();

文档doc2 = db.parse(new File(“ file2.xml”));
doc2.normalizeDocument();

Assert.assertTrue(doc1.isEqualNode(doc2));

normalize()可以确保没有周期(从技术上讲不会有周期)

上面的代码将要求元素中的空格必须相同,因为它保留并评估了空格。Java随附的标准XML解析器不允许您设置功能以提供规范版本,也不能理解xml:space是否会出现问题,那么您可能需要替换的XML解析器(例如xerces)或使用JDOM。


4
这对于没有名称空间或带有“规范化”名称空间前缀的XML完全适用。我怀疑如果一个XML是<ns1:a xmlns:ns1 =“ ns” />而另一个是<ns2:a xmlns:ns2 =“ ns” />是否有效
koppor 2013年

dbf.setIgnoringElementContentWhitespace(true)没有结果,在这种解决方案中,我希望<root> name </ root>与<root> name </ name>不相等(用两个空格填充),但是XMLUnit给出的结果相同在这种情况下(JDK8)
Miklos Krivan '16

对我来说,它不会忽略换行符,这是一个问题。
Flyout91

setIgnoringElementContentWhitespace(false)
Archimedes Trajano

28

Xom具有Canonicalizer实用程序,可将您的DOM转换为常规形式,然后您可以对其进行字符串化和比较。因此,无论空白不规则或属性排序如何,您都可以对文档进行定期的,可预测的比较。

这在具有专用可视化字符串比较器的IDE(例如Eclipse)中尤其有效。您可以直观地看到文档之间的语义差异。


21

XMLUnit的最新版本可以帮助断言两个XML相等。同样XMLUnit.setIgnoreWhitespace()XMLUnit.setIgnoreAttributeOrder()对于该案件可能是必要的。

请参阅下面的XML单元使用简单示例的工作代码。

import org.custommonkey.xmlunit.DetailedDiff;
import org.custommonkey.xmlunit.XMLUnit;
import org.junit.Assert;

public class TestXml {

    public static void main(String[] args) throws Exception {
        String result = "<abc             attr=\"value1\"                title=\"something\">            </abc>";
        // will be ok
        assertXMLEquals("<abc attr=\"value1\" title=\"something\"></abc>", result);
    }

    public static void assertXMLEquals(String expectedXML, String actualXML) throws Exception {
        XMLUnit.setIgnoreWhitespace(true);
        XMLUnit.setIgnoreAttributeOrder(true);

        DetailedDiff diff = new DetailedDiff(XMLUnit.compareXML(expectedXML, actualXML));

        List<?> allDifferences = diff.getAllDifferences();
        Assert.assertEquals("Differences found: "+ diff.toString(), 0, allDifferences.size());
    }

}

如果使用Maven,请将其添加到您的pom.xml

<dependency>
    <groupId>xmlunit</groupId>
    <artifactId>xmlunit</artifactId>
    <version>1.4</version>
</dependency>

对于需要从静态方法进行比较的人们来说,这是完美的选择。
安迪B

这是完美的答案。谢谢..但是我需要忽略不存在的节点。因为我不想在结果输出中看到这样的输出:子节点“ null”的预期存在,但是............我该怎么做?问候。@acdcjunior
limonik '16

1
XMLUnit.setIgnoreAttributeOrder(true); 不起作用。如果某些节点的顺序不同,则比较将失败。
Bevor

[更新]此解决方案有效:stackoverflow.com/questions/33695041/…–
Bevor

您确实意识到“ IgnoreAttributeOrder”意味着忽略属性顺序而不是忽略节点顺序,对吗?
acdcjunior

7

谢谢,我扩展了这个,尝试这个...

import java.io.ByteArrayInputStream;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

public class XmlDiff 
{
    private boolean nodeTypeDiff = true;
    private boolean nodeValueDiff = true;

    public boolean diff( String xml1, String xml2, List<String> diffs ) throws Exception
    {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        dbf.setCoalescing(true);
        dbf.setIgnoringElementContentWhitespace(true);
        dbf.setIgnoringComments(true);
        DocumentBuilder db = dbf.newDocumentBuilder();


        Document doc1 = db.parse(new ByteArrayInputStream(xml1.getBytes()));
        Document doc2 = db.parse(new ByteArrayInputStream(xml2.getBytes()));

        doc1.normalizeDocument();
        doc2.normalizeDocument();

        return diff( doc1, doc2, diffs );

    }

    /**
     * Diff 2 nodes and put the diffs in the list 
     */
    public boolean diff( Node node1, Node node2, List<String> diffs ) throws Exception
    {
        if( diffNodeExists( node1, node2, diffs ) )
        {
            return true;
        }

        if( nodeTypeDiff )
        {
            diffNodeType(node1, node2, diffs );
        }

        if( nodeValueDiff )
        {
            diffNodeValue(node1, node2, diffs );
        }


        System.out.println(node1.getNodeName() + "/" + node2.getNodeName());

        diffAttributes( node1, node2, diffs );
        diffNodes( node1, node2, diffs );

        return diffs.size() > 0;
    }

    /**
     * Diff the nodes
     */
    public boolean diffNodes( Node node1, Node node2, List<String> diffs ) throws Exception
    {
        //Sort by Name
        Map<String,Node> children1 = new LinkedHashMap<String,Node>();      
        for( Node child1 = node1.getFirstChild(); child1 != null; child1 = child1.getNextSibling() )
        {
            children1.put( child1.getNodeName(), child1 );
        }

        //Sort by Name
        Map<String,Node> children2 = new LinkedHashMap<String,Node>();      
        for( Node child2 = node2.getFirstChild(); child2!= null; child2 = child2.getNextSibling() )
        {
            children2.put( child2.getNodeName(), child2 );
        }

        //Diff all the children1
        for( Node child1 : children1.values() )
        {
            Node child2 = children2.remove( child1.getNodeName() );
            diff( child1, child2, diffs );
        }

        //Diff all the children2 left over
        for( Node child2 : children2.values() )
        {
            Node child1 = children1.get( child2.getNodeName() );
            diff( child1, child2, diffs );
        }

        return diffs.size() > 0;
    }


    /**
     * Diff the nodes
     */
    public boolean diffAttributes( Node node1, Node node2, List<String> diffs ) throws Exception
    {        
        //Sort by Name
        NamedNodeMap nodeMap1 = node1.getAttributes();
        Map<String,Node> attributes1 = new LinkedHashMap<String,Node>();        
        for( int index = 0; nodeMap1 != null && index < nodeMap1.getLength(); index++ )
        {
            attributes1.put( nodeMap1.item(index).getNodeName(), nodeMap1.item(index) );
        }

        //Sort by Name
        NamedNodeMap nodeMap2 = node2.getAttributes();
        Map<String,Node> attributes2 = new LinkedHashMap<String,Node>();        
        for( int index = 0; nodeMap2 != null && index < nodeMap2.getLength(); index++ )
        {
            attributes2.put( nodeMap2.item(index).getNodeName(), nodeMap2.item(index) );

        }

        //Diff all the attributes1
        for( Node attribute1 : attributes1.values() )
        {
            Node attribute2 = attributes2.remove( attribute1.getNodeName() );
            diff( attribute1, attribute2, diffs );
        }

        //Diff all the attributes2 left over
        for( Node attribute2 : attributes2.values() )
        {
            Node attribute1 = attributes1.get( attribute2.getNodeName() );
            diff( attribute1, attribute2, diffs );
        }

        return diffs.size() > 0;
    }
    /**
     * Check that the nodes exist
     */
    public boolean diffNodeExists( Node node1, Node node2, List<String> diffs ) throws Exception
    {
        if( node1 == null && node2 == null )
        {
            diffs.add( getPath(node2) + ":node " + node1 + "!=" + node2 + "\n" );
            return true;
        }

        if( node1 == null && node2 != null )
        {
            diffs.add( getPath(node2) + ":node " + node1 + "!=" + node2.getNodeName() );
            return true;
        }

        if( node1 != null && node2 == null )
        {
            diffs.add( getPath(node1) + ":node " + node1.getNodeName() + "!=" + node2 );
            return true;
        }

        return false;
    }

    /**
     * Diff the Node Type
     */
    public boolean diffNodeType( Node node1, Node node2, List<String> diffs ) throws Exception
    {       
        if( node1.getNodeType() != node2.getNodeType() ) 
        {
            diffs.add( getPath(node1) + ":type " + node1.getNodeType() + "!=" + node2.getNodeType() );
            return true;
        }

        return false;
    }

    /**
     * Diff the Node Value
     */
    public boolean diffNodeValue( Node node1, Node node2, List<String> diffs ) throws Exception
    {       
        if( node1.getNodeValue() == null && node2.getNodeValue() == null )
        {
            return false;
        }

        if( node1.getNodeValue() == null && node2.getNodeValue() != null )
        {
            diffs.add( getPath(node1) + ":type " + node1 + "!=" + node2.getNodeValue() );
            return true;
        }

        if( node1.getNodeValue() != null && node2.getNodeValue() == null )
        {
            diffs.add( getPath(node1) + ":type " + node1.getNodeValue() + "!=" + node2 );
            return true;
        }

        if( !node1.getNodeValue().equals( node2.getNodeValue() ) )
        {
            diffs.add( getPath(node1) + ":type " + node1.getNodeValue() + "!=" + node2.getNodeValue() );
            return true;
        }

        return false;
    }


    /**
     * Get the node path
     */
    public String getPath( Node node )
    {
        StringBuilder path = new StringBuilder();

        do
        {           
            path.insert(0, node.getNodeName() );
            path.insert( 0, "/" );
        }
        while( ( node = node.getParentNode() ) != null );

        return path.toString();
    }
}

3
很晚了,但只是想指出这段代码有一个错误:在diffNodes()中,未引用node2-第二个循环错误地重用了node1(我编辑了代码以解决此问题)。同样,它有1个限制:由于对子映射进行键控的方式,此diff不支持元素名称不唯一(即包含可重复子元素的元素)的情况。
aberrant80

7

Tom的答案为基础,这是一个使用XMLUnit v2的示例。

它使用这些Maven依赖项

    <dependency>
        <groupId>org.xmlunit</groupId>
        <artifactId>xmlunit-core</artifactId>
        <version>2.0.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.xmlunit</groupId>
        <artifactId>xmlunit-matchers</artifactId>
        <version>2.0.0</version>
        <scope>test</scope>
    </dependency>

..这是测试代码

import static org.junit.Assert.assertThat;
import static org.xmlunit.matchers.CompareMatcher.isIdenticalTo;
import org.xmlunit.builder.Input;
import org.xmlunit.input.WhitespaceStrippedSource;

public class SomeTest extends XMLTestCase {
    @Test
    public void test() {
        String result = "<root></root>";
        String expected = "<root>  </root>";

        // ignore whitespace differences
        // https://github.com/xmlunit/user-guide/wiki/Providing-Input-to-XMLUnit#whitespacestrippedsource
        assertThat(result, isIdenticalTo(new WhitespaceStrippedSource(Input.from(expected).build())));

        assertThat(result, isIdenticalTo(Input.from(expected).build())); // will fail due to whitespace differences
    }
}

概述此内容的文档是https://github.com/xmlunit/xmlunit#comparing-two-documents


3

skaffman似乎给出了很好的答案。

另一种方法可能是使用诸如xmlstarlet(http://xmlstar.sourceforge.net/)之类的命令行实用程序来格式化XML ,然后格式化两个字符串,然后使用任何diff实用程序(库)来对结果输出文件进行差异化。当名称空间出现问题时,我不知道这是否是一个好的解决方案。



2

我正在使用Altova DiffDog,它具有用于在结构上比较XML文件的选项(忽略字符串数据)。

这意味着(如果选中“忽略文本”选项):

<foo a="xxx" b="xxx">xxx</foo>

<foo b="yyy" a="yyy">yyy</foo> 

就结构上的平等而言,它们是平等的。如果您的示例文件的数据不同,但结构不同,这将很方便!


3
唯一的缺点是它不是免费的(专业版许可证为99欧元),可试用30天。
Pimin Konstantin Kefaloukos,2010年

2
我发现只有该实用程序(altova.com/diffdog/diff-merge-tool.html); 很高兴有一个图书馆。
dma_k 2010年

1

这将比较全字符串XML(途中对其进行格式化)。它使您可以轻松地使用IDE(IntelliJ,Eclipse),只需单击一下即可直观地看到XML文件中的差异。

import org.apache.xml.security.c14n.CanonicalizationException;
import org.apache.xml.security.c14n.Canonicalizer;
import org.apache.xml.security.c14n.InvalidCanonicalizerException;
import org.w3c.dom.Element;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.io.StringReader;

import static org.apache.xml.security.Init.init;
import static org.junit.Assert.assertEquals;

public class XmlUtils {
    static {
        init();
    }

    public static String toCanonicalXml(String xml) throws InvalidCanonicalizerException, ParserConfigurationException, SAXException, CanonicalizationException, IOException {
        Canonicalizer canon = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS);
        byte canonXmlBytes[] = canon.canonicalize(xml.getBytes());
        return new String(canonXmlBytes);
    }

    public static String prettyFormat(String input) throws TransformerException, ParserConfigurationException, IOException, SAXException, InstantiationException, IllegalAccessException, ClassNotFoundException {
        InputSource src = new InputSource(new StringReader(input));
        Element document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src).getDocumentElement();
        Boolean keepDeclaration = input.startsWith("<?xml");
        DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
        DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
        LSSerializer writer = impl.createLSSerializer();
        writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);
        writer.getDomConfig().setParameter("xml-declaration", keepDeclaration);
        return writer.writeToString(document);
    }

    public static void assertXMLEqual(String expected, String actual) throws ParserConfigurationException, IOException, SAXException, CanonicalizationException, InvalidCanonicalizerException, TransformerException, IllegalAccessException, ClassNotFoundException, InstantiationException {
        String canonicalExpected = prettyFormat(toCanonicalXml(expected));
        String canonicalActual = prettyFormat(toCanonicalXml(actual));
        assertEquals(canonicalExpected, canonicalActual);
    }
}

我更喜欢XmlUnit,因为客户端代码(测试代码)更干净。


1
在我现在进行的两个测试中,使用相同的XML和不同的XML可以很好地解决这个问题。使用IntelliJ diff,可以很容易地发现所比较XML的差异。
Yngvar Kristiansen

1
顺便说一句,如果您使用Maven,则需要此依赖项:<dependency> <groupId> org.apache.santuario </ groupId> <artifactId> xmlsec </ artifactId> <version> 2.0.6 </ version> </依赖>
Yngvar Kristiansen

1

下面的代码对我有用

String xml1 = ...
String xml2 = ...
XMLUnit.setIgnoreWhitespace(true);
XMLUnit.setIgnoreAttributeOrder(true);
XMLAssert.assertXMLEqual(actualxml, xmlInDb);

1
有什么背景吗?图书馆参考?

0

在Java应用程序中使用JExamXML

    import com.a7soft.examxml.ExamXML;
    import com.a7soft.examxml.Options;

       .................

       // Reads two XML files into two strings
       String s1 = readFile("orders1.xml");
       String s2 = readFile("orders.xml");

       // Loads options saved in a property file
       Options.loadOptions("options");

       // Compares two Strings representing XML entities
       System.out.println( ExamXML.compareXMLString( s1, s2 ) );

0

我需要与主要问题中要求的功能相同的功能。由于不允许使用任何第三方库,因此我基于@Archimedes Trajano解决方案创建了自己的解决方案。

以下是我的解决方案。

import java.io.ByteArrayInputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.junit.Assert;
import org.w3c.dom.Document;

/**
 * Asserts for asserting XML strings.
 */
public final class AssertXml {

    private AssertXml() {
    }

    private static Pattern NAMESPACE_PATTERN = Pattern.compile("xmlns:(ns\\d+)=\"(.*?)\"");

    /**
     * Asserts that two XML are of identical content (namespace aliases are ignored).
     * 
     * @param expectedXml expected XML
     * @param actualXml actual XML
     * @throws Exception thrown if XML parsing fails
     */
    public static void assertEqualXmls(String expectedXml, String actualXml) throws Exception {
        // Find all namespace mappings
        Map<String, String> fullnamespace2newAlias = new HashMap<String, String>();
        generateNewAliasesForNamespacesFromXml(expectedXml, fullnamespace2newAlias);
        generateNewAliasesForNamespacesFromXml(actualXml, fullnamespace2newAlias);

        for (Entry<String, String> entry : fullnamespace2newAlias.entrySet()) {
            String newAlias = entry.getValue();
            String namespace = entry.getKey();
            Pattern nsReplacePattern = Pattern.compile("xmlns:(ns\\d+)=\"" + namespace + "\"");
            expectedXml = transletaNamespaceAliasesToNewAlias(expectedXml, newAlias, nsReplacePattern);
            actualXml = transletaNamespaceAliasesToNewAlias(actualXml, newAlias, nsReplacePattern);
        }

        // nomralize namespaces accoring to given mapping

        DocumentBuilder db = initDocumentParserFactory();

        Document expectedDocuemnt = db.parse(new ByteArrayInputStream(expectedXml.getBytes(Charset.forName("UTF-8"))));
        expectedDocuemnt.normalizeDocument();

        Document actualDocument = db.parse(new ByteArrayInputStream(actualXml.getBytes(Charset.forName("UTF-8"))));
        actualDocument.normalizeDocument();

        if (!expectedDocuemnt.isEqualNode(actualDocument)) {
            Assert.assertEquals(expectedXml, actualXml); //just to better visualize the diffeences i.e. in eclipse
        }
    }


    private static DocumentBuilder initDocumentParserFactory() throws ParserConfigurationException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(false);
        dbf.setCoalescing(true);
        dbf.setIgnoringElementContentWhitespace(true);
        dbf.setIgnoringComments(true);
        DocumentBuilder db = dbf.newDocumentBuilder();
        return db;
    }

    private static String transletaNamespaceAliasesToNewAlias(String xml, String newAlias, Pattern namespacePattern) {
        Matcher nsMatcherExp = namespacePattern.matcher(xml);
        if (nsMatcherExp.find()) {
            xml = xml.replaceAll(nsMatcherExp.group(1) + "[:]", newAlias + ":");
            xml = xml.replaceAll(nsMatcherExp.group(1) + "=", newAlias + "=");
        }
        return xml;
    }

    private static void generateNewAliasesForNamespacesFromXml(String xml, Map<String, String> fullnamespace2newAlias) {
        Matcher nsMatcher = NAMESPACE_PATTERN.matcher(xml);
        while (nsMatcher.find()) {
            if (!fullnamespace2newAlias.containsKey(nsMatcher.group(2))) {
                fullnamespace2newAlias.put(nsMatcher.group(2), "nsTr" + (fullnamespace2newAlias.size() + 1));
            }
        }
    }

}

它比较两个XML字符串,并通过将它们转换为两个输入字符串中的唯一值来处理任何不匹配的名称空间映射。

可以进行微调,即在转换名称空间的情况下。但是对于我的要求就可以了。


-2

因为您说的是“语义上等效”,所以我想您的意思是您要做的不只是从字面上验证xml输出是否等于(字符串),而且还需要类似

<foo>这里的一些东西</ foo> </ code>

<foo>一些东西</ foo> </ code>

等同阅读。最终,与要从中重构消息的任何对象上定义“语义等效”的问题有关。只需根据消息构建该对象,然后使用自定义的equals()定义您要查找的内容。


4
不是答案,而是问题。
卡托奇2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.