在 Java 中合并两个 XML 文件

Posted

技术标签:

【中文标题】在 Java 中合并两个 XML 文件【英文标题】:Merge Two XML Files in Java 【发布时间】:2010-10-13 12:06:50 【问题描述】:

我有两个结构相似的 XML 文件,我希望将它们合并到一个文件中。 目前我正在使用我在本教程中遇到的EL4J XML Merge。 但是它并没有像我期望的那样合并,例如主要问题是它没有将两个文件中的合并到一个元素中,即包含 1、2、3 和 4 的元素。 相反,它只是丢弃 1 和 2 或 3 和 4,具体取决于首先合并的文件。

因此,我将感谢任何有 XML Merge 经验的人,如果他们能告诉我我可能做错了什么,或者是否有人知道一个好的 XML API for Java,它能够根据我的需要合并文件?

非常感谢您提前提供的帮助

编辑:

确实可以提供一些关于这样做的好建议,因此增加了赏金。我尝试了 jdigital 的建议,但仍然遇到 XML 合并问题。

以下是我尝试合并的 XML 文件的结构类型示例。

<run xmloutputversion="1.02">
    <info type="a" />
    <debugging level="0" />
    <host starttime="1237144741" endtime="1237144751">
        <status state="up" reason="somereason"/>
        <something avalue="test" test="alpha" />
        <target>
            <system name="computer" />
        </target>
        <results>
            <result id="1">
                <state value="test" />
                <service value="gamma" />
            </result>
            <result id="2">
                <state value="test4" />
                <service value="gamma4" />
            </result>
        </results>
        <times something="0" />
    </host>
    <runstats>
        <finished time="1237144751" timestr="Sun Mar 15 19:19:11 2009"/>
        <result total="0" />
    </runstats>
</run>

<run xmloutputversion="1.02">
    <info type="b" />
    <debugging level="0" />
    <host starttime="1237144741" endtime="1237144751">
        <status state="down" reason="somereason"/>
        <something avalue="test" test="alpha" />
        <target>
            <system name="computer" />
        </target>
        <results>
            <result id="3">
                <state value="testagain" />
                <service value="gamma2" />
            </result>
            <result id="4">
                <state value="testagain4" />
                <service value="gamma4" />
            </result>
        </results>
        <times something="0" />
    </host>
    <runstats>
        <finished time="1237144751" timestr="Sun Mar 15 19:19:11 2009"/>
        <result total="0" />
    </runstats>
</run>

预期输出

<run xmloutputversion="1.02">
    <info type="a" />
    <debugging level="0" />
    <host starttime="1237144741" endtime="1237144751">
        <status state="down" reason="somereason"/>
        <status state="up" reason="somereason"/>
        <something avalue="test" test="alpha" />
        <target>
            <system name="computer" />
        </target>
        <results>
            <result id="1">
                <state value="test" />
                <service value="gamma" />
            </result>
            <result id="2">
                <state value="test4" />
                <service value="gamma4" />
            </result>
            <result id="3">
                <state value="testagain" />
                <service value="gamma2" />
            </result>
            <result id="4">
                <state value="testagain4" />
                <service value="gamma4" />
            </result>
        </results>
        <times something="0" />
    </host>
    <runstats>
        <finished time="1237144751" timestr="Sun Mar 15 19:19:11 2009"/>
        <result total="0" />
    </runstats>
</run>

【问题讨论】:

你能添加想要的结果吗? 已经添加了预期的输出,将结果添加到结果节点是最关键的事情。 【参考方案1】:

您也许可以编写一个 java 应用程序,将 XML 文档反序列化为对象,然后以编程方式将各个对象“合并”到一个集合中。然后,您可以将集合对象序列化回 XML 文件,并将所有内容“合并”。

JAXB API 有一些工具可以将 XML 文档/模式转换为 java 类。 “xjc”工具可能能够做到这一点,但我不记得是否可以直接从 XML 文档创建类,或者是否必须先生成模式。有一些工具可以从 XML 文档生成模式。

希望这会有所帮助...不确定这是否是您要找的。​​p>

【讨论】:

感谢您的回答,这并不是我真正想到的,但如果没有人提出另一种解决方案,我会保留作为一种选择。【参考方案2】:

我查看了引用的链接;奇怪的是 XMLMerge 不能按预期工作。你的例子似乎很简单。您是否阅读了标题为Using XPath declarations with XmlMerge 的部分?使用该示例,尝试为结果设置 XPath 并将其设置为合并。如果我正确阅读了文档,它看起来像这样:

