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

时间:2012-05-25 18:41:23

标签: java xml performance merge out-of-memory

我正在尝试将许多xml文件合并为一个。我已经在DOM中成功完成了这项工作,但此解决方案仅限于几个文件。当我在多个文件> 1000上运行它时,我得到一个java.lang.OutOfMemoryError。

我想要实现的是我有以下文件的地方

文件1:

<root>
....
</root>

文件2:

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

文件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进行了很多实验,但我似乎总是缺少一些东西。任何帮助都将受到高度赞赏

6 个答案:

答案 0 :(得分: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>

答案 1 :(得分: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 :(得分:2)

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

答案 3 :(得分:2)

Dom确实消耗了大量内存。你有,imho,以下的替代品。

最好的是使用SAX。使用sax,只使用非常少量的内存,导致在任何给定时间基本上几乎单个元素从输入传输到输出,因此内存占用极低。但是,使用sax并不是那么简单,因为与dom相比,它有点违反直觉。

尝试使用Stax,而不是尝试自己,但它是一种更容易实现和使用类固醇的sax,因为与仅接收您无法控制的sax事件相反,您实际上“请求源”将流元素传递给您你想要的,所以它适合在dom和sax之间的中间,具有类似于sax的内存占用,但是更友好的范例。

如果你想正确保存,声明等等......名称空间和其他XML怪异,那么Sax,stax,dom都很重要。

但是,如果你只需要一种快速而又脏的方式,也可能是符合命名空间的方式,那就使用普通的旧字符串和编写器。

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

答案 4 :(得分:1)

对于这种工作,我建议不要使用DOM,读取文件内容并使子字符串更简单和足够。

我在想这样的事情:

String rootContent = document.substring(document.indexOf("<root>"), document.lastIndexOf("</root>")+7);

然后避免太多的记忆圆满。例如,在每次xml提取后使用BufferedWritter写入主文件。为了获得更好的性能,您还可以使用java.nio

答案 5 :(得分:1)

我认为你所做的事情是有效的。使其扩展到真正大量文件的唯一方法是使用基于文本的流式处理方法,因此您永远不会将整个内容保留在内存中。但是,嘿!好消息。这些天内存很便宜,64位JVM风靡一时,所以你可能只需要增加堆大小。尝试使用-Xms1g JVM选项重新运行程序(分配1Gb初始堆大小)。

我也倾向于使用XOM来满足我的所有DOM要求。搏一搏。效率更高。不确定内存需求,但根据我的经验,它的数量级更快。