java读取海量Excel数据操作类

java读取海量Excel数据操作类

前言

JAVA读取的Excel数据非常多时,会因为内存不够造成程序崩溃,本文通过org.apache.poi包可以读取海量Excel数据。

读取海量Excel数据操作类

import com.alibaba.mos.api.ExcelReadHandler;
import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.ss.usermodel.BuiltinFormats;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.util.SAXHelper;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.model.StylesTable;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.LinkedHashMap;

/**
 * 读取超大量Excel数据操作类
 */
public class ExcelEventUtil {

    /**
     * 读取Excel
     *
     * @param filename
     * @param excelReadHandler
     */
    public static void processAllSheets(String filename, ExcelReadHandler excelReadHandler) {
        Iterator<InputStream> sheets = null;
        XMLReader parser = null;
        try {
            OPCPackage pkg = OPCPackage.open(filename);
            XSSFReader reader = new XSSFReader(pkg);
            SharedStringsTable sst = reader.getSharedStringsTable();
            StylesTable styleTable = reader.getStylesTable();
            parser = fetchSheetParser(sst, styleTable, excelReadHandler);
            sheets = reader.getSheetsData();
        } catch (IOException | OpenXML4JException | SAXException | ParserConfigurationException e) {
            throw new ExcelReadException("读取Excel报错");
        }

        while (sheets != null && sheets.hasNext()) {
            try (InputStream sheet = sheets.next();) {
                InputSource sheetSource = new InputSource(sheet);
                parser.parse(sheetSource);
            } catch (IOException | SAXException e) {
                throw new ExcelReadException("读取Excel中sheet表报错");
            }
        }
    }

    /**
     * @param sst
     * @param styleTable
     * @param excelReadHandler
     * @return
     * @throws SAXException
     * @throws ParserConfigurationException
     */
    public static XMLReader fetchSheetParser(SharedStringsTable sst, StylesTable styleTable, ExcelReadHandler excelReadHandler) throws SAXException, ParserConfigurationException {
        XMLReader parser = SAXHelper.newXMLReader();
        ContentHandler handler = new SheetHandlerUtil(sst, styleTable, excelReadHandler);
        parser.setContentHandler(handler);
        return parser;
    }

    private static class SheetHandlerUtil extends DefaultHandler {
        /**
         * 单元格中的数据可能的数据类型
         */
        enum CellDataType {
            BOOL, ERROR, FORMULA, INLINESTR, SSTINDEX, NUMBER, DATE, NULL
        }

        /**
         *
         */
        private final DataFormatter formatter = new DataFormatter();

        /**
         * excel中,若单元格内的内容是字符串,那么这些字符串都存在这个变量中
         */
        private SharedStringsTable sst;

        /**
         * 用于获取时间类型单元格的时间格式
         */
        private StylesTable styleTable;

        /**
         * 当前单元格的内容
         */
        private String currentContents;

        /**
         * 当前单元格的位置
         */
        private String ref;

        /**
         * 当前单元格的类型
         */
        private CellDataType cellDataType;

        /**
         * 当前单元格为时间时的格式索引
         */
        private short formatIndex;

        /**
         * 当前单元格为时间时的格式
         */
        private String formatString;

        /**
         *
         */
        private LinkedHashMap<String, String> result = new LinkedHashMap<>();

        /**
         * 读取一行的回调
         */
        private ExcelReadHandler excelReadHandler;

        /**
         * 构造方法
         *
         * @param sst
         * @param styleTable
         * @param excelReadHandler
         */
        private SheetHandlerUtil(SharedStringsTable sst, StylesTable styleTable, ExcelReadHandler excelReadHandler) {
            this.sst = sst;
            this.styleTable = styleTable;
            this.excelReadHandler = excelReadHandler;
        }

        /**
         * 这个方法在遇到一个xml文件的元素开始之前被触发,取出单元格内存放的内容的类型
         *
         * @param uri
         * @param localName
         * @param name
         * @param attributes
         * @throws SAXException
         */
        @Override
        public void startElement(String uri, String localName, String name,
                                 Attributes attributes) throws SAXException {
            // name为c表示遇到了单元格
            if (name.equals("c")) {
                ref = attributes.getValue("r");
                setNextDataType(attributes);
            }
            // 即将获取单元格的内容,所以置空该变量
            currentContents = "";
        }


        /**
         * 处理数据类型
         *
         * @param attributes 单元格参数
         */
        private void setNextDataType(Attributes attributes) {
            cellDataType = CellDataType.NUMBER; //cellType为空,则表示该单元格类型为数字
            formatIndex = -1;
            formatString = null;
            String cellType = attributes.getValue("t"); //单元格类型
            String cellStyleStr = attributes.getValue("s");
            if ("b".equals(cellType)) { //处理布尔值
                cellDataType = CellDataType.BOOL;
            } else if ("e".equals(cellType)) {  //处理错误
                cellDataType = CellDataType.ERROR;
            } else if ("inlineStr".equals(cellType)) {
                cellDataType = CellDataType.INLINESTR;
            } else if ("s".equals(cellType)) { //处理字符串
                cellDataType = CellDataType.SSTINDEX;
            } else if ("str".equals(cellType)) {
                cellDataType = CellDataType.FORMULA;
            }
            if (cellStyleStr != null) { //处理日期
                int styleIndex = Integer.parseInt(cellStyleStr);
                XSSFCellStyle style = styleTable.getStyleAt(styleIndex);
                formatIndex = style.getDataFormat();
                formatString = style.getDataFormatString();

                if (formatString == null) {
                    cellDataType = CellDataType.NULL;
                    formatString = BuiltinFormats.getBuiltinFormat(formatIndex);
                }

                if (formatString.contains("m/d/yy")) {
                    cellDataType = CellDataType.DATE;
                    formatString = "yyyy-MM-dd hh:mm:ss";
                }
            }
        }

