如何使用Java将1000多个XML文件合并为一个

8
我将尝试合并多个XML文件成一个。我已经在DOM中成功完成了这个任务,但是这种解决方案仅适用于少量文件。当我在多个文件(>1000)上运行它时,会出现java.lang.OutOfMemoryError错误。
我的目标是将以下文件合并为一个:
文件1:
<root>
....
</root>

文件 2:

<root>
......
</root>

file n:

<root>
....
</root>

导致: 输出:
<rootSet>
<root>
....
</root>
<root>
....
</root>
<root>
....
</root>
</rootSet>

这是我的当前实现方式:
    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
    Document doc = docBuilder.newDocument();
    Element rootSetElement = doc.createElement("rootSet");
    Node rootSetNode = doc.appendChild(rootSetElement);
    Element creationElement = doc.createElement("creationDate");
    rootSetNode.appendChild(creationElement);
    creationElement.setTextContent(dateString); 
    File dir = new File("/tmp/rootFiles");
    String[] files = dir.list();
    if (files == null) {
        System.out.println("No roots to merge!");
    } else {
        Document rootDocument;
            for (int i=0; i<files.length; i++) {
                       File filename = new File(dir+"/"+files[i]);        
               rootDocument = docBuilder.parse(filename);
               Node tempDoc = doc.importNode((Node) Document.getElementsByTagName("root").item(0), true);
               rootSetNode.appendChild(tempDoc);
        }
    }   

我已经尝试过很多xslt、sax的实验,但似乎总是缺少某些东西。非常感谢任何帮助。


4
您需要将DOM保留在内存中吗?在这种情况下,您是否需要更多的简单字符串连接? - Yuval Adam
1
简单的连接将保留每个要合并的单个XML文件的XML声明。但实际上,我正在寻找一种简单的XML文件连接方式。 - Andra
2
为什么不将多个XML文件放入一个归档文件中?这样它们就会变成一个文件。如果读/写速度很重要,可以选择不压缩;如果文件大小或带宽更重要,则可以选择压缩。 - Andrew Thompson
6个回答

10

你也可以考虑使用StAX。这是一个可以实现你想要的功能的代码:

import java.io.File;
import java.io.FileWriter;
import java.io.Writer;

import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.stream.StreamSource;

public class XMLConcat {
    public static void main(String[] args) throws Throwable {
        File dir = new File("/tmp/rootFiles");
        File[] rootFiles = dir.listFiles();

        Writer outputWriter = new FileWriter("/tmp/mergedFile.xml");
        XMLOutputFactory xmlOutFactory = XMLOutputFactory.newFactory();
        XMLEventWriter xmlEventWriter = xmlOutFactory.createXMLEventWriter(outputWriter);
        XMLEventFactory xmlEventFactory = XMLEventFactory.newFactory();

        xmlEventWriter.add(xmlEventFactory.createStartDocument());
        xmlEventWriter.add(xmlEventFactory.createStartElement("", null, "rootSet"));

        XMLInputFactory xmlInFactory = XMLInputFactory.newFactory();
        for (File rootFile : rootFiles) {
            XMLEventReader xmlEventReader = xmlInFactory.createXMLEventReader(new StreamSource(rootFile));
            XMLEvent event = xmlEventReader.nextEvent();
            // Skip ahead in the input to the opening document element
            while (event.getEventType() != XMLEvent.START_ELEMENT) {
                event = xmlEventReader.nextEvent();
            }

            do {
                xmlEventWriter.add(event);
                event = xmlEventReader.nextEvent();
            } while (event.getEventType() != XMLEvent.END_DOCUMENT);
            xmlEventReader.close();
        }

        xmlEventWriter.add(xmlEventFactory.createEndElement("", null, "rootSet"));
        xmlEventWriter.add(xmlEventFactory.createEndDocument());

        xmlEventWriter.close();
        outputWriter.close();
    }
}

一个小问题是这个API似乎会更改空标签,将<foo/>变成<foo></foo>


3

不需要进行任何xml解析,因为它似乎不需要对xml进行实际解析。

要提高效率,请执行以下操作:

File dir = new File("/tmp/rootFiles");
String[] files = dir.list();
if (files == null) {
    System.out.println("No roots to merge!");
} else {
        try (FileChannel output = new FileOutputStream("output").getChannel()) {
            ByteBuffer buff = ByteBuffer.allocate(32);
            buff.put("<rootSet>\n".getBytes()); // specify encoding too
            buff.flip();
            output.write(buff);
            buff.clear();
            for (String file : files) {
                try (FileChannel in = new FileInputStream(new File(dir, file).getChannel()) {
                    in.transferTo(0, 1 << 24, output);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            buff.put("</rootSet>\n".getBytes()); // specify encoding too
            buff.flip();
            output.write(buff);
        } catch (IOException e) {
            e.printStackTrace();
        }

2

DOM需要将整个文档保存在内存中。如果您不需要对标记执行任何特殊操作,我建议使用InputStream并读取所有文件。如果需要执行一些操作,则使用SAX。


2

Dom消耗了大量的内存。我认为,你有以下几个选择。

最好的选择是使用SAX。使用sax,只使用了非常少的内存,因为基本上几乎每次只有一个元素从输入到输出,所以内存占用极低。然而,使用sax并不那么简单,因为与dom相比它有点反直觉。

尝试Stax,我自己没有尝试过,但它是一种更容易实现和使用的类似于steroids的sax,因为与其只接收你无法控制的sax事件不同,你实际上是“询问源”流式传输你想要的元素,所以它处于dom和sax之间,具有类似于sax的内存占用,但是更友好的范例。

sax、stax、dom都很重要,如果您想正确地保留、声明等等...命名空间和其他XML奇特之处。

然而,如果您只需要一种快速而简单的方法,这也可能符合命名空间规范,那就使用普通的字符串和写入器。

开始向FileWriter输出您的“大”文档的声明和根元素。然后,使用dom(如果您喜欢),逐个加载每个单独的文件。选择要在“大”文件中结束的元素,将它们序列化回一个字符串,并将它们发送给writer。writer将在不使用大量内存的情况下刷新到磁盘,并且dom每次迭代只加载一个文档。除非您在输入端也有非常大的文件,或者计划在手机上运行它,否则您不应该有太多的内存问题。如果dom正确地序列化它,它应该保留命名空间声明等等,代码将比您发布的代码多出几行。


1

我认为你所做的是有效的。要使它适用于大量文件,唯一的方法是使用基于文本的流式处理方法,这样你就不必将整个文件保存在内存中。但是,好消息是,现在内存很便宜,64位JVM非常流行,也许你只需要增加堆大小。尝试使用-Xms1g JVM选项重新运行程序(分配1GB初始堆大小)。

我也倾向于使用XOM来满足所有DOM需求。试试看吧。在我的经验中,效率更高。我不确定内存需求如何,但速度快了几个数量级。


1
针对这种工作,我建议不要使用DOM,读取文件内容并进行子字符串操作会更简单而且足够。
我想的是这样的:
String rootContent = document.substring(document.indexOf("<root>"), document.lastIndexOf("</root>")+7);

为了避免过多内存消耗,建议在每次 XML 提取后使用 BufferedWriter 在主文件中进行写入操作。如果想要更好的性能,也可以考虑使用 java.nio


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接