Apache POI 解析复杂的excel表格

一:场景说明

       最近接到一个需求,让我解析Excel表。这要是简单常规的Excel表,那我还能摸一摸鱼给他整出来,主要是给我的Excel表长得跟下图中的Excel表一样复杂难搞,这可把我难倒了。于是开启了我的百度之旅,有可能是我不会百度或者理解能力太差,反正就是我的问题。愣是找不到一篇文章能让我这种智力不足的人弄明白该如何去解析这种复杂的Excel表格。于是我决定按照自己的思维去解析这种复杂的Excel表格,希望能得到小伙伴们的认可。

图片来源:https://blog.csdn.net/weixin_42803027/article/details/110189928

在这里插入图片描述

二:Apache POI常用类

​        Java中有很多框架可以去解析Excel文件,这里我使用到POI去解析Excel文件。由于前辈们已经给出了很多很好的使用说明了,这里我就直接粘几篇我看到觉得很好的POI使用详解文章。(Apache POI使用详解_vbirdbest的博客-CSDN博客POI核心类 - POI教程 (yiibai.com)

  • HSSF - 提供读写Microsoft Excel XLS格式档案的功能。

  • XSSF - 提供读写Microsoft Excel OOXML XLSX格式档案的功能。

  • HWPF - 提供读写Microsoft Word DOC97格式档案的功能。

  • XWPF - 提供读写Microsoft Word DOC2003格式档案的功能。

  • HSLF - 提供读写Microsoft PowerPoint格式档案的功能。

  • HDGF - 提供读Microsoft Visio格式档案的功能。

  • HPBF - 提供读Microsoft Publisher格式档案的功能。

  • HSMF - 提供读Microsoft Outlook格式档案的功能。

  • 在开发中我们经常使用HSSF用来操作Excel处理表格数据,对于其它的不经常使用。

       这里我使用到是XSSF接口,毕竟除了能解析xlsx格式的Excel以外,也可以兼容解析xls格式的Excel。详细的使用可以常考上面的两篇文章,第一篇是讲HSSF的用法,第二篇是讲XSSF的用法。两者的用法非常相似,建议两篇文章结合使用。为了消除接下来的代码阅读障碍,这里将简单讲解一下案例中使用到的方法。

// 创建一个关联输入流的工作簿
Workbook workbook = WorkbookFactory.create(file.getInputStream());
// 根据工作表下标获取指定工作表
Sheet sheet = workbook.getSheetAt(sheetIndex);
// 获取工作表中的行,此处为第一行
Row row = sheet.getRow(0);
// 获取工作表中第一行的第一列单元格
Cell cell = row.getCell(0);
// 获取单元格的属性值,由于cell有相应的单元格类型以及对应的值也有其数据类型,所以获取值的路途可能比较麻烦点。这里简单获取它的值一下,接下来会有一个专门的函数去获取它的值。
cell.getStringCellValue();

// 获取sheet工作表中所有的合并单元格的个数
int sheetMergerCount = sheet.getNumMergedRegions();
// 根据下标获取工作表中指定的合并单元格
CellRangeAddress cra = sheet.getMergedRegion(i);

Excel中的工作簿、工作表、行、单元格中的关系:

一个Excel文件对应于一个workbook(XSSFWorkbook),
一个workbook可以有多个sheet(XSSFSheet)组成,
一个sheet是由多个row(XSSFRow)组成,
一个row是由多个cell(XSSFCell)组成

为了加深对Excel的了解,这里将结合图片详细讲解一下。

在这里插入图片描述

三:案例分析

1、思路说明
  1. 在真正读取复杂Excel表格前,需要解析一下这个Excel表格的模板,也就是这个复杂Excel表格没有输入值的状态,如上图所示。
  2. 在解析这个Excel表格模板时,我们需要去一行一行的遍历每一个单元格,判断它的值是否为null。为null则说明该单元格是用来存储数据的,此时我们需要记录下它的位置(行号以及列号)。若该单元格为合并单元格时,只需记录其第一行、第一列对应的单元格的位置即可。若不为null,则说明其只是一个数据标识,如:姓名、性别等这类数据标识,并不需要记录相应位置。
  3. 这里可能会有些疑问,POI会不会识别到黑框框表单之外的单元格。需要说明的是,excel表格设计好了是不会识别黑框框外的单元格的。
  4. 重点:由于复杂表格是合并单元格和普通单元格混杂一起的,而遍历单元格的时候,会把合并单元格当成普通单元格去遍历。所以我们需要根据行号和列号去判断当前的单元格是否包含在合并单元格中,若是在合并单元格中,需要判断一下是否为该合并单元格的第一行、第一列。若为该合并单元格的第一行、第一列就去读数据,否则根据该合并单元格的最后一列下标去到当前行的下一个单元格。普通单元格直接读取判断其值即可。
  5. 解析完Excel模板后,就会得到一组用于数据存储的单元格的位置,这时我们只需根据这些位置去上传的Excel表中取值即可。
  6. 需要注意的是,我们是从左到右、从上到下的去存储数据存储单元格的位置,所以我们在定义数据类属性的时候也需要根据这个顺序来,否则在通过反射机制自动装配数据类对象属性时会出现顺序错误。(对于自动装配数据类对象属性不熟悉的小伙伴可以参考:Java通过反射机制获取数据类对象的属性及方法
2、重要代码展示
1、获取cell单元格中的数据
/**
 * 获取cell单元格中的值
 * @param cell
 * @return
 */
private Object getValue(Cell cell) {
    Object value = null;
    if (cell == null) {
        return null;
    } else if (cell.getCellType().equals(CellType.FORMULA)) {   // 单元格的数据属于公式类
        switch (cell.getCachedFormulaResultType()) {
            case STRING:
                // 字符串类型
                value = cell.getStringCellValue();
                break;
            case NUMERIC:
                // double类型
                value = cell.getNumericCellValue();
                break;
            case BOOLEAN:
                // boolean类型
                value = cell.getBooleanCellValue();
                break;
            default:
                // 字符串类型
                value = cell.getCellFormula();
        }
    } else if (cell.getCellType().equals(CellType.NUMERIC)) {   // 单元格的数据属于数字类
        if (DateUtil.isCellDateFormatted(cell)) {
            value = cell.getDateCellValue();
        } else {
            value = new BigDecimal(cell.toString());
        }
    } else {
        value = StringUtils.isBlank(cell.toString()) ? null : cell.toString();
    }
    return value;
}
2、通过行号和列号判断当前单元格是否属于合并单元格(MergedRegion类存储的是合并单元格的起始行号、结束行号、起始列号、结束列号)
/**
 * 判断cellRow行、cellCol列对应的单元格是否为合并单元格
 * @param sheet	工作表
 * @param cellRow	cell对应的行号
 * @param cellCol	cell对应的列号
 * @return
 */
private MergedRegion getMergerCellRegionRow(Sheet sheet, int cellRow, int cellCol) {
    // 获取sheet工作表中所有的合并单元格的个数
    int sheetMergerCount = sheet.getNumMergedRegions();
    for (int i = 0; i < sheetMergerCount; i++) {
        // 获取指定合并单元格
        CellRangeAddress cra = sheet.getMergedRegion(i);
        int firstRow = cra.getFirstRow(); // 合并单元格CELL起始行
        int firstCol = cra.getFirstColumn(); // 合并单元格CELL起始列
        int lastRow = cra.getLastRow(); // 合并单元格CELL结束行
        int lastCol = cra.getLastColumn(); // 合并单元格CELL结束列
        // 判断cellRow行、cellCol列对应的单元格是否在该合并单元格的范围中
        if (cellRow >= firstRow && cellRow <= lastRow) {
            if (cellCol >= firstCol && cellCol <= lastCol) {
                return new MergedRegion(firstRow, lastRow, firstCol, lastCol);
            }
        }
    }
    return null;
}
3、解析复杂Excel模板(TargetRegion存储的是数据存储单元格的行号和列号,targetRegions是TargetRegion类的List集合,用来存储所有的数据存储单元格的位置)
// 解析复杂excel表,并将目标值的位置存储起来
private void readExcelMixMergedRegion(Sheet sheet) {
    System.out.println("======= 解析Excel文件start =======");
    // 获取工作表中的行数
    int numberOfRows = sheet.getPhysicalNumberOfRows();
    for (int i = 0; i < numberOfRows; i++) {
        Row row = sheet.getRow(i);
        // 遍历每行的列(即遍历所有单元格)
        for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
            System.out.printf("(%d-%d)", i, j);
            int rowIndex = i, columnIndex = j;
            // 判断行号为i,列号为j的单元格是否为合并单元格,不为合并单元格返回null,若为合并单元格返回MergedRegion对象
            MergedRegion mergedRegion = getMergerCellRegionRow(sheet, i, j);
            Cell cell = null;
            if (mergedRegion != null) { // 若为合并单元格,需要进行进一步处理
                // 若当前的行号和列号是合并单元格的第一行、第一列,就获取单元格(合并单元格只有其第一行、第一列的位置有值)
                if (i == mergedRegion.getFirstRow() && j == mergedRegion.getFirstColumn()) {
                    cell = row.getCell(j);  // 获取单元格
                    j = mergedRegion.getLastColumn();  // 将列下标移到当前行的下一个单元格列下标,减少不必要的扫描
                }else{  // 若为合并单元格的非第一行,则直接将列下标移到当前行的下一个单元格列下标,并进行接下来的扫描
                    j = mergedRegion.getLastColumn();   //
                    continue;
                }
            } else {    // 若不为合并单元格,直接获取单元格
                cell = row.getCell(j);
            }
            Object val = getValue(cell);    // 获取单元格的值
            if (val==null) {    // 若单元格的值为null,说明该单元格为目标单元格,需要存储它的位置
                TargetRegion targetRegion = new TargetRegion(rowIndex, columnIndex);
                targetRegions.add(targetRegion);
            }
            System.out.println(val==null?null:val.toString());
        }
    }
    System.out.println("======= 解析Excel文件end =======");
}
4、根据数据存储单元格的位置去获取相关数据
/**
 * 根据存储的目标值位置去获取相应的value值
 * @param sheet
 * @param paramsValueList   存储目标值容器
 */
private void getTargetCellsValue(Sheet sheet,List<Object> paramsValueList) {
    System.out.println("======= 读取目标值start =======");
    targetRegions.forEach(item -> {
        int row = item.getRow();
        int column = item.getColumn();
        System.out.print("("+row+","+column+") ");
        Object val = getValue(sheet.getRow(row).getCell(column));
        paramsValueList.add(val);
        System.out.println(val==null?null:val.toString());
    });
    System.out.println("======= 读取目标值end =======");
}
3、成果展示

待解析模板:

在这里插入图片描述

解析成果:
请添加图片描述

四:项目示例代码

项目中包含测试文件,以及刘姥姥都说看得懂的代码注解,可以放心食用。下载项目配置好环境后只需运行测试案例(FileTest)即可。

示例代码只需5积分,绝对的良心之作,还在等什么,冲冲冲!

https://download.csdn.net/download/Mr_Hugo/86036793

  • 11
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值