XPath.resultsNode=results
action.resultsNode=MERGE

【讨论】:

我已经尝试过了,但不幸的是它仍然无法正常工作,我将四处看看是否可以找到一些更好的文档。【参考方案3】:

如果您明确说明您有兴趣实现的结果,这可能会有所帮助。这是你要求的吗?

文件 A:

<root>
  <a/>
  <b>
    <c/>
  </b>
</root>

文档 B:

<root>
  <d/>
</root>

合并结果:

<root>
  <a/>
  <b>
    <c/>
  </b>
  <d/>
</root>

您是否担心缩放大型文档?

在 Java 中实现此功能的最简单方法是使用流式 XML 解析器(google for 'java StAX')。如果您使用 javax.xml.stream 库,您会发现 XMLEventWriter 有一个方便的方法 XMLEventWriter#add(XMLEvent)。您所要做的就是遍历每个文档中的***元素,并使用此方法将它们添加到您的编写器以生成合并结果。唯一时髦的部分是实现仅考虑(仅调用“添加”)***节点的读取器逻辑。

如果您需要提示,我最近实现了此方法。

【讨论】:

【参考方案4】:

您是否考虑过不去“正确”解析 XML,而只是将文件视为大长字符串并使用诸如哈希映射和正则表达式之类的无聊的旧东西......?这可能是其中带有 X 的花哨的首字母缩写词只会让工作变得比它需要的更复杂的情况之一。

显然,这在一定程度上取决于您在进行合并时实际需要解析多少数据。但从事物的声音来看,答案并不多。

【讨论】:

你能保证直接字符串会重新生成正确的 XML 吗?您愿意对该解决方案进行多少验证和测试,而不是使用负责该解决方案的 X 工具的“麻烦”? 如果给出的示例文件具有代表性,并且要求符合规定,那么我认为,是的,我可以。如果问题有一些隐藏的部分(不同格式的文件,需要大量验证),那么最实用的可能是“正确”解析。【参考方案5】:

