使用POI導入導出大數據量的Excel

昨天,產品提了一個緊急需求,讓把十一月份已發貨的商品數據導出來,寫好SQL發給DBA執行之後,得到了三十多個100W數據的Excel文件。有一個屬性是以JSON格式存在表中一個字段裏面的,需要加工Excel文件將其單獨取出來(如圖的第四列)。

處理程序也在數據導出的過程中寫好了,大概思路就是讀入Excel構建Workbook對象,然後對指定列的值進行轉換,最後寫回原文件。想法很奈斯,結果很悲哀,OOM了。即使把Xmx和Xms調到最大,還是無濟於事。

 

系統平時的導入導出不會遇到這種問題,導入的話限制了上傳文件大小,導出的話應用裏面設置了最大導出數量校驗。直接從數據庫的導出是寫好SQL交給DBA處理的,他們有自己的工具。

本着遇到困難就解決的優秀品質,所以總結了一下POI中專門處理大數據量的SXSSF。

一、HSSF、XSSF與SXSSF

首先是HSSF,支持2003及以下的版本,即.xls結尾的Excel文件,最多隻允許存儲65536條數據。

然後是XSSF,支持2007及以上的版本,即.xlsx結尾的Excel文件,雖然單個Sheet就支持1048576條數據,但是性能不好,而且那麼多的數據存在內存中也容易OOM。

最後是SXSSF,它使用了不一樣的存儲方式,具體就不說了,只要知道大數量用它就OK了。呃,它只支持.xlsx文件,看它名字就知道了:"S" + "XSSF",S表示SAX事件驅動模式。

二、SXSSF導出數據

這個比較簡單,SXSSF也是實現了Workbook接口,所以就跟HSSF和XSSF用起來差不多,只是構建實例的方式不一樣而已。

它的基本思路是:當內存中的數據夠了一定行數(構造函數可以設置)之後就先刷到硬盤中。但是,你也別就真把100W數據一次性讀到內存中,應該根據總數分批加載到內存(比如從數據庫讀需要分頁一樣)。

直接上代碼吧

/**
 * POI 導出
 *
 * @author Zhou Huanghua
 * @date 2020/1/11 14:03
 */
public class PoiExport {

    private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());

    public static void main(String[] args) throws Exception {
        long begin = System.currentTimeMillis();
        // keep 100 rows in memory, exceeding rows will be flushed to disk
        try (SXSSFWorkbook wb = new SXSSFWorkbook(100);
             OutputStream os = new FileOutputStream("C:/Users/dell/Desktop/tmp/demo.xlsx")) {
            Sheet sh = wb.createSheet();
            String val = "第%s行第%s列";
            for (int rowNum = 0; rowNum < 100_0000; rowNum++) {
                Row row = sh.createRow(rowNum);
                int realRowNum = rowNum + 1;
                Cell cell1 = row.createCell(0);
                cell1.setCellValue(format(val, realRowNum, 1));
                Cell cell2 = row.createCell(1);
                cell2.setCellValue(format(val, realRowNum, 2));
                Cell cell3 = row.createCell(2);
                cell3.setCellValue(format(val, realRowNum, 3));
                Cell cell4 = row.createCell(3);
                cell4.setCellValue(format(val, realRowNum, 4));
            }
            wb.write(os);
        }
        LOGGER.info("導出100W行數據耗時(秒):" + (System.currentTimeMillis() - begin)/1000);
    }
}

測試得到一個17M的文件,耗時17秒。

三、SXSSF導入數據

這個比較複雜一些,不過使用步驟也還統一,區別就是需要實現自己的SheetContentsHandler,我管它叫內容處理器。

直接上代碼

/**
 * POI 導入
 *
 * @author Zhou Huanghua
 * @date 2020/1/11 14:02
 */
public class PoiImport {

    private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());

    public static void main(String[] args) throws Exception {
        String filePath = "C:/Users/dell/Desktop/tmp/demo.xlsx";
        // OPCPackage.open(...)有多個重載方法,比如String path、File file、InputStream in等
        try (OPCPackage opcPackage = OPCPackage.open(filePath);) {
            // 創建XSSFReader讀取StylesTable和ReadOnlySharedStringsTable
            XSSFReader xssfReader = new XSSFReader(opcPackage);
            StylesTable stylesTable = xssfReader.getStylesTable();
            ReadOnlySharedStringsTable sharedStringsTable = new ReadOnlySharedStringsTable(opcPackage);
            // 創建XMLReader,設置ContentHandler
            XMLReader xmlReader = SAXHelper.newXMLReader();
            xmlReader.setContentHandler(new XSSFSheetXMLHandler(stylesTable, sharedStringsTable, new SimpleSheetContentsHandler(), false));
            // 解析每個Sheet數據
            Iterator<InputStream> sheetsData = xssfReader.getSheetsData();
            while (sheetsData.hasNext()) {
                try (InputStream inputStream = sheetsData.next();) {
                    xmlReader.parse(new InputSource(inputStream));
                }
            }
        }
    }

    /**
     * 內容處理器
     */
    public static class SimpleSheetContentsHandler implements XSSFSheetXMLHandler.SheetContentsHandler {

        protected List<String> row;

        /**
         * A row with the (zero based) row number has started
         *
         * @param rowNum
         */
        @Override
        public void startRow(int rowNum) {
            row = new ArrayList<>();
        }

        /**
         * A row with the (zero based) row number has ended
         *
         * @param rowNum
         */
        @Override
        public void endRow(int rowNum) {
            if (row.isEmpty()) {
                return;
            }
            // 處理數據
            LOGGER.info(row.stream().collect(Collectors.joining("   ")));
        }

        /**
         * A cell, with the given formatted value (may be null),
         * and possibly a comment (may be null), was encountered
         *
         * @param cellReference
         * @param formattedValue
         * @param comment
         */
        @Override
        public void cell(String cellReference, String formattedValue, XSSFComment comment) {
            row.add(formattedValue);
        }

        /**
         * A header or footer has been encountered
         *
         * @param text
         * @param isHeader
         * @param tagName
         */
        @Override
        public void headerFooter(String text, boolean isHeader, String tagName) {
        }
    }
}

目前是解析一行數據就處理,這個看你使用場景是否能夠接受,異步的話還OK。

如果你覺得數據量和你JVM的堆內存還OK的話,可以在SimpleSheetContentsHandler的構造函數傳一個集合進來收集每行數據,等把數據全部解析完再統一處理。BUT,既然你用了SXSSF,那麼這麼做有OOM風險。

比較好的做法,還是先將每行數據逐一收集,不過需要達到一定行數之後批量處理,然後清空集合重新收集,這個還沒想好怎麼把代碼寫得優雅,就先不獻醜了。

對了,我使用的maven依賴是

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.17</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.17</version>
        </dependency>

demo代碼地址:https://github.com/zhouhuanghua/poi-demo

發佈了109 篇原創文章 · 獲贊 104 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章