相关文章:
在日常的开发过程中,当涉及到 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 的处理上,如果使用用户模型的话,操作起来的话就比较耗费时间了,甚至可能会导致内存溢出,因此针对大数据的读取,我们最好采用事件模型来处理
一、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()
方法就可以获得数据源了,剩下的就是处理自身的业务逻辑了
- 如上所示,将 MultipartFile 转换为 File 后,再调用