除了使用 Stax(这很有意义)之外,使用 StaxMate (http://staxmate.codehaus.org/Tutorial) 可能会更容易。只需创建 2 个 SMInputCursors,如果需要,还可以创建子光标。然后是带有 2 个游标的典型合并排序。类似于以递归下降的方式遍历 DOM 文档。

【讨论】:

给定的 URL (staxmate.codehaus.org) 似乎需要身份验证。请您验证并更新链接。 对,Codehaus 确实不幸关闭了。项目已移至github.com/FasterXML/StaxMate。感谢您指出。【参考方案6】:

不是很优雅,但您可以使用 DOM 解析器和 XPath 来做到这一点:

public class MergeXmlDemo 

  public static void main(String[] args) throws Exception 
    // proper error/exception handling omitted for brevity
    File file1 = new File("merge1.xml");
    File file2 = new File("merge2.xml");
    Document doc = merge("/run/host/results", file1, file2);
    print(doc);
  

  private static Document merge(String expression,
      File... files) throws Exception 
    XPathFactory xPathFactory = XPathFactory.newInstance();
    XPath xpath = xPathFactory.newXPath();
    XPathExpression compiledExpression = xpath
        .compile(expression);
    return merge(compiledExpression, files);
  

  private static Document merge(XPathExpression expression,
      File... files) throws Exception 
    DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory
        .newInstance();
    docBuilderFactory
        .setIgnoringElementContentWhitespace(true);
    DocumentBuilder docBuilder = docBuilderFactory
        .newDocumentBuilder();
    Document base = docBuilder.parse(files[0]);

    Node results = (Node) expression.evaluate(base,
        XPathConstants.NODE);
    if (results == null) 
      throw new IOException(files[0]
          + ": expression does not evaluate to node");
    

    for (int i = 1; i < files.length; i++) 
      Document merge = docBuilder.parse(files[i]);
      Node nextResults = (Node) expression.evaluate(merge,
          XPathConstants.NODE);
      while (nextResults.hasChildNodes()) 
        Node kid = nextResults.getFirstChild();
        nextResults.removeChild(kid);
        kid = base.importNode(kid, true);
        results.appendChild(kid);
      
    

    return base;
  

  private static void print(Document doc) throws Exception 
    TransformerFactory transformerFactory = TransformerFactory
        .newInstance();
    Transformer transformer = transformerFactory
        .newTransformer();
    DOMSource source = new DOMSource(doc);
    Result result = new StreamResult(System.out);
    transformer.transform(source, result);
  


这假设您可以同时在 RAM 中保存至少两个文档。

【讨论】:

这看起来很有前途,但更动态会更好。你有什么好的资源可以阅读更多关于 DOM 解析器和 XPath 的信息吗? devWorks上有一个不错的教程:ibm.com/developerworks/library/x-javaxpathapi.html 如果我需要传递整个 xml 文件而不是“/run/host/results”怎么办?只需要 file1.xml 和 file2.xml【参考方案7】:

所以,您只对合并“结果”元素感兴趣?其他一切都被忽略了吗? input0 有一个 并且 input1 有一个 并且预期结果有一个 的事实似乎表明了这一点。

如果您不担心缩放并且想快速解决此问题,那么我建议您编写一段特定于问题的代码,使用 JDOM 之类的简单库来考虑输入并编写输出结果。

尝试编写一个足够“智能”的通用工具来处理所有可能的合并情况将非常耗时 - 您必须公开配置功能来定义合并规则。如果您确切地知道您的数据将是什么样子并且您确切地知道需要如何执行合并,那么我想您的算法将遍历每个 XML 输入并写入单个 XML 输出。

【讨论】:

使用两个 XML 文件有点难以说清楚,我可能需要发布一些示例,但重要的是某些组(例如节点和目标)将适当地合并或添加新元素。但是其他的东西,比如跑步统计数据,可以作为一个单独的组。【参考方案8】:

您可以尝试Dom4J,它提供了一种使用 XPath 查询提取信息的非常好的方法,并且还允许您非常轻松地编写 XML。您只需要使用 API 一段时间即可完成您的工作

【讨论】:

【参考方案9】:

感谢大家的建议,不幸的是,建议的方法最终都不是合适的,因为我需要对结构的不同节点合并的方式制定规则。

所以我所做的是获取与我正在合并的 XML 文件相关的 DTD,并从中创建许多反映结构的类。 从这里我使用XStream 将 XML 文件反序列化回类。

通过这种方式,我注释了我的类,使其成为使用分配有注释的规则和一些反射的组合的过程,以便合并对象而不是合并实际的 XML 结构。

如果有人对在这种情况下合并 Nmap XML 文件的代码感兴趣,请参阅http://fluxnetworks.co.uk/NmapXMLMerge.tar.gz 代码并不完美,我承认它不是非常灵活,但它确实有效。我打算重新实现系统,当我有空闲时间时,它会自动解析 DTD。

【讨论】:

【参考方案10】:

我使用 XSLT 来合并 XML 文件。它允许我调整合并操作以将内容拼凑在一起或在特定级别合并。它需要更多的工作(并且 XSLT 语法有点特殊)但超级灵活。这里有一些你需要的东西

a) 包含一个附加文件 b) 1:1 复制原文件 c)设计你的合并点,有或没有重复避免

a) 一开始我有

<xsl:param name="mDocName">yoursecondfile.xml</xsl:param>
<xsl:variable name="mDoc" select="document($mDocName)" />

这允许使用 $mDoc 指向第二个文件

b) 1:1 复制源代码树的说明是 2 个模板:

<!-- Copy everything including attributes as default action -->
<xsl:template match="*">
    <xsl:element name="name()">
         <xsl:apply-templates select="@*" />
        <xsl:apply-templates />
    </xsl:element>
</xsl:template>

<xsl:template match="@*">
    <xsl:attribute name="name()"><xsl:value-of select="." /></xsl:attribute>
</xsl:template>

您将获得第一个源文件的 1:1 副本。适用于任何类型的 XML。合并部分是文件特定的。假设您有具有事件 ID 属性的事件元素。您不希望有重复的 ID。模板如下所示:

 <xsl:template match="events">
    <xsl:variable name="allEvents" select="descendant::*" />
    <events>
        <!-- copies all events from the first file -->
        <xsl:apply-templates />
        <!-- Merge the new events in. You need to adjust the select clause -->
        <xsl:for-each select="$mDoc/logbook/server/events/event">
            <xsl:variable name="curID" select="@id" />
            <xsl:if test="not ($allEvents[@id=$curID]/@id = $curID)">
                <xsl:element name="event">
                    <xsl:apply-templates select="@*" />
                    <xsl:apply-templates />
                </xsl:element>
            </xsl:if>
        </xsl:for-each>
    </properties>
</xsl:template>

当然,您可以比较标签名称等其他内容。合并的深度也取决于您。如果您没有要比较的键,则构造会变得更容易,例如日志:

 <xsl:template match="logs">
     <xsl:element name="logs">
          <xsl:apply-templates select="@*" />
          <xsl:apply-templates />
          <xsl:apply-templates select="$mDoc/logbook/server/logs/log" />
    </xsl:element>

