poi能够将指定目录下的Excel中的内容解析、读取到java程序中。下面是一个Demo:
java
使用poi须要导下包,以下:
lua
首先是准备读取的Excel表,存放在“E:\programming\备份数据\工单分析结果_2020年7月.xlsx”下:
其余的sheet页(一共9个可见Sheet页),和下面图中的内容也都相似,只是数据稍微不一样。
线程
为了将Excel表中的数据保存在java程序中,下面建立了一个WorkOrder工单的实体类:
3d
先大概看下主线程中的代码:
code
poi解析Excel内容,是须要先建立一个工做簿对象的。
可是由于Microsoft Excel的版本不一样,因此文件后缀名也有所不一样,例如.xls和.xlsx。
因此咱们须要根据解析的Excel的后缀名,自动建立出对应类型的工做簿对象,代码以下:orm
/** * 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的方法须要咱们本身编写,以下:blog
/** * 解析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对象的方法了,这个很简单,以下:rem
/** * 将单元格中的内容转换为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页隐蔽性的判断和设置的方法,
设置的方法很容易百度到,可是判断隐蔽性的方法基本上没有人去回答,因此这里我作个总结:
方法名 | 做用 | 调用对象 | 补充说明 |
---|---|---|---|
isSheetHidden(int) | 判断Sheet是否隐蔽 | Wookbook | 返回值为Boolean类型 |
isSheetVeryHidden(int) | 判断Sheet是否很是隐蔽 | Wookbook | 设置后须要从新生成Excel文件覆盖原来的文件才能生效 |
setSheetHidden(int,boolean) | 设置Sheet隐蔽性 | Wookbook | 设置后须要从新生成Excel文件覆盖原来的文件才能生效 |
setSheetHidden(int,int) | 设置Sheet隐蔽性 | Wookbook | 设置后须要从新生成Excel文件覆盖原来的文件才能生效 |
removeSheetAt(int) | 删除指定Sheet页 | Wookbook | 设置后须要从新生成Excel文件覆盖原来的文件才能生效 |
增长对于隐蔽性处理的代码后,再来运行代码,看下结果:
最后补充一下关于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------------------------------------