如何使用Apache POI加载大型xlsx文件?

57

我有一个大的.xlsx文件(141MB,包含293413行,每行62列),需要进行一些操作。

我在加载这个文件时遇到了问题(OutOfMemoryError),因为POI在XSSF(xlsx)工作簿中的内存占用很大。

这个SO问题类似,并且提出的解决方案是增加VM的分配/最大内存。

这对于那种文件大小(9MB)似乎可以奏效,但对于我来说,即使分配了所有可用的系统内存,它也根本不起作用。(嗯,这并不奇怪,考虑到该文件超过15倍以上)

我想知道是否有任何方法以不会消耗所有内存的方式加载工作簿,而又不会基于XSSF的底层XML进行处理。 (换句话说,保持一个纯粹的POI解决方案)

如果没有,你可以说("没有"),并将我指向“XML”解决方案的方法。


1
我认为与您所问的更相关的SO问题是“在Java中处理大型xlsx文件”。尝试使用此API http://poi.apache.org/spreadsheet/how-to.html#xssf_sax_api,因为我认为它只解析数据,而不存储内容。 - Bob Kuhar
你只需要读取文件来处理它吗?还是需要进行完整的读取/编辑/写入循环? - Gagravarr
@Gagravarr - 理论上我需要完整的循环,但就POI的范围而言,仅仅读取就足够了,因为对于这个特定的情况,最终文件可以保存为.txt(制表符分隔)、.csv或类似文件。换句话说,在数据提取后,我可以不用POI来管理纯文本。 - CosmicGiant
8个回答

78

我曾经在一个Web服务器环境下遇到类似的情况。上传文件的典型大小约为150k行,如果从单个请求中消耗大量内存将不太好。Apache POI Streaming API适用于此,但它需要彻底重新设计您的读取逻辑。我已经使用标准API编写了一堆读取逻辑,不想重做,所以我写了这个:https://github.com/monitorjbl/excel-streaming-reader

它并不完全是标准XSSFWorkbook类的替代品,但如果您只需要迭代行,它的行为类似:

import com.monitorjbl.xlsx.StreamingReader;

InputStream is = new FileInputStream(new File("/path/to/workbook.xlsx"));
StreamingReader reader = StreamingReader.builder()
        .rowCacheSize(100)    // number of rows to keep in memory (defaults to 10)
        .bufferSize(4096)     // buffer size to use when reading InputStream to file (defaults to 1024)
        .sheetIndex(0)        // index of sheet to use (defaults to 0)
        .read(is);            // InputStream or File for XLSX file (required)

for (Row r : reader) {
  for (Cell c : r) {
    System.out.println(c.getStringCellValue());
  }
}     

使用它时需要注意一些问题;由于XLSX表格的结构方式,流的当前窗口中并不是所有的数据都可用。但是,如果你只是想从单元格中读取简单的数据,那么它在这方面的工作效果还是相当不错的。


除了空单元格外,一切都正常。这些空单元格没有被返回,这给我带来了问题,因为我无法确定每行实际缺少哪个列(只知道返回的少于预期)。 - darkman97i
2
这个解决方案非常棒。在git页面上定义了一个新的用法:InputStream is = new FileInputStream(new File("/path/to/workbook.xlsx")); Workbook workbook = StreamingReader.builder().rowCacheSize(100).bufferSize(4096).sheetIndex(0).open(is);如果你只需要遍历行,这使你可以像内存中加载工作表一样处理工作表。 - Anthony Ruffino
@darkman97i: 现在支持使用Cell cell = row.getCell(i, Row.CREATE_NULL_AS_BLANK)来解决该问题。请参见此处 - themenace
1
这个解决方案适用于读取文件内容,但如果您想从工作簿中获取单元格样式,则无法使用。 - Arthur bauer
你刚刚救了我的工作(开玩笑!)。它运行得非常好。我尝试了所有的方法,但都没有奏效。 只是希望公式单元格也能被处理。目前我已要求客户不要上传带有公式的文件。 - saran3h
显示剩余2条评论

20

