如何加载500MB Excel(.xlsm)文件并删除工作表,规避内存溢出问题
嘿,我来帮你搞定这个大Excel文件的内存溢出问题!先说说为什么你的现有代码会出问题,再给你两个靠谱的解决方案。
为什么你的代码会内存溢出?
你用XSSFWorkbook直接加载500MB的xlsm文件,这本身就是问题所在——XSSFWorkbook会把整个Excel的所有内容(包括所有工作表、样式、公式等等)全部加载到内存里,500MB的压缩文件解压后,内存占用会飙升到几个GB,直接触发OOM太正常了。
而你后面套了SXSSFWorkbook也没用,因为SXSSFWorkbook是用来生成大文件的(它会把超出内存的行写到临时文件),但你还是先把整个XSSFWorkbook加载到内存了,等于做了无用功。
内存友好的解决方案
针对删除工作表这个需求,有两个高效的思路,完全不用加载整个文件到内存:
方案一:直接操作OPCPackage(底层Zip包)删除工作表
xlsm本质是个Zip压缩包,里面每个工作表对应xl/worksheets/sheetN.xml文件,而工作表的元数据存在xl/workbook.xml里。我们可以直接修改这两个部分,不需要加载整个工作簿:
import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationship; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.ByteArrayOutputStream; import java.io.OutputStream; public class LargeExcelSheetRemover { public static void removeSheet(String filePath, String targetSheetName) throws Exception { // 以可写模式打开OPCPackage try (OPCPackage pkg = OPCPackage.open(filePath, OPCPackage.OPEN_WRITE)) { // 1. 找到workbook.xml并删除目标工作表的元数据条目 PackagePart workbookPart = pkg.getPartsByName("/xl/workbook.xml").get(0); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); // Excel XML是带命名空间的,必须开启这个 DocumentBuilder db = dbf.newDocumentBuilder(); Document workbookDoc = db.parse(workbookPart.getInputStream()); NodeList sheetNodes = workbookDoc.getElementsByTagNameNS("http://schemas.openxmlformats.org/spreadsheetml/2006/main", "sheet"); String sheetRelId = null; // 遍历找到要删除的工作表节点 for (int i = 0; i < sheetNodes.getLength(); i++) { Element sheetElement = (Element) sheetNodes.item(i); if (targetSheetName.equals(sheetElement.getAttribute("name"))) { sheetRelId = sheetElement.getAttribute("r:id"); sheetElement.getParentNode().removeChild(sheetElement); break; } } if (sheetRelId == null) { throw new IllegalArgumentException("找不到名为 " + targetSheetName + " 的工作表"); } // 更新workbook.xml的内容 ByteArrayOutputStream baos = new ByteArrayOutputStream(); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.transform(new DOMSource(workbookDoc), new StreamResult(baos)); try (OutputStream os = workbookPart.getOutputStream()) { os.write(baos.toByteArray()); } // 2. 删除对应的工作表文件(比如xl/worksheets/sheet3.xml) PackageRelationship sheetRel = workbookPart.getRelationship(sheetRelId); PackagePart sheetPart = pkg.getPart(sheetRel); pkg.removePart(sheetPart); // 可选:如果该工作表有关联的绘图、注释文件,也可以一并删除,这里简化处理 } } public static void main(String[] args) throws Exception { removeSheet("C:\\PROJECTS\\test.xlsm", "要删除的工作表名称"); } }
这个方案的优势是内存占用极低,因为我们只操作了两个小XML文件,完全不用加载整个Excel的内容,几百MB的文件处理起来毫无压力。
方案二:用SAX流式解析修改workbook.xml(适合超大型workbook.xml)
如果你的workbook.xml本身也很大(比如有上百个工作表),用DOM解析可能还是会占内存,这时候可以用你提到的SAXParser来流式处理workbook.xml,跳过要删除的工作表节点,生成新的XML内容。核心逻辑示例:
import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import com.sun.org.apache.xml.internal.serialize.OutputFormat; import com.sun.org.apache.xml.internal.serialize.XMLWriter; import java.io.OutputStream; class SheetRemovalHandler extends DefaultHandler { private String targetSheetName; private boolean skipCurrentSheet = false; private XMLWriter writer; public SheetRemovalHandler(String targetSheetName, OutputStream outputStream) throws Exception { this.targetSheetName = targetSheetName; OutputFormat format = new OutputFormat("xml", "UTF-8", true); this.writer = new XMLWriter(outputStream, format); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if ("sheet".equals(localName) && targetSheetName.equals(attributes.getValue("name"))) { skipCurrentSheet = true; return; // 跳过这个sheet节点的所有内容 } if (!skipCurrentSheet) { writer.startElement(uri, localName, qName, attributes); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if ("sheet".equals(localName) && skipCurrentSheet) { skipCurrentSheet = false; return; } if (!skipCurrentSheet) { writer.endElement(uri, localName, qName); } } @Override public void characters(char[] ch, int start, int length) throws SAXException { if (!skipCurrentSheet) { writer.characters(ch, start, length); } } // 其他SAX生命周期方法按需实现,确保XML结构完整 }
用这个SAXHandler解析原workbook.xml,同时输出修改后的XML,再替换回OPCPackage里的workbook.xml,最后删除对应的工作表文件,这样连DOM解析的内存都省了。
总结
- 永远不要用
XSSFWorkbook加载几百MB的Excel文件,内存肯定爆; SXSSFWorkbook是写大文件用的,不是读大文件的,别搞混了;- 删除工作表最高效的方式是直接操作OPCPackage修改底层XML和删除Zip条目,完全不碰整个Excel内容,内存友好到爆炸;
- 如果你已经熟悉SAXParser,用它来处理workbook.xml会更适合极端大的场景。
内容的提问来源于stack exchange,提问作者Dhanaraja Rajendiran

