Easy Excel读取复杂表格文件

表格内容如下

Easy Excel读取复杂表格文件_第1张图片

分析

  1. 该文档总共9个Sheet页
  2. 以Sheet0(客户交易结算月报)为例:表格总共分为4个部分(图中红色部分)。一、二部分数据为横向数据(字段名:字段值),三四部分为纵向集合。第四部分每一行数据下有合计栏(合计栏数据不读取)。

实现逻辑

1. 前期准备

1) Pom文件引入Easy Excel

    <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.0.5</version>
    </dependency>

2)实现AnalysisEventListener

public class EasyExcelListener extends AnalysisEventListener<Object> {

    // 创建list集合封装最终的数据
    private List<Object> list = new ArrayList<>();
	// sheet页索引
    private int sheetNo = 0;

    @Override
    public void invoke(Object t, AnalysisContext context) {
        // 读取excle内容
        int currentSheetNo = context.readSheetHolder().getSheetNo();
        if (currentSheetNo != sheetNo) {
        	// 如果不根据sheet页索引更新状态重新创建list,list会反复添加前面的sheet页对象值
            list = new ArrayList<>();
            sheetNo = currentSheetNo;
        }
        list.add(t);
    }

    // 读取excel表头信息
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
    }

    // 读取完成后执行
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
    }

    /**
     * 获取表格内容(简单excel读取可用该方法)
     *
     * @param obj 需要转化的实体
     * @param 
     * @return
     */
    public <T> List<T> getList(Class<T> obj) {
        String jsonObj = JSONUtil.toJsonStr(list);
        return JSONUtil.toList(jsonObj, obj);
    }

    /**
     * 将表格转化为map集合(复杂excel读取用此方法)
     *
     * @return map集合
     */
    public List<LinkedHashMap> getListMap() {
        String jsonObj = JSONUtil.toJsonStr(list);
        return JSONUtil.toList(jsonObj, LinkedHashMap.class);
    }

}

ps: 为什么用LinkHashMap接收excel表格内容?
Easy Excel读取复杂表格文件_第2张图片
读取文件内容时,在不确定接收实体的情况下,对象都是以LinkedHashMap进行存储的。图中可以看到,每个map的key,value是以列索引以及对应的行值进行存储。

3)Excel工具类

@Slf4j
public class EasyExcelUtil {
    private EasyExcelUtil() {
    }

    /**
     * 根据easyexcel注解给指定实体赋值
     *
     * @param objects 读取的表格内容
     * @param clazz   需转化的实体
     * @param      实体
     * @return 需转化的提示集合
     */
    public static <T> List<T> convertList(List<LinkedHashMap> objects, Class<T> clazz) {
        List<T> results = new ArrayList<>(objects.size());
        try {
            Map<String, Field> objIndex = new HashMap<>();
            // 获取转化实体字段信息集合
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
            	// 根据实体上Easy Excel的ExcelProperty注解中的索引值对应excel读取数据的值
                int index = field.getAnnotation(ExcelProperty.class).index();
                // 设置字段可编辑
                field.setAccessible(true);
                objIndex.put(String.valueOf(index), field);
            }

            T obj = null;
            for (LinkedHashMap o : objects) {
                obj = clazz.newInstance();
                for (Object key : o.keySet()) {
                    // 如果表格索引与字段注解指定索引一样则赋值
                    if (objIndex.containsKey(key)) {
                        Object object = o.get(key);
                        Object value = null;
                        Field field = objIndex.get(key);
                        if (ObjectUtil.isEmpty(object)) {
                            continue;
                        }
                        Class<?> type = field.getType();
                        String replace = object.toString();
                        // 有特殊需要处理的字段类型则在此进行处理
                        if (type == BigDecimal.class) {
                            value = "--".equals(replace) ? null : new BigDecimal(replace.replace(",", ""));
                        } else if (type == Integer.class) {
                        	// String强转Integer会报错,所以需要单独进行转化
                            value = "--".equals(replace) ? null : Integer.valueOf(replace.replace(",", ""));
                        } else {
                            value = object;
                        }
                        field.set(obj, value);
                    }
                }
                results.add(obj);
            }
        } catch (Exception e) {
            log.error("字段解析失败", e);
            Asserts.fail("字段解析失败:" + e.getMessage());
        }
        return results;
    }
}