要在 Java 中运行 XSLT,请使用:

    Source xmlSource = new StreamSource(xmlFile);
    Source xsltSource = new StreamSource(xsltFile);
    Result xmlResult = new StreamResult(resultFile);
    TransformerFactory transFact = TransformerFactory.newInstance();
    Transformer trans = transFact.newTransformer(xsltSource);
    // Load Parameters if we have any
    if (ParameterMap != null) 
       for (Entry<String, String> curParam : ParameterMap.entrySet()) 
            trans.setParameter(curParam.getKey(), curParam.getValue());
       
    
    trans.transform(xmlSource, xmlResult);

或者您下载Saxon SAX Parser 并从命令行执行(Linux shell 示例):

#!/bin/bash
notify-send -t 500 -u low -i gtk-dialog-info "Transforming $1 with $2 into $3 ..."
# That's actually the only relevant line below
java -cp saxon9he.jar net.sf.saxon.Transform -t -s:$1 -xsl:$2 -o:$3
notify-send -t 1000 -u low -i gtk-dialog-info "Extraction into $3 done!"

YMMV

【讨论】:

你将如何在代码中实现它?我不太了解 XSLT,但我不知道如何执行此 XSLT。 源 xmlSource = new StreamSource(xmlFile);源 xsltSource = new StreamSource(xsltFile);结果 xmlResult = new StreamResult(resultFile); TransformerFactory transFact = TransformerFactory.newInstance();变压器 trans = transFact.newTransformer(xsltSource); // 如果我们有任何参数,则加载参数 if (ParameterMap != null) for (Entry curParam : ParameterMap.entrySet()) trans.setParameter(curParam.getKey(), curParam.getValue()); trans.transform(xmlSource, xmlResult);【参考方案11】:

这就是使用 XML Merge 的样子:

action.default=MERGE

xpath.info=/run/info
action.info=PRESERVE

xpath.result=/run/host/results/result
action.result=MERGE
matcher.result=ID

您必须为 //result 节点设置 ID 匹配器并为 //info 节点设置 PRESERVE 操作。还要注意 .properties XML Merge 使用区分大小写 - 您必须在 .properties 中使用“xpath”而不是“XPath”。

不要忘记像这样定义-config参数:

java -cp lib\xmlmerge-full.jar; ch.elca.el4j.services.xmlmerge.tool.XmlMergeTool -config xmlmerge.properties example1.xml example2.xml 

【讨论】:

【参考方案12】:

有时您只需将 XML 文件合并为一个,例如具有类似结构的文件,如下所示:

文件xml1

<root>
    <level1>
        ...
    </level1>
    <!--many records-->
    <level1>
        ...
    </level1>
</root>

文件xml2

<root>
    <level1>
        ...
    </level1>
    <!--many records-->
    <level1>
        ...
    </level1>
</root>

在这种情况下,使用jdom2 库的下一个过程可以帮助您:

void concatXML(Path fSource,Path fDest) 
     Document jdomSource = null;
     Document jdomDest = null;
     List<Element> elems = new LinkedList<Element>();
     SAXBuilder jdomBuilder = new SAXBuilder();
     try 
         jdomSource  = jdomBuilder.build(fSource.toFile());
         jdomDest    = jdomBuilder.build(fDest.toFile());
         Element root = jdomDest.getRootElement();
         root.detach();
         String sourceNextElementName=((Element) jdomSource.getRootElement().getContent().get(1)).getName();
         for (Element record:jdomSource.getRootElement().getDescendants(new ElementFilter(sourceNextElementName)))
                elems.add(record);
            for (Element elem : elems) (elem).detach();
            root.addContent(elems);

            Document newDoc = new Document(root);
            XMLOutputter xmlOutput = new XMLOutputter();

            xmlOutput.output(newDoc, System.out);
            xmlOutput.setFormat(Format.getPrettyFormat());
            xmlOutput.output(newDoc, Files.newBufferedWriter(fDest, Charset.forName("UTF-8")));
         catch (Exception e) 
            e.printStackTrace();
        
    

【讨论】:

以上是关于在 Java 中合并两个 XML 文件的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中合并两个 XML 文件

使用 XSL 转换合并两个 XML 文件

合并两个 XML 文件并添加缺少的标签和属性

Java+XSL合并多个XML文件

如何使用Java将> 1000个xml文件合并为一个

XSLT:一种合并 xml 文件的简单方法