在Java中,POI是指Apache POI(Poor Obfuscation Implementation),它是一个开源的Java库,用于处理Microsoft Office文档格式文件,如Excel、Word、PowerPoint等。POI提供了一组API,使得开发者可以通过Java代码读取、写入和修改Office文档,可以方便地操作Excel表格、Word文档、PowerPoint演示文稿等。
POI主要包括以下三个组件:
POI-HSSF:用于操作Excel2003及以前版本的.xls格式文件。
POI-XSSF:用于操作Excel2007及以后版本的.xlsx格式文件。
POI-HWPF:用于操作Word97-2003版本的.doc格式文件。
POI-HSLF:提供读写Microso PowerPointr格式档案的功能。
HDGF - 提供读写MicrosoftVisio格式档案的功能。
除了这5个主要的组件,POI还提供了其他的组件,例如POI-XWPF用于操作Word2007及以后版本的.docx格式文件、POI-HSLF用于操作PowerPoint文档等等。
使用POI,开发者可以读取Excel表格数据、创建新的Excel表格、编辑已有的Excel表格、读取Word文档内容等等。同时,POI也支持将文档导出为PDF、HTML等格式。
注意:03和07版本存在兼容性问题,03版本最多只能有65535行
03和07版本的写方法是一样的,但是对象不同。
1.工作簿:
2.工作表:
3.行:
4.列:
org.apache.poi
poi
5.2.2
org.apache.poi
poi-ooxml
5.2.2
joda-time
joda-time
2.10.14
junit
junit
4.12
//03版本
@Test
public void testWrite() {
//1.创建一个工作簿
Workbook workbook = new HSSFWorkbook();
//2.创建一个工作表
Sheet sheet = workbook.createSheet("测试表");
//3.创建一个行(1,1)
Row row1 = sheet.createRow(0);
//4.创建一个单元格1
Cell cell11 = row1.createCell(0);
cell11.setCellValue("表头1");
Cell cell12 = row1.createCell(1);
cell12.setCellValue("001");
//第二行(2,1)
Row row2 = sheet.createRow(1);
Cell cell21 = row2.createCell(0);
cell21.setCellValue("统计时间");
Cell cell22 = row2.createCell(1);
String time = new DateTime().toString("yyyy-MM-dd HH:mm:ss");
cell22.setCellValue(time);
//生成表(Io流) 03版本 xls
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(PATH + "测试表.xls");
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
//输出
try {
workbook.write(outputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
//关闭流
try {
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("文件生成完毕!");
}
//07版本
@Test
public void testWrite07() {
//1.创建一个工作簿
Workbook workbook = new XSSFWorkbook();
//2.创建一个工作表
Sheet sheet = workbook.createSheet("测试表");
//3.创建一个行(1,1)
Row row1 = sheet.createRow(0);
//4.创建一个单元格1
Cell cell11 = row1.createCell(0);
cell11.setCellValue("表头1");
Cell cell12 = row1.createCell(1);
cell12.setCellValue("001");
//第二行(2,1)
Row row2 = sheet.createRow(1);
Cell cell21 = row2.createCell(0);
cell21.setCellValue("统计时间");
Cell cell22 = row2.createCell(1);
String time = new DateTime().toString("yyyy-MM-dd HH:mm:ss");
cell22.setCellValue(time);
//生成表(Io流) 07版本 xlsx
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(PATH + "测试表.xlsx");
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
//输出
try {
workbook.write(outputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
//关闭流
try {
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("文件生成完毕!");
}
注意对象的区别和文件后缀
数据批量导入!
大文件写HSSF:
缺点:最多只能处理65536行,否则就会抛出异常
优点:过程中写入缓存,不操作磁盘,最后一次写入磁盘,速度快
大文件写XSSF:
缺点:写数据时速度慢,非常耗内存,也可能会发生内存溢出,如:100万条时
优点:可以写入较大量数据,如:20万条。
大文件写SXSSF
它是一种基于流式处理的用户模型,适用于处理大量数据时,以减少内存消耗并提高性能。
与传统的基于用户模型的 API(如 HSSF)相比,SXSSF 可以将生成的数据直接写入磁盘,而不是先将数据全部加载到内存中,从而节省了内存开销。这使得 SXSSF 更适合处理大规模数据集或需要导出大型 Excel 文件的场景。
优点:可以写非常大的数据量,如100万条甚至更多,写数据速度快,占用更少内存少。
注意:过程中会产生临时文件,需要清理临时文件,默认由100条记录被保存在内存中,如果超过这数量,则前面的文件将会被写入临时文件。如果想自定义内存中数据的数量,可以使用
new SXSSFWorkbook(数量)
package com.yang;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.Test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
/**
* @author yzh
* @date 2024-01-14
*/
public class ExcelReadTest {
public static final String PATH = "E:\\ExcleTest\\";
@Test
public void readTest03() throws Exception {
//1.获取文件流
FileInputStream fileInputStream = new FileInputStream(PATH + "测试表.xls");
//2..创建一个工作簿 使用excel能操作的工作簿都能操作
Workbook workbook = new HSSFWorkbook(fileInputStream);
//3.获取表
Sheet sheet = workbook.getSheetAt(0);
//4.获取行
Row row = sheet.getRow(0);
//5.获取列
Cell cell = row.getCell(1);
//getStringCellValue获取字符串类型
System.out.println(cell.getStringCellValue());
fileInputStream.close();
}
@Test
public void readTest07() throws Exception {
//1.获取文件流
FileInputStream fileInputStream = new FileInputStream(PATH + "测试表.xlsx");
//2..创建一个工作簿 使用excel能操作的工作簿都能操作
Workbook workbook = new XSSFWorkbook(fileInputStream);
//3.获取表
Sheet sheet = workbook.getSheetAt(0);
//4.获取行
Row row = sheet.getRow(0);
//5.获取列
Cell cell = row.getCell(1);
//getStringCellValue获取字符串类型
System.out.println(cell.getStringCellValue());
fileInputStream.close();
}
}
**注意获取值的类型**
@Test
public void cellTypeTest() throws Exception {
//1.获取文件流
FileInputStream fileInputStream = new FileInputStream(PATH + "权益导入模板.xlsx");
//2..创建一个工作簿 使用excel能操作的工作簿都能操作
Workbook workbook = new XSSFWorkbook(fileInputStream);
Sheet sheet = workbook.getSheetAt(0);
//读取标题内容
Row rowTitle = sheet.getRow(0);
if (rowTitle != null) {
//读取列,得到一行有多少列有数据
int cellCount = rowTitle.getPhysicalNumberOfCells();
for (int cellNum = 0; cellNum < cellCount; cellNum++) {
Cell cell = rowTitle.getCell(cellNum);
if (cell != null) {
String cellValue = cell.getStringCellValue();
System.out.print(cellValue + "|");
}
}
System.out.println();
}
//获取表中的内容
int rowCount = sheet.getPhysicalNumberOfRows();
for (int rowNum = 1; rowNum < rowCount; rowNum++) {
Row rowData = sheet.getRow(rowNum);
if (rowData != null) {//如果行不为空
//读取列
int cellCount = rowTitle.getPhysicalNumberOfCells();
for (int cellNum = 0; cellNum < cellCount; cellNum++) {
Cell cell = rowData.getCell(cellNum);
//匹配列的数据类型
if (cell != null) {
CellType cellType = cell.getCellType();
String cellValue = "";
switch (cellType) {
case STRING:
System.out.println("STRING");
cellValue = cell.getStringCellValue();
break;
case NUMERIC:
System.out.println("NUMERIC");
if (DateUtil.isCellDateFormatted(cell)) {
System.out.println("日期");
Date date = cell.getDateCellValue();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
cellValue = dateFormat.format(date);
} else {
System.out.println("普通数字");
//防止数字过长
cell.setCellType(CellType.STRING);//转换为字符串输出
cellValue = cell.getStringCellValue();
}
break;
case BOOLEAN:
System.out.println("BOOLEAN");
cellValue = String.valueOf(cell.getBooleanCellValue());
break;
case _NONE:
System.out.println("_NONE");
break;
case ERROR:
System.out.println("ERROR");
break;
}
System.out.println(cellValue);
}
}
}
}
fileInputStream.close();
}
//优化后
@Test
public void testReadExcel() throws Exception {
String filePath = PATH + "权益导入模板.xlsx";
try (InputStream is = new FileInputStream(filePath);
Workbook workbook = new XSSFWorkbook(is)) {
Sheet sheet = workbook.getSheetAt(0);
// 创建自定义的DataFormatter对象
DataFormatter dataFormatter = new DataFormatter(Locale.ENGLISH);
// 读取标题行
Row titleRow = sheet.getRow(0);
List titleList = readRowValues(titleRow, new DataFormatter());
// 读取数据行
List> dataList = new ArrayList<>();
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row dataRow = sheet.getRow(i);
if (dataRow == null) {
continue;
}
List rowData = readRowValues(dataRow, new DataFormatter());
dataList.add(rowData);
}
// 输出结果
System.out.println("标题行:" + titleList);
System.out.println("数据行:" + dataList);
}
}
/**
* 读取一行单元格的值
*
* @param row 行对象
* @param dataFormatter 数据格式化对象
* @return 单元格值列表
*/
private List readRowValues(Row row, DataFormatter dataFormatter) {
List values = new ArrayList<>();
for (int i = 0; i < row.getLastCellNum(); i++) {
Cell cell = row.getCell(i);
String value = dataFormatter.formatCellValue(cell);
// 判断是否为日期格式
if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
Date date = cell.getDateCellValue();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
value = dateFormat.format(date);
}
values.add(value);
}
return values;
}
@Test
public void testFormula() throws Exception {
FileInputStream fileInputStream = new FileInputStream(PATH + "求和.xlsx");
Workbook workbook = new XSSFWorkbook(fileInputStream);
Sheet sheet = workbook.getSheetAt(0);
Row row = sheet.getRow(5);
Cell cell = row.getCell(0);
//拿到计算公式
FormulaEvaluator evaluator = new XSSFFormulaEvaluator((XSSFWorkbook)workbook);
//输出单元格的内容
CellType cellType = cell.getCellType();
switch (cellType){
case FORMULA:
String formula = cell.getCellFormula();
System.out.println(formula);
//计算
CellValue evaluate = evaluator.evaluate(cell);
String result = evaluate.formatAsString();
System.out.println(result);
break;
}
}
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便
com.alibaba
easyexcel
2.2.0-beta2
com.alibaba
fastjson
1.2.76
package com.yang.data;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
/**
* @author yzh
* @date 2024-01-14
*/
@Data
public class DemoData {
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String ignore;
}
2.2.3测试
package com.yang;
import com.alibaba.excel.EasyExcel;
import com.yang.data.DemoData;
import org.apache.commons.collections4.ListUtils;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author yzh
* @date 2024-01-14
*/
public class EasyExcelTest {
public static final String PATH = "E:\\ExcleTest\\";
private List data() {
ArrayList 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;
}
/**
* 最简单的写
*
* 1. 创建excel对应的实体对象 参照{@link DemoData}
*
* 2. 直接写即可
*/
@Test
public void simpleWrite() {
String fileName= PATH+"simpleWrite.xlsx";
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
}
}
package com.yang;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import com.yang.data.DemoData;
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();
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private DemoDAO demoDAO;
public DemoDataListener() {
// 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
demoDAO = new DemoDAO();
}
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param demoDAO
*/
public DemoDataListener(DemoDAO demoDAO) {
this.demoDAO = demoDAO;
}
/**
* 这个每一条数据解析都会来调用
*
* @param data
* one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@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();
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
LOGGER.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
demoDAO.save(list);
}
}
package com.yang;
/**
* @author yzh
* @date 2024-01-14
*/
import com.yang.data.DemoData;
import java.util.List;
/**
* 假设这个是你的DAO存储。当然还要这个类让spring管理,当然你不用需要存储,也不需要这个类。
**/
public class DemoDAO {
public void save(List list) {
//持久化操作
// 如果是mybatis,尽量别直接调用多次insert,自己写一个mapper里面新增一个方法batchInsert,所有数据一次性插入
}
}
2.3.3测试
package com.yang;
import com.alibaba.excel.EasyExcel;
import com.yang.data.DemoData;
import org.apache.commons.collections4.ListUtils;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author yzh
* @date 2024-01-14
*/
public class EasyExcelTest {
public static final String PATH = "E:\\ExcleTest\\";
// 最简单的读
@Test
public void simpleRead() {
String fileName = PATH+"simpleWrite.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
}
}
参考文档:关于Easyexcel | Easy Excel
参考视频:【狂神说Java】POI及EasyExcel一小时搞定通俗易懂_哔哩哔哩_bilibili