参考实体:
@ExcelProperty注解必须有,且对应到字段值对应的列数。如果不确定列数可以先读取linkedlist 看看对应的索引值

@Data
@ApiModel("其他资金明细excel实体")
public class OtherFundDetailsExcelDTO {

    @ApiModelProperty(value = "发生日期")
    @ExcelProperty(value = "发生日期", index = 0)
    private String happenDate;

    @ApiModelProperty(value = "交易所")
    @ExcelProperty(value = "交易所", index = 2)
    private String exchange;

    @ApiModelProperty(value = "类型")
    @ExcelProperty(value = "类型", index = 4)
    private String type;

    @ApiModelProperty(value = "金额")
    @ExcelProperty(value = "金额", index = 6)
    private BigDecimal money;

    @ApiModelProperty(value = "备注")
    @ExcelProperty(value = "备注", index = 8)
    private String remark;
}

2. 编码

1)Controller获取文件内容

   	@ApiOperation("导入")
    @PostMapping(value = "/excel/upload")
    public void upload(@ApiParam(name = "file", value = "file", required = true) @RequestParam(value = "file") MultipartFile file, HttpServletResponse response) throws IOException {
    	// step1. 读取excel内容
        EasyExcelListener easyExcelListener = new EasyExcelListener();
        ExcelReaderBuilder read = EasyExcelFactory.read(file.getInputStream(), easyExcelListener);
        ExcelReader excelReader = read.build();
        // step2. 获取各个sheet页信息
        List<ReadSheet> sheets = excelReader.excelExecutor().sheetList();
        // step3. 获取各个Shhet页表格内容存于map
        Map<Integer, List<LinkedHashMap>> sheetInfos = new HashMap<>(sheets.size());
        for (ReadSheet sheet : sheets) {
            Integer sheetNo = sheet.getSheetNo();
            excelReader.read(sheet);
            sheetInfos.put(sheetNo, easyExcelListener.getListMap());
        }
        iSettlementMothReportService.saveExcelInfo(sheetInfos);
    }

2) Service将读取内容转化为实体类(此代码仅提供两种excel表格读取方式,横向数据读取以及纵向集合读取)

public void saveExcelInfo(Map<Integer, List<LinkedHashMap>> sheetInfos) {
        SettlementMothReportSaveDTO settlementMothReportSaveDTO = new SettlementMothReportSaveDTO();
        for (Integer sheetNo : sheetInfos.keySet()) {
            List<LinkedHashMap> maps = sheetInfos.get(sheetNo);
            // 不同sheet页数据处理方式不同
            switch (sheetNo) {
                case 0:
                    settlementMonthlyReport(maps, settlementMothReportSaveDTO);
                    break;
                case 1:
                    varietyCollects(maps, settlementMothReportSaveDTO);
                    break;
                case 2:
                    holdingsSubsidiary(maps, settlementMothReportSaveDTO);
                    break;
                case 3:
                    dealDetail(maps, settlementMothReportSaveDTO);
                    break;
                default:
                    break;
            }
        }
        iBzjBaseInfoService.saveSettlementMothReport(settlementMothReportSaveDTO);
    }
