Apache POI在使用HSSF比XSSF更快 - 接下来该怎么办?

12

我在使用Apache POI解析.xlsx文件时遇到了问题——我的部署应用程序中出现了java.lang.OutOfMemoryError: Java heap space错误。我只处理小于5MB且约70,000行的文件,因此从阅读其他问题中得出的猜测是存在问题。

根据这个评论的建议,我决定使用推荐的变量运行SSPerformanceTest.java,以查看我的代码或设置是否有问题。结果显示HSSF(.xls)和XSSF(.xlsx)之间存在显著差异:

1) HSSF 50000 50 1: 耗时1秒

2) SXSSF 50000 50 1: 耗时5秒

3) XSSF 50000 50 1: 耗时15秒

FAQ明确指出:

如果您无法在所有HSSF、XSSF和SXSSF中以50,000行和50列的方式运行,并在3秒内(理想情况下更少!)完成,则问题可能出在您的环境中。

然后,它建议运行XLS2CSV.java,我已经这样做了。输入上面生成的XSSF文件(具有50000行和50列)需要大约15秒钟——与编写文件所花费的相同时间。

我的环境是否存在问题,如果有,我该如何进一步调查?

来自VisualVM的统计数据显示,在处理过程中使用的堆跳升到了1.2GB。考虑到这比处理开始前的堆多出了1GB,这肯定太高了吧?

堆空间肯定过高?

注意:上面提到的堆空间异常只在生产环境(Google App Engine)中出现,并且仅适用于 .xlsx 文件,但是本问题中提到的所有测试都在我的开发机器上使用 -Xmx2g 运行。我希望如果我能在我的开发环境中解决问题,那么在部署时就会使用更少的内存。

来自应用引擎的堆栈跟踪:

Caused by: java.lang.OutOfMemoryError: Java heap space at org.apache.xmlbeans.impl.store.Cur.createElementXobj(Cur.java:260) at org.apache.xmlbeans.impl.store.Cur$CurLoadContext.startElement(Cur.java:2997) at org.apache.xmlbeans.impl.store.Locale$SaxHandler.startElement(Locale.java:3211) at org.apache.xmlbeans.impl.piccolo.xml.Piccolo.reportStartTag(Piccolo.java:1082) at org.apache.xmlbeans.impl.piccolo.xml.PiccoloLexer.parseAttributesNS(PiccoloLexer.java:1802) at org.apache.xmlbeans.impl.piccolo.xml.PiccoloLexer.parseOpenTagNS(PiccoloLexer.java:1521)


你并不孤单:https://dev59.com/GZHea4cB1Zd3GeqPwv5x - raggi
该死,我在这里读了那么多问题,但没找到那个!非常感谢。如果邮件列表的沉默是什么的话,似乎这是一个库的问题。可能要开始着手解决问题了。 - slugmandrew
2个回答

6
我曾经面临同样的问题,使用Apache POI读取大型.xlsx文件时遇到困难。后来我发现了excel-streaming-reader-github
这个库可以作为流API的包装器,同时保留标准POI API的语法。
这个库可以帮助你读取大型文件。

谢谢,这看起来正是我需要的东西!可惜这些问题没有文档记录。 - slugmandrew
1
由于这似乎是解决常见问题的最佳方案(尽管我可能不得不分叉它以使其与应用引擎兼容),因此您将获得战利品 :) - slugmandrew

