使用Java拆分大型XML文件并解决DOM操作内存溢出问题
解决大型XML文件的Java DOM内存溢出问题及拆分方案
兄弟,你遇到的java.lang.OutOfMemoryError: Java heap space完全是DOM解析的“天生缺陷”导致的——DOM会把整个XML文档完整加载到内存里构建树形结构,25GB的文件别说JVM堆内存,就算你把机器所有内存都给JVM也扛不住啊!
要处理这种超大XML,必须换用流式解析方案,比如SAX或者StAX。其中StAX比SAX更灵活,支持双向读写,非常适合这种需要筛选标签+拆分文件的场景。下面直接给你可行的代码方案,思路是逐节点读取XML,只保留你需要的标签,同时每处理一定数量的<page>节点就写入一个新的小文件:
首先,导入必要的包:
import javax.xml.stream.*; import javax.xml.stream.events.*; import java.io.*; import java.util.HashSet; import java.util.Set;
然后是核心处理类:
public class FrWikiProcessor { // 定义需要保留的标签集合,和你原来的筛选逻辑一致 private static final Set<String> ALLOWED_TAGS = new HashSet<>(); static { ALLOWED_TAGS.add("mediawiki"); ALLOWED_TAGS.add("page"); ALLOWED_TAGS.add("revision"); ALLOWED_TAGS.add("id"); ALLOWED_TAGS.add("title"); ALLOWED_TAGS.add("text"); } private final String inputFilePath; private final String outputDir; private int fileCounter = 1; private int pageCounter = 0; // 每个小文件包含1000个page节点,你可以根据需求调整这个数值 private static final int PAGES_PER_FILE = 1000; public FrWikiProcessor(String inputFilePath, String outputDir) { this.inputFilePath = inputFilePath; this.outputDir = outputDir; // 自动创建输出目录,避免找不到路径的问题 new File(outputDir).mkdirs(); } public void processAndSplit() throws XMLStreamException, IOException { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); try (FileInputStream fis = new FileInputStream(inputFilePath)) { XMLEventReader eventReader = inputFactory.createXMLEventReader(fis); XMLEventWriter eventWriter = null; boolean insideAllowedTag = false; while (eventReader.hasNext()) { XMLEvent event = eventReader.nextEvent(); if (event.isStartElement()) { StartElement startElement = event.asStartElement(); String tagName = startElement.getName().getLocalPart(); if (ALLOWED_TAGS.contains(tagName)) { insideAllowedTag = true; // 遇到page节点时计数,判断是否需要新建输出文件 if ("page".equals(tagName)) { pageCounter++; if (pageCounter % PAGES_PER_FILE == 1) { // 先关闭上一个文件的writer if (eventWriter != null) { eventWriter.add(outputFactory.createEndElement("", "", "mediawiki")); eventWriter.add(outputFactory.createEndDocument()); eventWriter.close(); } // 创建新的输出文件 String outputFilePath = outputDir + "/frwiki-part-" + fileCounter + ".xml"; eventWriter = outputFactory.createXMLEventWriter(new FileOutputStream(outputFilePath)); // 写入XML声明和根标签开始 eventWriter.add(outputFactory.createStartDocument("UTF-8", "1.0")); eventWriter.add(outputFactory.createStartElement("", "", "mediawiki")); fileCounter++; } } // 把允许的开始标签写入输出文件 eventWriter.add(event); } else { insideAllowedTag = false; } } else if (event.isEndElement()) { EndElement endElement = event.asEndElement(); String tagName = endElement.getName().getLocalPart(); if (ALLOWED_TAGS.contains(tagName)) { insideAllowedTag = true; // 把允许的结束标签写入输出文件 eventWriter.add(event); } } else if (insideAllowedTag && (event.isCharacters() || event.isProcessingInstruction())) { // 写入允许标签内的文本内容或处理指令 eventWriter.add(event); } } // 处理最后一个文件的收尾工作 if (eventWriter != null) { eventWriter.add(outputFactory.createEndElement("", "", "mediawiki")); eventWriter.add(outputFactory.createEndDocument()); eventWriter.close(); } } } public static void main(String[] args) { try { FrWikiProcessor processor = new FrWikiProcessor( "D:\\compressed\\frwiki-latest-pages-articles.xml", "D:\\compressed\\frwiki-split-parts" ); processor.processAndSplit(); System.out.println("文件处理完成,共生成 " + (processor.fileCounter - 1) + " 个小文件"); } catch (Exception e) { e.printStackTrace(); } } }
关键说明:
- 流式处理逻辑:逐事件读取XML,只处理你需要的标签,全程不会把整个文档加载到内存,彻底解决内存溢出问题。
- 自动拆分文件:通过
PAGES_PER_FILE控制每个小文件的<page>节点数量,你可以根据期望的小文件大小调整这个值。 - 标签筛选:和你原来的需求完全一致,只保留
id、title、text等指定标签,其他标签直接忽略。 - 资源管理:用try-with-resources自动关闭输入流,避免资源泄漏;每次切换文件时正确关闭旧的writer,保证XML结构完整。
另外提一句,你原来的代码里循环3次删除标签其实没必要——DOM的NodeList是动态的,删除节点后长度会变化,多次循环反而容易出错,流式处理一次遍历就搞定所有筛选工作。
内容的提问来源于stack exchange,提问作者Moharach
相关产品推荐
相关产品推荐

