53

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

我在加载此文件 ( OutOfMemoryError) 时遇到问题,因为 POI 在 XSSF (xlsx) 工作簿上占用了大量内存。

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

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

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

如果没有困难,欢迎您说出来(“没有。”)并向我指出“XML”解决方案的方法。

4

8 回答 8

73

我在网络服务器环境中处于类似情况。上传的典型大小约为 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 工作表的结构方式,并非所有数据都在流的当前窗口中可用。但是,如果您只是想从单元格中读取简单的数据,那么它的效果就很好。

于 2015-02-08T17:58:16.320 回答
17

可以通过使用 File 而不是 Stream 来提高内存使用率。(最好使用流式 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

文件与输入流

“打开工作簿时,无论是 .xls HSSFWorkbook 还是 .xlsx XSSFWorkbook,都可以从 File 或 InputStream 加载工作簿。使用 File 对象可以降低内存消耗,而 InputStream 需要更多内存,因为它有缓冲整个文件。”

于 2013-07-09T07:38:04.263 回答
10

Apache POI、HSSF 和 XSSF 中的 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 中使用它的 三个 类以获取更多详细信息。

于 2012-11-12T19:37:02.130 回答
8

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

注意:我读过 SXSSF 作为一个写作 API 工作。应该使用 XSSF 来完成加载,而不需要对文件进行输入流(以避免在内存中完全加载它)

于 2012-08-09T22:12:06.877 回答
6

检查这篇文章。我将展示如何使用 SAX 解析器来处理 XLSX 文件。

https://stackoverflow.com/a/44969009/4587961

简而言之,我org.xml.sax.helpers.DefaultHandler为 XLSX filez 扩展了 whih 处理 XML 结构。t 是事件解析器 - 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;
    }
}

然后我解析 XML 呈现 XLSX 文件

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;
}
于 2017-07-07T10:54:45.843 回答
0

基于从 poi 探索的 monitorjbl 的答案和测试套件,以下为我处理了具有 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);
            }
        }
    }
}
于 2018-08-14T17:13:49.623 回答
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);
     }}

于 2020-01-07T12:53:57.940 回答
-1

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

于 2013-01-19T19:57:39.710 回答