2
我处理的平均XLSX表格约为18-22个含有13-20列的750000行。它与其他许多功能一起在Spring Web应用程序中运行。我没有给整个应用程序太多的内存:-Xms1024m -Xmx4096m,效果很好!
首先,解决代码问题:将每一行数据全部加载到内存中再开始转储是错误的。对于我的情况(从PostgreSQL数据库报告),我重新设计了数据转储过程,使用RowCallbackHandler写入我的XLSX。当达到“限制”的750000行时,我会创建新的工作表。并且工作簿的可见窗口为50行。通过这种方式,我能够转储大量数据:XLSX文件的大小约为1230Mb。
以下是编写工作表的一些代码:
    jdbcTemplate.query(
        new PreparedStatementCreator() {
            @Override
            public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                PreparedStatement statement = connection.prepareStatement(finalQuery, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
                statement.setFetchSize(100);
                statement.setFetchDirection(ResultSet.FETCH_FORWARD);
                return statement;
            }
        }, new RowCallbackHandler() {
            Sheet sheet = null;
            int i = 750000;
            int tableId = 0;

            @Override
            public void processRow(ResultSet resultSet) throws SQLException {
                if (i == 750000) {
                    tableId++;
                    i = 0;
                    sheet = wb.createSheet(sheetName.concat(String.format("%02d%n", tableId)));


                    Row r = sheet.createRow(0);

                    Cell c = r.createCell(0);
                    c.setCellValue("id");
                    c = r.createCell(1);
                    c.setCellValue("Дата");
                    c = r.createCell(2);
                    c.setCellValue("Комментарий");
                    c = r.createCell(3);
                    c.setCellValue("Сумма операции");
                    c = r.createCell(4);
                    c.setCellValue("Дебет");
                    c = r.createCell(5);
                    c.setCellValue("Страхователь");
                    c = r.createCell(6);
                    c.setCellValue("Серия договора");
                    c = r.createCell(7);
                    c.setCellValue("Номер договора");
                    c = r.createCell(8);
                    c.setCellValue("Основной агент");
                    c = r.createCell(9);
                    c.setCellValue("Кредит");
                    c = r.createCell(10);
                    c.setCellValue("Программа");
                    c = r.createCell(11);
                    c.setCellValue("Дата начала покрытия");
                    c = r.createCell(12);
                    c.setCellValue("Дата планового окончания покрытия");
                    c = r.createCell(13);
                    c.setCellValue("Периодичность уплаты взносов");
                }
                i++;

                PremiumEntity e = PremiumEntity.builder()
                    .Id(resultSet.getString("id"))
                    .OperationDate(resultSet.getDate("operation_date"))
                    .Comments(resultSet.getString("comments"))
                    .SumOperation(resultSet.getBigDecimal("sum_operation").doubleValue())
                    .DebetAccount(resultSet.getString("debet_account"))
                    .Strahovatelname(resultSet.getString("strahovatelname"))
                    .Seria(resultSet.getString("seria"))
                    .NomPolica(resultSet.getLong("nom_polica"))
                    .Agentname(resultSet.getString("agentname"))
                    .CreditAccount(resultSet.getString("credit_account"))
                    .Program(resultSet.getString("program"))
                    .PoliciStartDate(resultSet.getDate("polici_start_date"))
                    .PoliciPlanEndDate(resultSet.getDate("polici_plan_end_date"))
                    .Periodichn(resultSet.getString("id_periodichn"))
                    .build();

                Row r = sheet.createRow(i);
                Cell c = r.createCell(0);
                c.setCellValue(e.getId());

                if (e.getOperationDate() != null) {
                    c = r.createCell(1);
                    c.setCellStyle(dateStyle);
                    c.setCellValue(e.getOperationDate());
                }

                c = r.createCell(2);
                c.setCellValue(e.getComments());

                c = r.createCell(3);
                c.setCellValue(e.getSumOperation());

                c = r.createCell(4);
                c.setCellValue(e.getDebetAccount());

                c = r.createCell(5);
                c.setCellValue(e.getStrahovatelname());

                c = r.createCell(6);
                c.setCellValue(e.getSeria());

                c = r.createCell(7);
                c.setCellValue(e.getNomPolica());

                c = r.createCell(8);
                c.setCellValue(e.getAgentname());

                c = r.createCell(9);
                c.setCellValue(e.getCreditAccount());

                c = r.createCell(10);
                c.setCellValue(e.getProgram());

                if (e.getPoliciStartDate() != null) {
                    c = r.createCell(11);
                    c.setCellStyle(dateStyle);
                    c.setCellValue(e.getPoliciStartDate());
                }
                ;

                if (e.getPoliciPlanEndDate() != null) {
                    c = r.createCell(12);
                    c.setCellStyle(dateStyle);
                    c.setCellValue(e.getPoliciPlanEndDate());
                }

                c = r.createCell(13);
                c.setCellValue(e.getPeriodichn());
            }
        });

