常用信息
1、将用户信息导出为 excel 表格(导出数据....)
2、将 Excel 表中的信息录入到网站数据库(习题上传....)大大减轻网站录入量!
开发中经常会设计到 excel 的处理,如导出 Excel,导入 Excel 到数据库中!
操作 Excel 目前比较流行的就是 Apache POI 和阿里巴巴的 easyExcel!
Apache POI
Apache POI 官网:Apache POI - the Java API for Microsoft Documents
easyExcel
easyExcel官网地址:https://github.com/alibaba/easyexcel
EasyExcel 是阿里巴巴开源的一个 excel 处理框架,以使用简单、节省内存著称。
EasyExcel 能大大减少占用内存的主要原因是在解析 Excel 时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
内存问题:POI = 100w 先加载到内存 OOM。。在写文件 es=1
下图是 EasyExcel 和 POI 在解析 Excel时的对比图。
创建项目
1、建立一个空项目,创建普通 Maven 的 Moudle kuang-poi
2、引入 pom 依赖
- <dependencies>
-
-
- <dependency>
- <groupId>org.apache.poigroupId>
- <artifactId>poiartifactId>
- <version>3.9version>
- dependency>
-
-
- <dependency>
- <groupId>org.apache.poigroupId>
- <artifactId>poi-ooxmlartifactId>
- <version>3.9version>
- dependency>
-
-
- <dependency>
- <groupId>joda-timegroupId>
- <artifactId>joda-timeartifactId>
- <version>2.10.1version>
- dependency>
-
-
- <dependency>
- <groupId>junitgroupId>
- <artifactId>junitartifactId>
- <version>4.12version>
- dependency>
- dependencies>
03|07 版本的写,就是对象不同,方法一样的!
需要注意:2003 版本和 2007 版本存在兼容性的问题!03 最多只有 65535 行!
1、工作簿: 2、工作表: 3、行: 4、列:
03 版本:
- String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";
-
- @Test
- public void testWirate03() throws IOException {
- // 1、创建一个工作簿 03
- Workbook workbook = new HSSFWorkbook();
- // 2、创建一个工作表
- Sheet sheet = workbook.createSheet("狂神观众统计表");
- // 3、创建一个行 0 代表第一行 (1,1)
- Row row1 = sheet.createRow(0);
- // 4、创建一个单元格 0 代表第一个单元格
- Cell cell11 = row1.createCell(0);
- // 去格子里边写入一个数据 第一行的第一列 (1,1)
- cell11.setCellValue("今日新增观众");
- // 5、创建第二个单元格 1 代表第二个单元格
- Cell cell12 = row1.createCell(1);
- // 去格子里边写入一个数据 第一行的第二列 (1,2)
- cell12.setCellValue(666);
-
- // 第二行
- Row row2 = sheet.createRow(1);
- // 第二行的第一列 (2,1)
- Cell cell21 = row2.createCell(0);
- cell21.setCellValue("统计时间");
- // 第二行的第二列 (2,2)
- Cell cell22 = row2.createCell(1);
- String time = new DateTime().toString("yyyy-MM-dd HH:mm:ss");
- cell22.setCellValue(time);
-
- // 生成一张表(IO 流) 03 版本就是使用 xls 结尾!
- FileOutputStream fileOutputStream = new FileOutputStream(PATH + "狂神观众统计表03.xls");
- // 输出
- workbook.write(fileOutputStream);
- // 关闭流
- fileOutputStream.close();
-
- System.out.println("狂神观众统计表03 生成完毕!");
- }
07 版本:
- String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";
-
- @Test
- public void testWirate07() throws IOException {
- // 1、创建一个工作簿 07
- Workbook workbook = new XSSFWorkbook();
- // 2、创建一个工作表
- Sheet sheet = workbook.createSheet("狂神观众统计表");
- // 3、创建一个行 0 代表第一行 (1,1)
- Row row1 = sheet.createRow(0);
- // 4、创建一个单元格 0 代表第一个单元格
- Cell cell11 = row1.createCell(0);
- // 去格子里边写入一个数据 第一行的第一列 (1,1)
- cell11.setCellValue("今日新增观众");
- // 5、创建第二个单元格 1 代表第二个单元格
- Cell cell12 = row1.createCell(1);
- // 去格子里边写入一个数据 第一行的第二列 (1,2)
- cell12.setCellValue(777);
-
- // 第二行
- Row row2 = sheet.createRow(1);
- // 第二行的第一列 (2,1)
- Cell cell21 = row2.createCell(0);
- cell21.setCellValue("统计时间");
- // 第二行的第二列 (2,2)
- Cell cell22 = row2.createCell(1);
- String time = new DateTime().toString("yyyy-MM-dd HH:mm:ss");
- cell22.setCellValue(time);
-
- // 生成一张表(IO 流) 07 版本就是使用 xlsx 结尾!
- FileOutputStream fileOutputStream = new FileOutputStream(PATH + "狂神观众统计表07.xlsx");
- // 输出
- workbook.write(fileOutputStream);
- // 关闭流
- fileOutputStream.close();
-
- System.out.println("狂神观众统计表07 生成完毕!");
-
- }
注意对象的一个区别,文件后缀
大文件写 HSSF
缺点:最多只能处理 65536 行,否则会抛出异常
java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)
优点:过程写入缓存,不操作磁盘,最后一次性写入磁盘,速度快
- String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";
-
- @Test
- public void testWirate03BigData() throws IOException {
- // 获取当前时间
- long begin = System.currentTimeMillis();
-
- // 创建工作簿
- Workbook workook = new HSSFWorkbook();
- // 创建工作表
- Sheet sheet = workook.createSheet();
- // 写入数据 超过 65536 行就会报错 java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)
- for (int rowNum = 0; rowNum < 65536; rowNum++) {
- // 创建行
- Row row = sheet.createRow(rowNum);
- for (int cellNum = 0; cellNum < 10; cellNum++) {
- // 创建列
- Cell cell = row.createCell(cellNum);
- cell.setCellValue(cellNum);
- }
- }
-
- System.out.println("over");
-
- // 生成一张表(io 流) 03 版本就是使用 xls 结尾!
- FileOutputStream fileOutputStream = new FileOutputStream(PATH + "testWirate03BigData.xls");
- // 输出
- workook.write(fileOutputStream);
- // 关闭流
- fileOutputStream.close();
- long end = System.currentTimeMillis();
- double time = (double) (end - begin) / 1000;
- System.out.println("时间差为:" + time);
- }
大文件写 XSSF
缺点:写数据时速度非常慢,非常耗内存,也会发生内存溢出,如 100 万条
优点:可以写较大的数据量,如 20 万条
- String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";
-
- // 耗时较长! 优化,缓存
- @Test
- public void testWirate07BigData() throws IOException {
- // 获取当前时间
- long begin = System.currentTimeMillis();
-
- // 创建工作簿
- Workbook workook = new XSSFWorkbook();
- // 创建工作表
- Sheet sheet = workook.createSheet();
- // 写入数据
- for (int rowNum = 0; rowNum < 100000; rowNum++) {
- // 创建行
- Row row = sheet.createRow(rowNum);
- for (int cellNum = 0; cellNum < 10; cellNum++) {
- // 创建列
- Cell cell = row.createCell(cellNum);
- cell.setCellValue(cellNum);
- }
- }
-
- System.out.println("over");
-
- // 生成一张表(io 流) 07 版本就是使用 xlsx 结尾!
- FileOutputStream fileOutputStream = new FileOutputStream(PATH + "testWirate07BigData2.xlsx");
- // 输出
- workook.write(fileOutputStream);
- // 关闭流
- fileOutputStream.close();
- long end = System.currentTimeMillis();
- double time = (double) (end - begin) / 1000;
- System.out.println("时间差为:" + time);
- }
大文件写 SXSSF
优点:可以写非常大的数据量,如 100 万条甚至更多条,写数据速度更快,占用更少内存
注意:
过程中会产生临时文件,需要清理临时文件
默认由 100 条记录被保存在内存中,如果超过这数量,则最前面的数据被写入临时文件
如果想自定义内存中的数量,可以使用 new SXSSFWorkbook(数量)
- String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";
-
- @Test
- public void testWirate07BigDataS() throws IOException {
- // 获取当前时间
- long begin = System.currentTimeMillis();
-
- // 创建工作簿
- Workbook workbook = new SXSSFWorkbook();
- // 创建工作表
- Sheet sheet = workbook.createSheet();
- // 写入数据
- for (int rowNum = 0; rowNum < 1000000; rowNum++) {
- // 创建行
- Row row = sheet.createRow(rowNum);
- for (int cellNum = 0; cellNum < 10; cellNum++) {
- // 创建列
- Cell cell = row.createCell(cellNum);
- cell.setCellValue(cellNum);
- }
- }
-
- System.out.println("over");
-
- // 生成一张表(io 流) 07 版本就是使用 xlsx 结尾!
- FileOutputStream fileOutputStream = new FileOutputStream(PATH + "testWirate07BigDataS2.xlsx");
- // 输出
- workbook.write(fileOutputStream);
- // 关闭流
- fileOutputStream.close();
- // 清除临时文件!
- ((SXSSFWorkbook) workbook).dispose();
- long end = System.currentTimeMillis();
- double time = (double) (end - begin) / 1000;
- System.out.println("时间差为:" + time);
- }
-
SXSSFWorkbook 来自官方的解释:实现"BigGridDemo"策略的流式 XSSFWorkbook 版本。这允许写入非常大的文件而不会耗尽内存,因为任何时候只有可配置的行部分被保存在内存中。
请注意:仍然可能会消耗大量内存,这些内存基于您正在使用的功能,例如合并区域,注释......仍然只存储在内存中,因此如果广泛使用,可能需要大量内存。
再使用 POI 的时候!内存问题 Jprofile
03|07
03 版本
- String PATH = "/Users/zhang/Desktop/gitee/excel/"; // zhang-poi
-
- @Test
- public void testRead03() throws IOException {
-
- // 获取文件流
- FileInputStream fileInputStream = new FileInputStream(PATH + "zhang-poi狂神观众统计表03.xls");
- // 1、 创建一个工作簿 将文件流放到工作簿里 使用 excel 能操作的这边也都可以操作
- Workbook workbook = new HSSFWorkbook(fileInputStream);
- // 2、得到表 获取第 1 个表
- Sheet sheetAt = workbook.getSheetAt(0);
- // 3、得到行 获取第 1 行
- Row row = sheetAt.getRow(0);
- // 4、得到列 获取第一个单元格里边的数据
- Cell cell = row.getCell(1);
-
- // 读取值的时候一定要注意类型,否则就会失败
-
- // 取出 cell 的值 getStringCellValue() 获取字符串类型
- // System.out.println("cell.getStringCellValue() = " + cell.getStringCellValue());
- System.out.println("cell.getNumericCellValue() = " + cell.getNumericCellValue());
- fileInputStream.close();
-
- }
07 版本
- String PATH = "/Users/zhang/Desktop/gitee/excel/"; // zhang-poi
-
- @Test
- public void testRead07() throws IOException {
-
- // 获取文件流
- FileInputStream fileInputStream = new FileInputStream(PATH + "zhang-poi狂神观众统计表07.xlsx");
- // 1、 创建一个工作簿 将文件流放到工作簿里 使用 excel 能操作的这边也都可以操作
- Workbook workbook = new XSSFWorkbook(fileInputStream);
- // 2、得到表 获取第 1 个表
- Sheet sheetAt = workbook.getSheetAt(0);
- // 3、得到行 获取第 1 行
- Row row = sheetAt.getRow(0);
- // 4、得到列 获取第一个单元格里边的数据
- Cell cell = row.getCell(1);
-
- // 读取值的时候一定要注意类型,否则就会失败
-
- // 取出 cell 的值 getStringCellValue() 获取字符串类型
- // System.out.println("cell.getStringCellValue() = " + cell.getStringCellValue());
- System.out.println("cell.getNumericCellValue() = " + cell.getNumericCellValue());
- fileInputStream.close();
-
- }
注意获取值得类型即可
读不同的数据类型(最麻烦的就是这里)
- String PATH = "/Users/zhang/Desktop/gitee/excel/"; // zhang-poi
-
- @Test
- public void testCellType() throws IOException {
- // 获取文件流
- FileInputStream fileInputStream = new FileInputStream(PATH + "明细表.xls");
- // 1、 创建一个工作簿 将文件流放到工作簿里 使用 excel 能操作的这边也都可以操作
- Workbook workbook = new HSSFWorkbook(fileInputStream);
- // 2、得到表 获取第 1 个表
- Sheet sheetAt = workbook.getSheetAt(0);
- // 获取标题内容
- Row rowTitle = sheetAt.getRow(0);
- if (rowTitle != null) {
- // 读取行的列 数量
- int cellCount = rowTitle.getPhysicalNumberOfCells();
- for (int cellNum = 0; cellNum < cellCount; cellNum++) {
- // 获取单元格里边的数据 根据传入的第几列
- Cell cell = rowTitle.getCell(cellNum);
- if (cell != null) {
- int cellType = cell.getCellType();
-
- // 根据单元格里边的数据类型,把具体的值给读取出来
- String cellValue = cell.getStringCellValue();
- System.out.print(cellValue + "|");
- }
- }
- System.out.println();
- }
-
- // 获取表中的内容 获取表中的所有行 ?
- int rowCount = sheetAt.getPhysicalNumberOfRows();
- for (int rowNum = 1; rowNum < rowCount; rowNum++) {
- // 获取行里边的内容
- Row rowData = sheetAt.getRow(rowNum);
- if (rowData != null) {
- // 读取行中的列
- int cellCount = rowTitle.getPhysicalNumberOfCells();
- for (int cellNum = 0; cellNum < cellCount; cellNum++) {
- System.out.print("[" + (rowNum+1) + "-" +(cellNum+1) + "]");
-
- // 获取单元格里边的数据 根据每列
- Cell cell = rowData.getCell(cellNum);
- // 匹配列的数据类型
- if (cell != null) {
- int cellType = cell.getCellType();
- String cellValue = "";
- switch (cellType){
- case Cell.CELL_TYPE_STRING: // 字符串
- System.out.print("【String】");
- cellValue = cell.getStringCellValue();
- break;
-
- case Cell.CELL_TYPE_BOOLEAN: // 布尔类型
- System.out.print("【Boolean】");
- cellValue = String.valueOf(cell.getBooleanCellValue());
- break;
-
- case Cell.CELL_TYPE_BLANK: // 为空
- System.out.print("【Blank】");
- break;
-
- case Cell.CELL_TYPE_NUMERIC: // 数字(日期、普通数字)类型
- System.out.print("【Numeric】");
- // 判断当前列是不是时间日期
- if (HSSFDateUtil.isCellDateFormatted(cell)) {
- System.out.print("【日期】");
- Date date = cell.getDateCellValue();
- cellValue = new DateTime(date).toString("yyyy-MM-dd");
- }else {
- // 不是日期格式,防止数字过长
- System.out.print("【转换为字符串输出】");
- cell.setCellType(Cell.CELL_TYPE_STRING);
- cellValue = cell.toString();
- }
- break;
-
- case Cell.CELL_TYPE_ERROR: // 布尔类型
- System.out.print("【数据类型错误】");
- cellValue = String.valueOf(cell.getErrorCellValue());
- break;
-
- case Cell.CELL_TYPE_FORMULA: // 公式类型,代表这个cell存的是一个公式
- System.out.print("【公式】");
- // 6、获取计算公式
- FormulaEvaluator formulaEvaluator = new HSSFFormulaEvaluator((HSSFWorkbook) workbook);
- // 8、得到第 5 行第 1 列 计算公式的内容
- String formula = cell.getCellFormula();
- // formula = SUM(A2:A4)
- System.out.println("formula = " + formula);
-
- // 9、计算得到值 把获取到第 5 行,第 1 列的内容放入到
- CellValue evaluate = formulaEvaluator.evaluate(cell);
- // 10、将的到的内容转化为字符串
- cellValue = evaluate.formatAsString();
- // cellValue = 600.0
- // System.out.println("cellValue = " + cellValue);
- break;
- }
- System.out.println(cellValue);
- }
- }
- }
- }
- fileInputStream.close();
- }
计算公式(了解即可)
- // 计算公式
- @Test
- public void testFormula() throws IOException {
- // 1、获取文件流 通过文件路径 加 文件名
- FileInputStream fileInputStream = new FileInputStream( PATH + "公式表.xls" );
- // 2、创建一个工作簿,并把文件流放入工作簿当中
- Workbook workbook = new HSSFWorkbook(fileInputStream);
- // 3、得到表 获取第一个表
- Sheet sheetAt = workbook.getSheetAt(0);
-
- // 4、获取到第 5 行的内容
- Row row = sheetAt.getRow(4);
- // 5、获取第 5 行,第 1 列的内容
- Cell cell = row.getCell(0);
-
- // 6、获取计算公式
- FormulaEvaluator formulaEvaluator = new HSSFFormulaEvaluator((HSSFWorkbook) workbook);
- // 7、匹配列的数据类型 输出单元格的内容
- int cellType = cell.getCellType();
- switch (cellType){
- case Cell.CELL_TYPE_FORMULA: // 公式
- // 8、得到第 5 行第 1 列 计算公式的内容
- String formula = cell.getCellFormula();
- // formula = SUM(A2:A4)
- System.out.println("formula = " + formula);
-
- // 9、计算得到值 把获取到第 5 行,第 1 列的内容放入到
- CellValue evaluate = formulaEvaluator.evaluate(cell);
- // 10、将的到的内容转化为字符串
- String cellValue = evaluate.formatAsString();
- // cellValue = 600.0
- System.out.println("cellValue = " + cellValue);
- break;
- }
-
- }
EasyExcel操作
导入依赖
- <dependency>
- <groupId>com.alibabagroupId>
- <artifactId>easyexcelartifactId>
- <version>3.1.1version>
- dependency>
-
- <dependency>
- <groupId>com.alibabagroupId>
- <artifactId>fastjsonartifactId>
- <version>1.2.79version>
- dependency>
写入测试
1、DemoData.java
- package com.zhang.easyexcelpojo;
-
- import com.alibaba.excel.annotation.ExcelIgnore;
- import com.alibaba.excel.annotation.ExcelProperty;
- import lombok.Data;
-
- import java.util.Date;
-
- @Data
- public class DemoData {
- @ExcelProperty("字符串标题")
- private String string;
- @ExcelProperty("日期标题")
- private Date date;
- @ExcelProperty("数字标题")
- private Double doubleData;
- /**
- * 忽略这个字段
- */
- @ExcelIgnore
- private String ignore;
- }
-
2、测试写入数据:
- String PATH = "/Users/zhang/Desktop/gitee/excel/";
-
- private List
data() { - List
list = new ArrayList(); - for (int i = 0; i < 10; i++) {
- DemoData data = new DemoData();
- data.setString("字符串" + i);
- data.setDate(new Date());
- data.setDoubleData(0.56);
- list.add(data);
- }
- return list;
- }
-
- // 根据 list 写入 excel
- @Test
- public void simplewrite(){
- String fileName = PATH + "EasyTest.xlsx";
- //这里需要指定写用哪个 class去写,然后写到第一个 sheet,名字为模板然后文件流会自动关闭
- // 如果这里想使用03 则 传入excelType参数即可
- //write(fileName,格式类)
- //sheet(表名)
- //doWrite(数据),真实的就是从数据库中读取的或前端传过来的
- EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
- }
3、最终的结果:
读取测试
测试读取:
1、创建类 DemoDataListener 监听器:
- package com.zhang.easyexcelpojo;
-
- import com.alibaba.excel.context.AnalysisContext;
- import com.alibaba.excel.event.AnalysisEventListener;
- import com.alibaba.fastjson.JSON;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-
- import java.util.ArrayList;
- import java.util.List;
-
- // 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
- public class DemoDataListener extends AnalysisEventListener
{ -
- private static final Logger LOGGER = LoggerFactory.getLogger(DemoDataListener.class);
-
- /**
- * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
- */
-
- private static final int BATCH_COUNT = 5;
- List
list = new ArrayList(); -
- private DemoDAO demoDAO;
- public DemoDataListener() {
- demoDAO = new DemoDAO();
- }
- public DemoDataListener(DemoDAO demoDAO) {
- this.demoDAO = demoDAO;
- }
-
- // 读取数据 会执行 invoke 方法 这个每一条数据解析都会来调用
- // DemoData 类型
- // AnalysisContext 分析上下文
- @Override
- public void invoke(DemoData data, AnalysisContext context) {
- System.out.println(JSON.toJSONString(data));
- list.add(data);
- System.out.println(list);
- // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
- if (list.size() >= BATCH_COUNT) {
- saveData(); //
- // 存储完成清理 list
- list.clear();
- }
- }
-
- // 所有数据解析完成了 都会来调用
- @Override
- public void doAfterAllAnalysed(AnalysisContext context) {
- // 这里也要保存数据,确保最后遗留的数据也存储到数据库
- saveData();
- LOGGER.info("所有数据解析完成!");
- }
-
- // 加上存储数据库
- private void saveData() {
- demoDAO.save(list);
- }
- }
具体操作:
2、持久层DemoDAO:
- package com.zhang.easyexcelpojo;
-
- import java.util.List;
-
- /**
- * 假设这个是你的DAO存储。当然还要这个类让spring管理,当然你不用需要存储,也不需要这个类。
- **/
- public class DemoDAO {
-
- public void save(List
list) { - // 持久化操作
- // 如果是mybatis,尽量别直接调用多次insert,自己写一个mapper里面新增一个方法batchInsert,所有数据一次性插入
- }
-
- }
3、测试:
- // 最简单的读
- @Test
- public void simpleRead() {
- String fileName = PATH + "EasyTest.xlsx";
- // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
- EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
- }
4、结果:
固定套路:
1、写入、固定类格式进行写入
2、读取、根据监听器设置的规则进行读取!
学习方式
了解,面向对象的思想,学会面向接口编程!
理解使用测试 API!
可以把EasyExcel的所有 api 测试一下(2-3小时!)