使用文件而不是流可以提高内存使用效率。 (最好使用流API,但是流API存在一些限制,请参见http://poi.apache.org/spreadsheet/index.html

因此,不要使用

Workbook workbook = WorkbookFactory.create(inputStream);

Workbook workbook = WorkbookFactory.create(new File("yourfile.xlsx"));

这是根据:http://poi.apache.org/spreadsheet/quick-guide.html#FileInputStream

文件 vs 输入流

"当打开工作簿时,可以从文件或输入流加载Workbook,其中 .xls HSSFWorkbook 或 .xlsx XSSFWorkbook。使用 File 对象可降低内存消耗,而 InputStream 需要更多内存,因为它必须缓冲整个文件。"


11

Apache POI中的Excel支持有3种不同的模式。

第一种是完整的、类似DOM的内存中“UserModel”,支持读写。使用常见的SS(SpreadSheet)接口,你可以基本上透明地编码为HSSF(.xls)和XSSF(.xlsx)两种格式。但是,它需要大量的内存。

POI还支持一种流式只读处理文件的方式,即EventModel。这比UserModel更低级别,使您非常接近文件格式。对于HSSF(.xls),您获得一系列记录流,并可选择一些帮助处理它们(缺少单元格、格式跟踪等)。对于XSSF(.xlsx),您获得来自文件不同部分的SAX事件流,帮助您获取文件的正确部分以及轻松处理文件的常见但小的位。

仅适用于XSSF(.xlsx),POI还支持一种只写流式写入,适用于低级别但低内存写入。它主要只支持新文件(某些追加类型可能是可能的)。没有HSSF等效项,由于许多记录中的前后字节偏移和索引偏移往返,因此这将非常困难......

针对您所描述的特定情况,我认为您需要使用XSSF EventModel代码。请参考POI文档进行开始使用,并尝试查看这些三个中POI和Tika用于更多详细信息。


9

现在POI已经包含了一个适用于这些情况的API。SXSSF http://poi.apache.org/spreadsheet/index.html 它不会将所有内容都加载到内存中,因此可以让您处理这种文件。

注意:我读到过SXSSF作为写入API工作的信息。使用XSSF进行加载时,不应该使用inputstream来加载文件(以避免在内存中完全加载文件)。


1
你能给我一些关于如何实现加载现有文件的来源吗?我在那个链接上看到的都是用于创建目的,这对我的情况没有用处。 - CosmicGiant
4
我了解到SXSSF是一个用于写入数据的API,加载数据时应该使用XSSF,不需要使用inputstream来读取文件(以避免将整个文件加载到内存中)。 - Alfabravo
我怎样才能在不使用InputStream读取文件的情况下加载它呢?这里的问题是读取(load),而不是保存(write)。 - CosmicGiant
1
这个问题可以通过阅读这个帖子来解决。希望能够帮到你:https://dev59.com/qmgu5IYBdhLWcg3wrY0l - Alfabravo
不行。即使使用“-Xmx2048m”,以下代码仍会导致“OutOfMemoryError”错误:OPCPackage opcPackage = OPCPackage.open(file.getAbsolutePath()); workbook = new XSSFWorkbook(opcPackage);---仍有事件驱动API(XSSF_SAX)可以尝试,但我不确定该如何做。 - CosmicGiant
@Panther 是的,我之前在这里的评论中添加了那个。可能会将其添加到实际答案中以使其更清晰。谢谢。 - Alfabravo

8

请查看这篇文章。我展示了如何使用SAX解析器处理XLSX文件。

https://dev59.com/JnfZa4cB1Zd3GeqPT6C0#44969009

简而言之,我扩展了org.xml.sax.helpers.DefaultHandler,它处理XLSX文件的XML结构。它是一种事件解析器 - SAX。

class SheetHandler extends DefaultHandler {

    private static final String ROW_EVENT = "row";
    private static final String CELL_EVENT = "c";

    private SharedStringsTable sst;
    private String lastContents;
    private boolean nextIsString;

    private List<String> cellCache = new LinkedList<>();
    private List<String[]> rowCache = new LinkedList<>();

    private SheetHandler(SharedStringsTable sst) {
        this.sst = sst;
    }

    public void startElement(String uri, String localName, String name,
                             Attributes attributes) throws SAXException {
        // c => cell
        if (CELL_EVENT.equals(name)) {
            String cellType = attributes.getValue("t");
            if(cellType != null && cellType.equals("s")) {
                nextIsString = true;
            } else {
                nextIsString = false;
            }
        } else if (ROW_EVENT.equals(name)) {
            if (!cellCache.isEmpty()) {
                rowCache.add(cellCache.toArray(new String[cellCache.size()]));
            }
            cellCache.clear();
        }

        // Clear contents cache
        lastContents = "";
    }

    public void endElement(String uri, String localName, String name)
            throws SAXException {
        // Process the last contents as required.
        // Do now, as characters() may be called more than once
        if(nextIsString) {
            int idx = Integer.parseInt(lastContents);
            lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
            nextIsString = false;
        }

        // v => contents of a cell
        // Output after we've seen the string contents
        if(name.equals("v")) {
            cellCache.add(lastContents);
        }
    }

    public void characters(char[] ch, int start, int length)
            throws SAXException {
        lastContents += new String(ch, start, length);
    }

    public List<String[]> getRowCache() {
        return rowCache;
    }
}

然后我解析代表XLSX文件的XML。

private List<String []> processFirstSheet(String filename) throws Exception {
    OPCPackage pkg = OPCPackage.open(filename, PackageAccess.READ);
    XSSFReader r = new XSSFReader(pkg);
    SharedStringsTable sst = r.getSharedStringsTable();

    SheetHandler handler = new SheetHandler(sst);
    XMLReader parser = fetchSheetParser(handler);
    Iterator<InputStream> sheetIterator = r.getSheetsData();

    if (!sheetIterator.hasNext()) {
        return Collections.emptyList();
    }

    InputStream sheetInputStream = sheetIterator.next();
    BufferedInputStream bisSheet = new BufferedInputStream(sheetInputStream);
    InputSource sheetSource = new InputSource(bisSheet);
    parser.parse(sheetSource);
    List<String []> res = handler.getRowCache();
    bisSheet.close();
    return res;
}

public XMLReader fetchSheetParser(ContentHandler handler) throws SAXException {
    XMLReader parser = new SAXParser();
    parser.setContentHandler(handler);
    return parser;
}

2
你的fetchSheetParser在哪里? - abr
@abr 已编辑,请再次检查。 - Yan Khonski
目前,它会忽略空单元格。这是给你的一道作业 - 修复这个漏洞。等我有时间了,我会自己修复并更新代码。 - Yan Khonski
1
但是,如果使用您的代码,如何在处理程序中读取时获取单元格公式?您能建议我吗,@YanKhonski。 - Umesh Sonawane
这篇帖子中链接的答案已更新,可以处理所有行和空单元格。 - DaviM
显示剩余2条评论

0

基于 monitorjbl 的答案和从 poi 探索出来的测试套件,以下代码对于我在包含 200K 记录(大小大于 50 MB)的多工作表 xlsx 文件上运行成功:

import com.monitorjbl.xlsx.StreamingReader;
. . .
try (
        InputStream is = new FileInputStream(new File("sample.xlsx"));
        Workbook workbook = StreamingReader.builder().open(is);
) {
    DataFormatter dataFormatter = new DataFormatter();
    for (Sheet sheet : workbook) {
        System.out.println("Processing sheet: " + sheet.getSheetName());
        for (Row row : sheet) {
            for (Cell cell : row) {
                String value = dataFormatter.formatCellValue(cell);
            }
        }
    }
}

0

请使用最新的代码


InputStream file = new FileInputStream(
                    new File("uploads/" + request.getSession().getAttribute("username") + "/" + userFile));
Workbook workbook = StreamingReader.builder().rowCacheSize(100) // number of rows to keep in memory
                    .bufferSize(4096) // index of sheet to use (defaults to 0)
                    .open(file); // InputStream or File for XLSX file (required)

Iterator<Row> rowIterator = workbook.getSheetAt(0).rowIterator();
 while (rowIterator.hasNext()) {
     while (cellIterator.hasNext()) {
         Cell cell = cellIterator.next();
        String cellValue = dataFormatter.formatCellValue(cell);
     }}


流式读取器类从哪里来(我在使用的poi版本中没有它)。 - John
1
它看起来来自于:https://github.com/monitorjbl/excel-streaming-reader - Justin N

-1

您可以使用SXXSF而不是使用HSSF。我可以生成具有200000行的Excel。


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