一个通用Excel解析导入类的实现

本文介绍了一种通用的Excel动态表头导入工具类实现方法,支持表头动态配置及数据校验,提高了灵活性和实用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


通用的Excel动态表头导入工具类实现(附带数据校验)


前言

在工作中,遇到了一个工资导入功能的处理,有两个不同的工资表,需要导入对应的工资。一开始通过自定义注解来标注表头和字段的对应关系,将其一一对应,但是这种方式只要导入模板发生改变,哪怕是交换两个表头的位置,就需要改动代码,因此,组长希望实现一个可以动态配置表头的通用Excel导入工具类,这里将实现作一个记录。


一、实现分析

  • 动态表头的含义:我们可以动态的增加、减少表头或者是修改表头的名称。例如实体字段age,可以对应Excel表的年龄,也可以对应Excel表的岁数。
  • 既然是动态,就需要一个地方存储实体字段和表头的对应关系,我们项目采用的是数据字典的方式,一条字典的记录内容包括CODE(编码)、VALUE(值)、REMARK(备注),所有相同类型的字典放在同一父编码的字典集合中,通过父编码和CODE编码可以获得对应的内容,因此我们使用CODE存储实体字段,通过VALUE存储对应的表头内容,使用REMARK存储校验规则。例如其中一条记录是
    |CODE|VALUE|REMARK
    |SFGZ|实发工资,实际发放工资|^-?\d+(\.\d+)?
    |NAME|姓名,职员姓名,用户名|
    从上可以看出,Excel的表头可以是姓名,职员姓名,用户名等,哪怕Excel表头用猪猪,只要我们在字典上配好,就可以导入数据。注意这个字典是在系统上有配置页面的,非常方便。
  • 整体流程:获取到Excel的所有表头和所有的表头实体对应关系(这里只是我们的业务实现,可以根据自己的业务情况具体实现,只要保证实体字段是唯一的即可),通过循环处理上面的内容,获取表头和字段一一对应的数据,可以使用Map存储,我是用的是effectTitle,同时,那些没有对应的表头也可以存入另一个Map,用来判断用户上传的Excel文件是否和模板一致。得到这个effectTitle的Map集合后,就可以去遍历Excel的具体数据了,逐行读取,然后将实体字段和数据一一对应,同理也是存入一个Map集合,注意,这里做了数据校验,例如对于某些数据,我们只希望得到数值,因此,我们在字典记录中就记录了正则规则,这在我们代码中使用regMap这个集合存储,我们得到某个字段及其值后,可以根据这个字段获取其正则表达式,然后判断值是否满足规则,对于不满足规则的我们将其记录下来,并将这条记录抛弃(这里的抛弃只是不把它放入导入数据集合,然后将其记录下来放到提示修改集合中)。上面就是大概的流程,做完这些后我们就得到了导入数据集合datas,没有匹配的表头集合noEffectTitle,没有匹配的表头数量noEffectTitleNum,不符合规则的数据集合regFailUserAndTitle,不符合规则的数据数量noPatternRow,后续在自己的具体业务中可以进行进一步的处理,例如返回不匹配数据,导入合规数据等等。
  • 在下面的源码中,我对一些变量做了注释,对于输入变量这里做一些简单的解释。file肯定是对应的Excel文件,parentDictCode是我们业务中字典集合的父编码,通过这个获取所有字典记录,这个是可以根据具体业务去修改的,看具体实现方式,startRow是数据行开始的行数,noUseRow是不需要导入的行数,例如在末尾行可能有一些说明性的文字,用户不希望去掉,我们只能通过这个值控制数据行的结束,title故名思意就是表头行了,我这里用数组是因为我们业务中有两行表头。
  • 匹配数字的正则是^-?\\d+(\\.\\d+)?$,但是因为需要通过sql进行数据库的修改,$的存在会让sql报错,我尝试了^-?\\d+(\\.\\d+)?匹配正则,暂时没发现问题。
  • 说了这么多,可能会有一点点懵逼,还是来看看代码吧,我做了一些注释,应该是可以明白的!!!

