poi可以将指定目录下的Excel中的内容解析、读取到java程序中。下面是一个Demo:

使用poi需要导下包,如下:
poi解析Excel内容-LMLPHP


首先是准备读取的Excel表,存放在“E:\programming\备份数据\工单分析结果_2020年7月.xlsx”下:
其他的sheet页(一共9个可见Sheet页),和下面图中的内容也都类似,只是数据稍微不同。
poi解析Excel内容-LMLPHP


为了将Excel表中的数据保存在java程序中,下面创建了一个WorkOrder工单的实体类:
poi解析Excel内容-LMLPHP

先大概看下主线程中的代码:
poi解析Excel内容-LMLPHP

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;
}

程序编写到这里,已经差不多了,运行一下,我们看下结果:
poi解析Excel内容-LMLPHP


一开始看到运行结果,我很蒙,前9个Sheet页的数据很正常。
但是明明没有什么“基础数据”的Sheet,那这些数据到底是从哪儿解析出来的(当初没考office计算机证,现在觉得脑壳痛...)?
难道是解析过程出错了?于是开始了漫长的问题排查过程...
最后在需要解析的Ecxel表本身中找到了答案:Sheet可以被隐藏,隐藏后是看不到的(内心毫无波澜...),如下:
poi解析Excel内容-LMLPHP
poi解析Excel内容-LMLPHP


根据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解析Excel内容-LMLPHP

我在百度这部分内容的时候,发现很多人都在寻找poi中对于Sheet页隐蔽性的判断和设置的方法,
设置的方法很容易百度到,但是判断隐蔽性的方法基本上没有人去回答,所以这里我做个总结:


增加对于隐蔽性处理的代码后,再来运行代码,看下结果:
poi解析Excel内容-LMLPHP


最后补充一下关于Excel中的公式的相关内容:
如果对于Excel用的比较粗浅的人(比如我自己...),一开始听到公式,你是不是以为是这个?
poi解析Excel内容-LMLPHP
或者是这个?
poi解析Excel内容-LMLPHP
如果你觉得是这样的,那咱俩就是同志了.......
其实Excel中单元格的公式,更多是指的用公式计算出来结果的单元格内容
这么一听是不是很绕?那举个例子,比如大家都会用的选中几个单元格,然后点下求和按钮自动计算出来和,这就是公式处理。
那怎么判断哪个单元格中的值是用公式计算出来的呢?很简单(对我这个Excel渣渣来说,可能一辈子也不会注意到这点,简单个锤子...),如下:
poi解析Excel内容-LMLPHP
poi解析Excel内容-LMLPHP


如果只看Excel中的内容,其实单元格是否使用公式计算值,我们看的并没什么差别,
But,放到Java中用poi读取后,处理与不处理的差别可就大了去了(刚开始的时候被这个公式坑惨了...),看以下运行结果:
poi解析Excel内容-LMLPHP
poi解析Excel内容-LMLPHP

刚开始对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------------------------------------

08-26 23:25