private void settlementMonthlyReport(List<LinkedHashMap> maps, SettlementMothReportSaveDTO settlementMothReportSaveDTO) {
        // 基本资料
        BzjBaseInfo baseInfo = new BzjBaseInfo();
        // 期货期权账户出入金明细
        List<LinkedHashMap> optionAccountsDetails = new ArrayList<>();

        // 是否基本资料
        boolean whetherBaseInfo = true;
        // 是否期货期权账户出入金明细
        boolean whetherOptionAccountsDetails = false;
      
        for (LinkedHashMap map : maps) {
            if (whetherBaseInfo) {
                // 基本资料
                whetherBaseInfo = dealBaseInfo(map, baseInfo);
                if (!whetherBaseInfo) {
                    continue;
                }
            }
    
            if (map.containsValue(OptionAccountsDetailsConstant.OPTION_ACCOUNTS_DETAILS)) {
                whetherOptionAccountsDetails = true;
            }
            if (!whetherBaseInfo && whetherOptionAccountsDetails) {
                // 期货期权账户出入金明细
                whetherOptionAccountsDetails = dealOptionAccountsDetails(map, optionAccountsDetails);
                if (!whetherOptionsFuturesFunds) {
                    continue;
                }
            }
        }
        if (!optionAccountsDetails.isEmpty()) {
            List<FuturesOptionAccountsDetailsExcelDTO> dtos = EasyExcelUtil.convertList(optionAccountsDetails, FuturesOptionAccountsDetailsExcelDTO.class);
            settlementMothReportSaveDTO.setOptionAccountsDetails(dtos);
        }
        settlementMothReportSaveDTO.setBzjBaseInfo(baseInfo);
    }

ps:为什么要用模块标题作为标识而不是索引?
因为以模块标题作为标识,如果上一个模块出现增删数据时,不影响本模块的读取。如果直接用表格行索引,那么表格内容出现变更时就无法灵活读取数据。

 /**
     * 处理基础资料
     *
     * @param map      excel表格信息
     * @param baseInfo 基础资料信息
     * @return 是否基本资料
     */
    private boolean dealBaseInfo(LinkedHashMap map, BzjBaseInfo baseInfo) {
        // 是否基础信息内容
        if (map.containsValue(BaseInfoConstant.BASE_INFO)) {
            return true;
        }
		// 根据表格内容分析,对象值在2,7列。合并单元格在读取之后会将值存在合并的第一列索引下,如合并A-C列,那A列会存储字段值,B、C列值为空。具体内容可见上面读取excel截图
        Object obj = map.get("2");
        String str = null;
        if (ObjectUtil.isNotEmpty(obj)) {
            str = obj.toString();
        }
        Object object = map.get("7");
        String value = null;
        if (ObjectUtil.isNotEmpty(object)) {
            value = object.toString();
        }
        // 通过本行的第一个字段名,获取本行的所有字段信息(后面的判断均是如此,常量类对应了各个字段名)
        if (map.containsValue("客户期货期权内部资金账户")) {
            baseInfo.setCusFuturesInnerFundAccount(str);
            baseInfo.setTransactionMonth(value);
        }
        if (map.containsValue(BaseInfoConstant.CUSTOMER_NAME)) {
            baseInfo.setCustomerName(str);
            DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            baseInfo.setQueryTime(LocalDateTime.parse(value, fmt));
        }
        if (map.containsValue(BaseInfoConstant.FUTURES_COMPANY_NAME)) {
            baseInfo.setFuturesCompanyName(str);
            baseInfo.setCusSecuritiesSpotInnnerFundAccount(value);
            return false;
        }
        return true;
    }
    /**
     * 处理期货期权账户出入金明细
     *
     * @param map                   excel表格信息
     * @param optionAccountsDetails 账户出入金明细Excel集合
     * @return 是否期货期权账户出入金明细
     */
    private Boolean dealOptionAccountsDetails(LinkedHashMap map, List<LinkedHashMap> optionAccountsDetails) {
    	// 确定模块结束标识
        if (map.containsValue("合计")) {
            return false;
        }
        // 模块开始标识为模块标题以及模块第一行字段名,所以读取数据时要排除这两行
        if (!map.containsValue("发生日期") && !map.containsValue("期货期权账户出入金明细")) {
            optionAccountsDetails.add(map);
        }
        return true;
    }

PS:为什么确定模块开始标识和结束标识?
确定了开始和结束标识相当于直接将读取数据锁死,只读取开始和结束中间的数据,避免数据误读!

以上是Easy Excel读取复杂表格的代码分析,请多指教!
晚安,玛卡巴卡~

你可能感兴趣的:(java,java)