二、源码

代码如下:

public class ExcelCellToEntity {
    /**
     * 处理Excel文件
     * @param file excel文件
     * @param parentDictCode 当前excel文件对应的字典父编码
     * @param startRow 数据开始行
     * @param noUseRow 不需要导入数据行数
     * @param title 标题行行号数组
     * @return
     * @throws Exception
     */
        public static Map<String, Object> test(MultipartFile file, String parentDictCode, int startRow, int noUseRow, int []title) throws Exception {
        Map<String, Object> result = new HashMap<>();
        //根据父编码获取所有字典项及其编码(编码存放的是数据库实体类字段,值存的是可能表头名称,以逗号分隔),这里可以换做其他业务实现哦
        List dictListByParentCode = DictUtil.getDictListByParentCode(parentDictCode);
        //所有当前父编码的字典项,就是字典数据的集合
        Map<String,String>dictMap = new HashMap<>();
        //key是字典code,value是对应的正则规则,这个是正则集合
        Map<String,String>regMap = new HashMap<>();
        for (int i = 0; i < dictListByParentCode.size(); i++) {
            Map map = (Map) dictListByParentCode.get(i);
            //对应的表头名称,可能是多个,将可能的表头名称都做个记录
            String[]codes = String.valueOf(map.get("VALUE")).split(",");
            //表头对应的字段名称,例如年龄->age,这个CODE是实体字段
            String value = String.valueOf(map.get("CODE"));
            //当前字段的正则校验,例如检验这个是否是数值类型,这里我用%分割是因为一开始没有做数据校验,只是一个备注,后来新增了需求,才想到通过%分割,在其后面保存正则规则,当然,正则规则也可以配置多个哦
            String reg = String.valueOf(map.get("REMARK"));
            if(reg.lastIndexOf("%")!=-1){
                //正则放在字典备注里,在"split"后,若有多个用","隔开,这里存储所有正则,后面需要分隔,因为key是code,保证唯一性
                reg = reg.substring(reg.lastIndexOf("%")+1);
            }else{
                reg = "";
            }
            //这里key是表头,value是实体字段,数组表示多个表头对同一个实体字段,然后根据传入的模板的表头去取值
            for (int j = 0; j < codes.length; j++) {
                dictMap.put(codes[j].trim(),value);
            }
            //正则集合
            regMap.put(value,reg);
        }
        InputStream inputStream = file.getInputStream();
        Workbook workbook = WorkbookFactory.create(inputStream);
        //key是Integer类型,说明存储的是excel表的列数,后续不管是表头还是数据都是通过列数来取到的
        //存放有效的表头对应的实体字段即excel表列数
        Map<Integer, Object> effectTitle = new HashMap<>();
        //存放没有对应上的表头
        Map<Integer, String> noEffectTitle = new HashMap<>();
        Map<String,String>oneToOneDict = new HashMap();
//        Map<String,String>regFailUserAndTitle = new HashMap<>();
        //正则匹配失败的记录
        List<String>regFailUserAndTitle = new ArrayList<>();
        //没有对应的表头数量
        int noEffectTitleNum = 0;
        //正则匹配失败的数量
        int noPatternRow = 0;
        //实体字段和数据对应的集合
        List<Map<String, Object>> datas = new ArrayList<>();
        //sheet页数量
        int sheetCount = workbook.getNumberOfSheets();
        for (int i = 0; i < sheetCount; i++) {
            //当前sheet页
            Sheet sheet = workbook.getSheetAt(i);
            if(!AegisCommonUtils.isNull(sheet) && sheet.getLastRowNum()!=0){
                //总行数
                int rowCount = sheet.getPhysicalNumberOfRows();
                //是否将当前列名及其列号加入集合
                boolean flag = true;
                //取出表头数据
                for (int t = 0; t < title.length; t++) {
                    //当前行是表头
                    Row row = sheet.getRow(title[t]);
                    int cellCount = row.getPhysicalNumberOfCells();
                    for (int c = 0; c < cellCount; c++) {
                        //当前列
                        Cell cell = row.getCell(c);
                        if(!AegisCommonUtils.isNull(cell)) {
                            String cellValue = ExcelAnalysisUtils.getCellValue(cell);
                            //去掉表头的换行和空格
                            String s = cellValue.replaceAll("\r\n|\r|\n", "");
                            if(cell.getCellType() == HSSFCell.CELL_TYPE_BLANK || "".equals(s)){
                                //遇到了表头是空单元格,处理下一个
                            }else{
                                if(AegisCommonUtils.isNull(dictMap.get(s))){
                                    //没有从所有字典项中找到当前列名对应的实体类编码
                                    noEffectTitle.put(c,s);
                                    noEffectTitleNum++;
                                }else{
                                    //判断当前表头是否已经加入
                                    for (Integer integer : effectTitle.keySet()) {
                                        String str = String.valueOf(effectTitle.get(integer));
                                        if(str.equals(dictMap.get(s))){
                                            flag = false;
                                            break;
                                        }
                                    }
                                    if(flag){
                                        effectTitle.put(c,dictMap.get(s));
                                        oneToOneDict.put(dictMap.get(s),s);
                                    }
                                }
                            }
                        }
                    }
                }
                //这里startRow代表第一行要导入的数据所在的行数
                for (int r = startRow; r < rowCount-noUseRow; r++) {
                    Row row = sheet.getRow(r);
                    Map<String,Object>temp = new HashMap<>();
                    //列数
                    int cellCount = row.getPhysicalNumberOfCells();
                    int noPatternNum = 0;
                    StringBuilder sb = new StringBuilder();
                    Pattern pattern = null;
                    for (int c = 0; c < cellCount; c++) {
                        //当前列
                        Cell cell = row.getCell(c);
                        if(!AegisCommonUtils.isNull(cell)) {
                            //这里行是数据
                            if(!AegisCommonUtils.isNull(effectTitle.get(c))){
                                String cellValue = ExcelAnalysisUtils.getCellValue(cell);
                                String reg = regMap.get(String.valueOf(effectTitle.get(c)));
                                if(!AegisCommonUtils.isNull(reg)){
                                    //匹配正则
                                    String[]regs = reg.split(",");
                                    for (String rreg : regs) {
                                         pattern = Pattern.compile(rreg);
                                        if(!pattern.matcher(cellValue).matches()){
                                            noPatternNum++;
                                            sb.append(oneToOneDict.get(effectTitle.get(c)));
                                            sb.append(",");
                                            break;
                                        }
                                    }
                                }
                                temp.put(String.valueOf(effectTitle.get(c)),cellValue);
                            }
                        }
                    }
                    //把数据集合加入,这里temp对应一整行的数据
                    if(!AegisCommonUtils.isNull(temp)){
                        if(noPatternNum == 0){
                            datas.add(temp);
                        }else{
                            noPatternRow++;
                            regFailUserAndTitle.add(String.valueOf(temp.get("userName"))+","+sb.toString().substring(0,sb.toString().length()-1));
//                            regFailUserAndTitle.put(String.valueOf(temp.get("userName")),sb.toString().substring(0,sb.toString().length()-1)+"列的数据不符合规范");
                        }
                    }
                }
            }
        }
        result.put("datas",datas);
        result.put("noEffectTitle",noEffectTitle);
        result.put("noEffectTitleNum",noEffectTitleNum);
        result.put("regFailUserAndTitle",regFailUserAndTitle);
        result.put("noPatternRow",noPatternRow);
        return result;
    }
}

总结

总体来说,这个工具类还是能处理一些常见的业务,对于复杂的Excel文件,可能要进行相应的修改,相比于写死数据列的方式,这种方式还是比较好的,对于上面有任何疑问的也可以给我留言,我在看到的第一时间一定回复,这是我的第一篇文章,如有不清晰的地方还请谅解。对于上面提到的写死字段的方式,在本站有许多文章做了详细说明,我也是参考了其中的内容。对于正则表达式的问题,我自己也是在学习,如果使用有问题,就需要再学习改进一下了。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雅俗共赏zyyyyyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值