POI -- 01 -- Excel大数据读取

原文链接:POI – 01 – Excel大数据读取


相关文章:


在日常的开发过程中,当涉及到 Excel 报表导出的时候,我们通常会使用 POI 来进行处理

POI 是 Apache 软件基金会下一个开放源码的函式库,提供了相关 API 供 Java 程序对 Microsoft Office 文档进行读写操作,POI 提供了两种读写 API 模型,分别是:用户模型 (usermodel) 和事件模型 (eventmodel)

在这里插入图片描述

  • 用户模型

    • 基于内存树 (memory tree) 的方式实现,使用 DOM 对 Excel 进行解析

    • 对 CPU 和内存的消耗较大,但可以通过面向对象的方式来进行操作,使用简单,可读可写

  • 事件模型

    • 基于流 (stream) 的方式实现,使用 SAX (Simple API for XML) 模型对 Excel 进行解析

    • 对 CPU 和内存的消耗较小,但使用复杂,且无法进行写操作

  • 在对大数据 Excel 的处理上,如果使用用户模型的话,操作起来的话就比较耗费时间了,甚至可能会导致内存溢出,因此针对大数据的读取,我们最好采用事件模型来处理


官方文档:POI-HSSF and POI-XSSF/SXSSF

官方文档:Event API (HSSF Only)