在重新编写将数据转储为XLSX的代码后,我遇到了一个问题,它需要64位Office才能打开它们。因此,我需要将具有许多工作表的工作簿拆分为单个工作表的单独XLSX文件,以使它们可在普通计算机上阅读。再次使用小的可见窗口和流处理,并保持整个应用程序正常工作,没有任何OutOfMemory的迹象。
一些读取和拆分工作表的代码:
        OPCPackage opcPackage = OPCPackage.open(originalFile, PackageAccess.READ);


        ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(opcPackage);
        XSSFReader xssfReader = new XSSFReader(opcPackage);
        StylesTable styles = xssfReader.getStylesTable();
        XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) xssfReader.getSheetsData();
        int index = 0;
        while (iter.hasNext()) {
            InputStream stream = iter.next();
            String sheetName = iter.getSheetName();

            DataFormatter formatter = new DataFormatter();
            InputSource sheetSource = new InputSource(stream);

            SheetToWorkbookSaver saver = new SheetToWorkbookSaver(sheetName);
            try {
                XMLReader sheetParser = SAXHelper.newXMLReader();
                ContentHandler handler = new XSSFSheetXMLHandler(
                    styles, null, strings, saver, formatter, false);
                sheetParser.setContentHandler(handler);
                sheetParser.parse(sheetSource);
            } catch(ParserConfigurationException e) {
                throw new RuntimeException("SAX parser appears to be broken - " + e.getMessage());
            }

            stream.close();

            // this creates new File descriptors inside storage
            FileDto partFile = new FileDto("report_".concat(StringUtils.trimToEmpty(sheetName)).concat(".xlsx"));
            File cloneFile = fileStorage.read(partFile);
            FileOutputStream cloneFos = new FileOutputStream(cloneFile);
            saver.getWb().write(cloneFos);
            cloneFos.close();
        }

并且。
public class SheetToWorkbookSaver implements XSSFSheetXMLHandler.SheetContentsHandler {

    private SXSSFWorkbook wb;
    private Sheet sheet;
    private CellStyle dateStyle ;


    private Row currentRow;

    public SheetToWorkbookSaver(String workbookName) {
        this.wb = new SXSSFWorkbook(50);
        this.dateStyle = this.wb.createCellStyle();
        this.dateStyle.setDataFormat(this.wb.getCreationHelper().createDataFormat().getFormat("dd.mm.yyyy"));

        this.sheet = this.wb.createSheet(workbookName);

    }

    @Override
    public void startRow(int rowNum) {
        this.currentRow = this.sheet.createRow(rowNum);
    }

    @Override
    public void endRow(int rowNum) {

    }

    @Override
    public void cell(String cellReference, String formattedValue, XSSFComment comment) {
        int thisCol = (new CellReference(cellReference)).getCol();
        Cell c = this.currentRow.createCell(thisCol);
        c.setCellValue(formattedValue);
        c.setCellComment(comment);
    }

    @Override
    public void headerFooter(String text, boolean isHeader, String tagName) {

    }


    public SXSSFWorkbook getWb() {
        return wb;
    }
}

因此它可以读写数据。我猜在你的情况下,你应该重新设计你的代码,使其保持小内存占用的模式。所以我建议为读取创建自定义SheetContentsReader,它将把数据推送到某个数据库,在那里可以轻松地处理、聚合等。


非常感谢您的回答,以及分享您的代码。看来解决同一问题有许多方法! - slugmandrew

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