poi可以将指定目录下的Excel中的内容解析、读取到java程序中。下面是一个Demo:
使用poi需要导下包,如下:
首先是准备读取的Excel表,存放在“E:\programming\备份数据\工单分析结果_2020年7月.xlsx”下:
其他的sheet页(一共9个可见Sheet页),和下面图中的内容也都类似,只是数据稍微不同。
为了将Excel表中的数据保存在java程序中,下面创建了一个WorkOrder工单的实体类:
先大概看下主线程中的代码:
poi解析Excel内容,是需要先创建一个工作簿对象的。
但是因为Microsoft Excel的版本不同,所以文件后缀名也有所不同,例如.xls和.xlsx。
所以我们需要根据解析的Excel的后缀名,自动创建出对应类型的工作簿对象,代码如下:
/**
* HSSF读写.xls格式文件
* XSSF读写.xlsx格式文件
* SXSSF读写.xlsx格式文件
* HWPE读写doc格式文件
* HSLF读写PowerPoint文件
* HDGF读写visio格式文件
* HPBF读写oublisher格式文件
* HSMF读写outlook文件
*/
/**
* 根据后缀名创建对应的工作簿对象
* @param fileType
* @param is
* @return WorkBook
* @throws IOException
*/
public static Workbook getWorkBook(String fileType,InputStream is) throws IOException {
//根据文件后缀名确定需要创建的工作簿对象(这里只解析.xls和.xlsx后缀名的Excel)
if(fileType.equalsIgnoreCase("xls")){
workbook = new HSSFWorkbook(is);
}else if(fileType.equalsIgnoreCase("xlsx")){
workbook = new XSSFWorkbook(is);
}
return workbook;
}
有了自动创建工作簿对象类型的方法之后,我们开始编写读取Excel的方法readExcel(),如下:
/**
* 读取Excel
* @param FileUrl
* @return String
*/
public static List<WorkOrder> readExcel(String FileUrl){
//Workbook workbook = null;
FileInputStream fis = null;
try {
String fileType = FileUrl.substring(FileUrl.lastIndexOf(".")+1,FileUrl.length()); //获取文件后缀名
File file = new File(FileUrl); //获取Excel对象
if(!file.exists()){ //判断路径是否正确
System.out.println("文件不存在!");
return null;
}
fis = new FileInputStream(file); //将文件对象写入流
workbook = getWorkBook(fileType,fis); //根据文件后缀名,自动创建对应类型的工作簿对象
List<WorkOrder> resulet = parseExcel(workbook); //解析Excel,获取工作簿中的值
return resulet;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(null != fis){
try {
fis.close(); //关闭流对象
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
解析Ecxel的方法需要我们自己编写,如下:
/**
* 解析Excel中的内容
* @param workbook
* @return List<WorkOrder>
*/
public static List<WorkOrder> parseExcel(Workbook workbook){
List<WorkOrder> resultList = new ArrayList<>(); //创建返回值接收对象
//解析Sheet数量,确定循环解析次数(这里不做Sheet的隐蔽性校验)
for (int sheetNum=0; sheetNum<workbook.getNumberOfSheets(); sheetNum++){
sheet = workbook.getSheetAt(sheetNum); //根据下标获取每个Sheet页对象(下标从0开始)
if(sheet==null){ //检验sheet合法性
continue;
}
Row firstRow = sheet.getRow(sheet.getFirstRowNum()); //获取第一行数据
if(null == firstRow){ //判断第一行数据是否为空
System.out.println("第一行没有读取到任何数据!");
}
//解析每行数据,构造数据对象
int rowStart = sheet.getFirstRowNum() + 2; //根据模板确定开始解析内容的行标
int rowEnd = sheet.getPhysicalNumberOfRows(); //结束的行标
for(int rowNum=rowStart; rowNum<rowEnd; rowNum++){
Row row = sheet.getRow(rowNum); //根据下标逐次获取行对象
//判断该行对象(及第一个单元格)是否为空
if(row==null || "".equals(convertCellValueToString(row.getCell(0))) || null==convertCellValueToString(row.getCell(0))){
continue;
}
WorkOrder workOrder = convertRowToData(row); //自定义方法,用于解析行中单元格内容,并将每行内容封装为一个WorkOrder对象
if(workbook == null){
System.out.println("获取的数据为空!");
}
resultList.add(workOrder);
}
}
return resultList;
}
Excel的单元格中,填写的内容可能是日期、数字、字符串、布尔值等,也有可能是其他的意想不到的类型。
为了能够顺利解析内容,我们需要自定义一个单元格内容类型转换为String类型的方法——convertRowToData(),如下:
/**
* 类型转换(无论什么类型的值都将转换为String类型)
* @param cell
* @return String
*/
public static String convertCellValueToString(Cell cell){
FormulaEvaluator formulaEvaluator = workbook.getCreationHelper().createFormulaEvaluator(); //用于解析和处理Cell公式的接口对象
if(cell==null){ //判断单元格对象是否为空(这里其实可以不做判断,因为poi源码中有对null进行处理的代码)
return null;
}
String returnValue = null;
switch (cell.getCellType()){
case Cell.CELL_TYPE_BLANK: //空值
break;
case Cell.CELL_TYPE_BOOLEAN: //布尔
returnValue = String.valueOf(cell.getBooleanCellValue());
break;
case Cell.CELL_TYPE_ERROR: //异常
returnValue = "非法字符";
break;
case Cell.CELL_TYPE_NUMERIC: //数值和日期
if(HSSFDateUtil.isCellDateFormatted(cell)){ //处理日期、时间格式
SimpleDateFormat sdf = null;
if(cell.getCellStyle().getDataFormat()==14){
sdf = new SimpleDateFormat("yyyy/MM/dd");
}else if(cell.getCellStyle().getDataFormat()==21){
sdf = new SimpleDateFormat("HH:mm:ss");
}else if(cell.getCellStyle().getDataFormat()==22){
sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
}else{
throw new RuntimeException("日期格式错误!");
}
Date date = cell.getDateCellValue();
returnValue = sdf.format(date);
}else if(cell.getCellStyle().getDataFormat()==0){ //处理数值格式
cell.setCellType(Cell.CELL_TYPE_STRING);
returnValue = String.valueOf(cell.getRichStringCellValue().getString());
}
break;
case Cell.CELL_TYPE_FORMULA: //公式————这个公式的处理方式需要特别注意一下,文末我会特意再说明一下这个公式的特殊
if(cell.getCachedFormulaResultType()==Cell.CELL_TYPE_NUMERIC){
returnValue = String.valueOf(cell.getNumericCellValue());
}else if(cell.getCachedFormulaResultType()==Cell.CELL_TYPE_STRING){
returnValue = String.valueOf(cell.getStringCellValue());
}
break;
case Cell.CELL_TYPE_STRING: //字符串
returnValue = String.valueOf(cell.getStringCellValue());
break;
default: //其他
returnValue="未知类型";
break;
}
return returnValue;
}
最后就是用来封装每行数据为一个WorkOrder对象的方法了,这个很简单,如下:
/**
* 将单元格中的内容转换为String并封装为WorkOrder对象
* @param row
* @return WorkOrder
*/
public static WorkOrder convertRowToData(Row row){
WorkOrder workOrder = new WorkOrder();
int cellNum = 0;
Cell cell = null;
workOrder.setSheetName(sheet.getSheetName()); //获取Sheet名字
workOrder.setMonth(convertCellValueToString(row.getCell(cellNum++))); //获取月份信息——cellNum++是因为Excel是固定的模板,模板的前两行内容是固定格式,没必要读取
workOrder.setMonth1(convertCellValueToString(row.getCell(cellNum++))); //1月
workOrder.setMonth2(convertCellValueToString(row.getCell(cellNum++))); //2月
workOrder.setMonth3(convertCellValueToString(row.getCell(cellNum++))); //3月
workOrder.setMonth4(convertCellValueToString(row.getCell(cellNum++))); //4月
workOrder.setMonth5(convertCellValueToString(row.getCell(cellNum++))); //5月
workOrder.setMonth6(convertCellValueToString(row.getCell(cellNum++))); //6月
workOrder.setMonth7(convertCellValueToString(row.getCell(cellNum++))); //7月
workOrder.setMonth8(convertCellValueToString(row.getCell(cellNum++))); //8月
workOrder.setMonth9(convertCellValueToString(row.getCell(cellNum++))); //9月
workOrder.setMonth10(convertCellValueToString(row.getCell(cellNum++))); //10月
workOrder.setMonth11(convertCellValueToString(row.getCell(cellNum++))); //11月
workOrder.setMonth12(convertCellValueToString(row.getCell(cellNum++))); //12月
return workOrder;
}
程序编写到这里,已经差不多了,运行一下,我们看下结果:
一开始看到运行结果,我很蒙,前9个Sheet页的数据很正常。
但是明明没有什么“基础数据”的Sheet,那这些数据到底是从哪儿解析出来的(当初没考office计算机证,现在觉得脑壳痛...)?
难道是解析过程出错了?于是开始了漫长的问题排查过程...
最后在需要解析的Ecxel表本身中找到了答案:Sheet可以被隐藏,隐藏后是看不到的(内心毫无波澜...),如下:
根据Excel的Sheet页可以被隐藏的特性,加上刚刚程序运行出来的结果来一起推理,我们可以知道:
poi读取Excel内容的时候,即便是Sheet页被隐藏,内容依旧是可以被读取出来的!
但是我们不妨想一下,既然Sheet页被隐藏,那说明隐藏者认为该Sheet页的数据不再使用、或不希望被读取,
那么,我们就需要在程序中做处理,即被隐藏的Sheet页的数据不再进行解析,这部分处理代码我写在parseExcel()中,如下:
/**
* 解析Excel中的内容
* @param workbook
* @return List<WorkOrder>
*/
public static List<WorkOrder> parseExcel(Workbook workbook){
List<WorkOrder> resultList = new ArrayList<>(); //创建返回值接收对象
//解析Sheet数量,确定循环解析次数(这里不做Sheet的隐蔽性校验)
for (int sheetNum=0; sheetNum<workbook.getNumberOfSheets(); sheetNum++){
//如果Sheet是隐蔽的或非常隐蔽的,则不解析Sheet内容
if(workbook.isSheetHidden(sheetNum) || workbook.isSheetVeryHidden(sheetNum)){
continue;
}
sheet = workbook.getSheetAt(sheetNum);
if(sheet==null){ //检验sheet合法性
continue;
}
Row firstRow = sheet.getRow(sheet.getFirstRowNum()); //获取第一行数据
if(null == firstRow){ //判断第一行数据是否为空
System.out.println("第一行没有读取到任何数据!");
}
//解析每行数据,构造数据对象
int rowStart = sheet.getFirstRowNum() + 2;
int rowEnd = sheet.getPhysicalNumberOfRows();
for(int rowNum=rowStart; rowNum<rowEnd; rowNum++){
Row row = sheet.getRow(rowNum); //根据下标逐次获取行对象
//判断该行对象是否为空
if(row==null || "".equals(convertCellValueToString(row.getCell(0))) || null==convertCellValueToString(row.getCell(0))){
continue;
}
WorkOrder workOrder = convertRowToData(row); //解析行中单元格内容
if(workbook == null){
System.out.println("获取的数据为空!");
}
resultList.add(workOrder);
}
}
return resultList;
}
这里用到的isSheetHidden()和isSheetVeryHidden()是我在poi源码中找到的,
主要是用于判断Sheet页是否是Hidden(隐蔽)或VeryHidden(非常隐蔽),它们的返回值都是Boolean类型的,
具体可参见Excel中对于Sheet页的Visible设置,更能帮助理解,如下:
我在百度这部分内容的时候,发现很多人都在寻找poi中对于Sheet页隐蔽性的判断和设置的方法,
设置的方法很容易百度到,但是判断隐蔽性的方法基本上没有人去回答,所以这里我做个总结:
增加对于隐蔽性处理的代码后,再来运行代码,看下结果:
最后补充一下关于Excel中的公式的相关内容:
如果对于Excel用的比较粗浅的人(比如我自己...),一开始听到公式,你是不是以为是这个?
或者是这个?
如果你觉得是这样的,那咱俩就是同志了.......
其实Excel中单元格的公式,更多是指的用公式计算出来结果的单元格内容。
这么一听是不是很绕?那举个例子,比如大家都会用的选中几个单元格,然后点下求和按钮自动计算出来和,这就是公式处理。
那怎么判断哪个单元格中的值是用公式计算出来的呢?很简单(对我这个Excel渣渣来说,可能一辈子也不会注意到这点,简单个锤子...),如下:
如果只看Excel中的内容,其实单元格是否使用公式计算值,我们看的并没什么差别,
But,放到Java中用poi读取后,处理与不处理的差别可就大了去了(刚开始的时候被这个公式坑惨了...),看以下运行结果:
刚开始对poi不熟悉,各种百度poi对于公式的处理,找了很多,也走了很多弯路,最终确定这个方式靠谱,
这个方式的好处在于,处理公式的结果很稳~就算是原本用于公式计算的单元格被删掉,它也可以正常获取单元格的值!主要代码如下:
FormulaEvaluator formulaEvaluator = workbook.getCreationHelper().createFormulaEvaluator(); //用于解析和处理Cell公式的接口对象
case Cell.CELL_TYPE_FORMULA: //公式
if(cell.getCachedFormulaResultType()==Cell.CELL_TYPE_NUMERIC){
returnValue = String.valueOf(cell.getNumericCellValue());
}else if(cell.getCachedFormulaResultType()==Cell.CELL_TYPE_STRING){
returnValue = String.valueOf(cell.getStringCellValue());
}
break;
顺带补充一下,我用的是poi-3.9版本,目前已经更新到4.X版本了,但是很多使用者反应不稳定,还是建议使用比较老的,稳定~
以上就是目前我对poi读取Excel内容的理解和实践,如果以后还有更多的理解,再回来补充!
有问题欢迎留言,看到必回!
------------------------------------时间分割线 2020/8/26------------------------------------