一、Excel 工具类

  • 工作表处理器

    public class SheetHandler extends DefaultHandler {
    
        /**
         * 共享字符串表
         */
        private SharedStringsTable sst;
        /**
         * 单元格内容
         */
        private String lastContents;
        /**
         * 字符串标识
         */
        private boolean nextIsString;
        /**
         * 单元格位置
         */
        private String cellPosition;
        /**
         * 单元格数据(单元格位置 + 单元格内容)
         */
        private LinkedHashMap<String, String> cellContents = new LinkedHashMap<>();
    
        public LinkedHashMap<String, String> getCellContents() {
            return cellContents;
        }
    
        public void setCellContents(LinkedHashMap<String, String> cellContents) {
            this.cellContents = cellContents;
        }
    
        public void createCellContents() {
            this.cellContents = new LinkedHashMap<>();
        }
    
        public SheetHandler(SharedStringsTable sst) {
            this.sst = sst;
        }
    
        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
            // 判断标签名是否为c (c表示单元格)
            if ("c".equals(name)) {
                // 获取单元格的r属性 (r表示单元格位置)
                cellPosition = attributes.getValue("r");
                // 获取单元格的t属性 (t表示单元格类型;注意:类型为数字、日期的单元格没有t属性)
                String cellType = attributes.getValue("t");
                // 判断单元格类型是否为字符串 (s表示字符串)
                if (cellType != null && "s".equals(cellType)) {
                    nextIsString = true;
                } else {
                    nextIsString = false;
                }
            }
            // 清除缓存内容
            lastContents = "";
        }
    
        @Override
        public void endElement(String uri, String localName, String name) throws SAXException {
            if (nextIsString) {
                int idx = Integer.parseInt(lastContents);
                lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
                nextIsString = false;
            }
    
            // 判断标签名是否为v (v表示单元格内容)
            if ("v".equals(name)) {
                // 通常情况下,第一行为表头,我们可以将其去掉
                boolean headLine = cellPosition.length() == 2 && !"1".equals(cellPosition.substring(1));
                boolean dataLine = cellPosition.length() != 2;
                if (headLine || dataLine){
                    cellContents.put(cellPosition, lastContents);
                }
            }
        }
    
        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            lastContents += new String(ch, start, length);
        }
    }
    
  • Excel 导入工具类

    public class LargeExcelImportUtil {
    
        /**
         * sheet处理器
         */
        private SheetHandler sheetHandler;
        /**
         * 单元格数据(单个sheet)
         */
        private LinkedHashMap<String, String> cellContents = new LinkedHashMap<>();
        /**
         * 单元格数据(多个sheet)
         */
        private LinkedHashMap<String, LinkedHashMap<String, String>> allCellContents = new LinkedHashMap<>();
    
        public SheetHandler getSheetHandler() {
            return sheetHandler;
        }
    
        public void setSheetHandler(SheetHandler sheetHandler) {
            this.sheetHandler = sheetHandler;
        }
    
        public LinkedHashMap<String, String> getCellContents() {
            return cellContents;
        }
    
        public void setCellContents(LinkedHashMap<String, String> cellContents) {
            this.cellContents = cellContents;
        }
    
        public LinkedHashMap<String, LinkedHashMap<String, String>> getAllCellContents() {
            return allCellContents;
        }
    
        public void setAllCellContents(LinkedHashMap<String, LinkedHashMap<String, String>> allCellContents) {
            this.allCellContents = allCellContents;
        }
    
        /**
         * 处理单个sheet
         *
         * @param filePath
         * @throws Exception
         */
        public void processOneSheet(String filePath) {
            InputStream sheet = null;
            OPCPackage pkg = null;
            try {
                pkg = OPCPackage.open(filePath);
                XSSFReader r = new XSSFReader(pkg);
                SharedStringsTable sst = r.getSharedStringsTable();
                XMLReader parser = fetchSheetParser(sst);
                // rId1表示当前Excel的第一个sheet
                sheet = r.getSheet("rId1");
                InputSource sheetSource = new InputSource(sheet);
                parser.parse(sheetSource);
                setCellContents(sheetHandler.getCellContents());
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (pkg != null) {
                        pkg.close();
                    }
                    if (sheet != null) {
                        sheet.close();
                    }
                } catch (Exception e) {  
                    e.printStackTrace();  
                } 
            }
        }
    
        /**
         * 处理多个sheet
         * 
         * @param filePath
         * @throws Exception
         */
        public void processAllSheets(String filePath) {
            OPCPackage pkg = null;
            InputStream sheet = null;
            try{
                pkg = OPCPackage.open(filePath);
                XSSFReader r = new XSSFReader(pkg);
                SharedStringsTable sst = r.getSharedStringsTable();
                XMLReader parser = fetchSheetParser(sst);
                Iterator<InputStream> sheets = r.getSheetsData();
                int count = 1;
                LinkedHashMap<String, LinkedHashMap<String, String>> allCellContents = new LinkedHashMap<>();
                while (sheets.hasNext()) {
                    sheet = sheets.next();
                    InputSource sheetSource = new InputSource(sheet);
                    sheetHandler.createCellContents();
                    parser.parse(sheetSource);
                    setCellContents(sheetHandler.getCellContents());
                    allCellContents.put("sheet" + count, cellContents);
                    count++;
                }
                setAllCellContents(allCellContents);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (pkg != null) {
                        pkg.close();
                    }
                    if (sheet != null) {
                        sheet.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         * 获取sheet解析器
         *
         * @param sst
         * @return
         * @throws SAXException
         */
        private XMLReader fetchSheetParser(SharedStringsTable sst) {
            XMLReader parser = new SAXParser();
            setSheetHandler(new SheetHandler(sst));
            parser.setContentHandler(sheetHandler);
            return parser;
        }
    }
    

二、导入测试

  • 需要自行准备一个有 10w 条数据的 Excel

  • 处理单个 sheet

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        LargeExcelImportUtil example = new LargeExcelImportUtil();
        example.processAllSheets("C:\\Users\\Qi\\Desktop\\Test.xlsx");
        LinkedHashMap<String, String> cellContents = example.getCellContents();
        int count = 0;
        for (Map.Entry<String, String> entry : cellContents.entrySet()) {
            System.out.println(entry.getKey() + " ===> " + entry.getValue());
            count++;
        }
        long end = System.currentTimeMillis();
        System.out.println("共处理【" + count + "】条数据,共耗时【" + (end - start) + "】毫秒" );
    }
    
    // 共处理【100000】条数据,共耗时【3060】毫秒
    
  • 处理多个 sheet

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        LargeExcelImportUtil example = new LargeExcelImportUtil();
        example.processAllSheets("C:\\Users\\Qi\\Desktop\\Test.xlsx");
        LinkedHashMap<String, LinkedHashMap<String, String>> allCellContents = example.getAllCellContents();
        int count = 0;
        for (Map.Entry<String, LinkedHashMap<String, String>> mapEntry : allCellContents.entrySet()) {
            System.out.println(mapEntry.getKey());
            for (Map.Entry<String, String> entry : mapEntry.getValue().entrySet()) {
                System.out.println(entry.getKey() + " ===> " + entry.getValue());
                count++;
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("共处理【"+count+"】条数据,共耗时【"+(end - start)+"】毫秒" );
    }
    
    // 共处理【300000】条数据,共耗时【4997】毫秒
    
  • 从上面的测试中,我们可以看出,采用事件模型来读取大数据的 Excel 时,速度是相当之快的


三、解析

  • 在使用事件模型的过程中我们会发现出现了许多莫名其妙的标签,不知道是从哪里来的,刚开始看官方文档的时候一脸茫然,后来了解到其实是将 Excel 转换成了一个 xml 文件,那些标签都是 xml 文件中的节点,下面我们来讲讲如何查看这个 xml 文件

    • 将 Excel 文件后缀改为 .zip

    • 双击打开压缩文件,里面会有一个 worksheets 文件夹

      在这里插入图片描述

    • 进入 worksheets 文件夹后会有一个 sheet1.xml 文件 (即当前工作表)

      在这里插入图片描述

    • sheet1.xml 里存放了 Excel 中的所有数据,格式如下

      <c r="A2" s="3" t="s"><v>1</v></c>
      
      • c 标签表示单元格,r 属性表示是哪个单元格

      • s 属性暂时不清楚什么含义,t 属性表示单元格类型,v 标签表示单元格内容

  • 通过上面几个步骤,我们就可以很清楚的知道了这些标签的由来,使用起来也就更加方便了


  • PS:通常情况下,我们会通过 web 页面上传 Excel 进行解析,那么问题来了,上面用到的方法解析的是 Excel 文件的绝对地址,那我们该怎么办呢?

  • 不用着急,OPCPackage.open() 这个方法还有个重载体可以接收一个 File 对象

    public void processOneSheet(File file) {
        InputStream sheet = null;
        OPCPackage pkg = null;
        try {
            pkg = OPCPackage.open(file);
            XSSFReader r = new XSSFReader(pkg);
            SharedStringsTable sst = r.getSharedStringsTable();
            XMLReader parser = fetchSheetParser(sst);
            // rId1表示当前Excel的第一个sheet
            sheet = r.getSheet("rId1");
            InputSource sheetSource = new InputSource(sheet);
            parser.parse(sheetSource);
            setCellContents(sheetHandler.getCellContents());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (pkg != null) {
                    pkg.close();
                }
                if (sheet != null) {
                    sheet.close();
                }
            } catch (Exception e) {  
                e.printStackTrace();  
            } 
        }
    }
    
  • 此外,由于后端接收到的文件格式为 MultipartFile 类型的,不是 File 类型,所以需要进行转换,步骤如下

    @PostMapping(value = "/import")
    public void importUserAndEle(@RequestParam("excel") MultipartFile excel) {
        try {
            // 用uuid作为文件名,防止生成的临时文件重复
            final File excelFile = File.createTempFile(UUID.randomUUID().toString(), ".xlsx");
            // MultipartFile to File
            excel.transferTo(excelFile);
            LargeExcelParseUtil largeExcelParseUtil = new LargeExcelParseUtil();
            largeExcelParseUtil.processOneSheet(excelFile);
            deleteFile(excelFile);
            LinkedHashMap<String, String> cellContents = largeExcelParseUtil.getCellContents();
            
            // ......
            
        } catch (Exception e) {
           e.printStackTrace();
        }
    }
    
    private void deleteFile(File... files) {
        for (File file : files) {
            if (file.exists()) {
                file.delete();
            }
        }
    }
    
    • 如上所示,将 MultipartFile 转换为 File 后,再调用 processOneSheet()方法,然后通过 getCellContents() 方法就可以获得数据源了,剩下的就是处理自身的业务逻辑了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值