poi解析Excel内容

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

相关文章
相关标签/搜索