        /**
         * @param value
         * @return
         */
        private String getDataValue(String value) {
            String thisStr;
            switch (cellDataType) {
                case BOOL:
                    char first = value.charAt(0);
                    thisStr = first == '0' ? "FALSE" : "TRUE";
                    break;
                case ERROR:
                    thisStr = "\"ERROR:" + value.toString() + '"';
                    break;
                case FORMULA:
                    thisStr = '"' + value.toString() + '"';
                    break;
                case INLINESTR:
                    XSSFRichTextString rtsi = new XSSFRichTextString(value);
                    thisStr = rtsi.toString();
                    break;
                case SSTINDEX:
                    String sstIndex = value.toString();
                    try {
                        int idx = Integer.parseInt(sstIndex);
                        XSSFRichTextString rtss = new XSSFRichTextString(sst.getEntryAt(idx));//根据idx索引值获取内容值
                        thisStr = rtss.toString();
                        rtss = null;
                    } catch (NumberFormatException ex) {
                        thisStr = value.toString();
                    }
                    break;
                case NUMBER:
                    if (formatString != null) {
                        thisStr = formatter.formatRawCellContents(Double.parseDouble(value), formatIndex, formatString).trim();
                    } else {
                        thisStr = value;
                    }
                    thisStr = thisStr.replace("_", "").trim();
                    break;
                case DATE:
                    thisStr = formatter.formatRawCellContents(Double.parseDouble(value), formatIndex, formatString);
                    thisStr = thisStr.replace("T", " ");
                    break;
                default:
                    thisStr = " ";
                    break;
            }
            return thisStr;
        }

        /**
         * 存储当前单元格的内容
         *
         * @param ch
         * @param start
         * @param length
         */
        @Override
        public void characters(char[] ch, int start, int length) {
            currentContents = new String(ch, start, length);
        }

        /**
         * 读取完单元格的内容后被执行
         *
         * @param uri
         * @param localName
         * @param name
         * @throws SAXException
         */
        @Override
        public void endElement(String uri, String localName, String name) throws SAXException {
            if (name.equals("v")) {
                result.put(ref, getDataValue(currentContents));
            }
            if (name.equals("row")) {
                excelReadHandler.processOneRow(result);
                result.clear();
            }
        }
    }
}

调用方法

String fileName = this.getClass().getClassLoader().getResource("data/skus.xls").getPath();
        ExcelEventUtil.processAllSheets(fileName, (result) -> {
            SkuDO skuDO = new SkuDO();
            ExcelConvertUtil.excelToSkuDO(result, skuDO);
            if (skuDO.getId() != null) {
                handler.handleSku(skuDO);
            }
        });
目前处理Excel的开源javaAPI主要有两种,一是Jxl(Java Excel API),Jxl只支持Excel2003以下的版本。另外一种是Apache的Jakarta POI,相比于Jxl,POI对微软办公文档的支持更加强大,但是它使用复杂,上手慢。POI可支持更高的Excel版本2007。对Excel读取,POI有两种模式,一是用户模式,这种方式同Jxl的使用很似,使用简单,都是将文件一次性读到内存,文件小的时候,没有什么问题,当文件大的时候,就会出现OutOfMemory的内存溢出问题。第二种是事件驱动模式,拿Excel2007来说,其内容采用XML的格式来存储,所以处理excel就是解析XML,而目前使用事件驱动模式解析XML的API是SAX(Simple API for XML),这种模型在读取XML文档时,并没有将整个文档读入内存,而是按顺序将整个文档解析完,在解析过程中,会主动产生事件交给程序中相应的处理函数来处理当前内容。因此这种方式对系统资源要求不高,可以处理海量数据。笔者曾经做过测试,这种方法处理一千万条,每条五列的数据花费大约11分钟。可见处理海量数据的文件事件驱动是一个很好的方式。而本文中用到的AbstractExcel2003Reader、AbstractExcel2007ReaderExcel读取都是采用这种POI的事件驱动模式。至于Excel的写操作,对较高版本的Excel2007,POI提供了很好的支持,主要流程是第一步构建工作薄和电子表格对象,第二步在一个流中构建文本文件,第三步使用流中产生的数据替换模板中的电子表格。这种方式也可以处理海量数据文件。AbstractExcel2007Writer就是使用这种方式进行写操作。对于写入较低版本的Excel2003,POI使用了用户模式来处理,就是将整个文档加载进内存,如果数据量大的话就会出现内存溢出的问题,Excel2003Writer就是使用这种方式。据笔者的测试,如果数据量大于3万条,每条8列的话,就会报OutOfMemory的错误。Excel2003中每个电子表格的记录数必须在65536以下,否则就会发生异常。目前还没有好的解决方案,建议对于海量数据写入操作,尽量使用Excel2007。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值