easyexcel icon indicating copy to clipboard operation
easyexcel copied to clipboard

多行表头,合并单元格表头无法按名称识别

Open ahjian opened this issue 2 years ago • 17 comments

EasyExcel版本:3.1.0-3.1.2 demo.xlsx wps编辑了F列表头,取消合并,合并单元格,F2单元格值被清空。 导致导入时,无法按照F列的名称识别导入属性。

DefaultAnalysisEventProcessor 中只按第二行单元格数据识别处理对象,导致无法识别属性,必须采用index

if (!isData && currentHeadRowNumber == rowIndex + 1) { this.buildHead(analysisContext, cellDataMap); }

ahjian avatar Nov 02 '22 12:11 ahjian

这个问题要如何解决,必须使用index吗?

dqget avatar Mar 22 '23 04:03 dqget

把第二行写上 再重新合并 就可以了

gongxuanzhang avatar Mar 28 '23 07:03 gongxuanzhang

我试过可以在AnalysisEventListener的invokeHeadMap或者invokeHead方法中补齐合并表头,然后补齐AnalysisContext.readSheetHolder().excelReadHeadProperty().getHeadMap()的表头映射关系就可以找到对应关系,这样就可以用name而不是index。

是不是可以给加一个AnalysisContext.readSheetHolder().excelReadHeadProperty().getHeadMap()加一个回调方便修改。

dqget avatar Mar 29 '23 03:03 dqget

这根本就不是个问题啊。 你修改了原本的excel,等于修改了元数据,还期待和以前的解析是相同的吗。

gongxuanzhang avatar Mar 29 '23 05:03 gongxuanzhang

并不是修改了,而是读取合并表头的问题。 Snipaste_2023-03-29_15-58-20

这是从其他系统导出的excel,合并表头只有第一行有值。 像这种excel表头读取的时候就有问题,因为表头的列导出的时候可以随意更改顺序,所以没办法试用index来确定列。

Snipaste_2023-03-29_16-01-19

dqget avatar Mar 29 '23 07:03 dqget

我听懂你的问题了,合并之后单元格的数据和合并之前是可以没关系的。 你可以粗略理解为一个合并之后的大单元格盖在了你合并之前的小单元格上。 所以你需要做的是把小单元格的数据保留,而不是删除

gongxuanzhang avatar Mar 29 '23 08:03 gongxuanzhang

可是我没有办法修改导出这个excel的功能,因为这是其它系统做的事情。你有其他更好的建议吗 :)

dqget avatar Mar 29 '23 08:03 dqget

并不是修改了,而是读取合并表头的问题。 Snipaste_2023-03-29_15-58-20

这是从其他系统导出的excel,合并表头只有第一行有值。 像这种excel表头读取的时候就有问题,因为表头的列导出的时候可以随意更改顺序,所以没办法试用index来确定列。

Snipaste_2023-03-29_16-01-19

这种情况我将第2行作为表头的时候读取不到“客户名称*-名称”和 “客户名称电话” 这种列,将第3行作为表头的话又读取不到像“合同编号”这种的合并单元格表头,所以陷入了两难

dqget avatar Mar 29 '23 08:03 dqget

啊感觉是个很难顶的问题,index肯定能解决但是就不动态了。 如果你的excel是固定的那就用index解决吧

gongxuanzhang avatar Mar 29 '23 08:03 gongxuanzhang

痛点我会记录的 之后有优雅方式我来reply

gongxuanzhang avatar Mar 29 '23 08:03 gongxuanzhang

非常感谢!

dqget avatar Mar 29 '23 08:03 dqget

遇到同样问题,插眼

Alleninggx avatar May 17 '23 05:05 Alleninggx

没解决吗 只能用index??

cangwuwuwu avatar Mar 13 '24 08:03 cangwuwuwu

遇到同样问题,插眼

duckmoon avatar Mar 18 '24 04:03 duckmoon

插眼

cloudlessa avatar May 27 '24 07:05 cloudlessa

这是我的实现方式,easyexcel一行行读,一行行往grpc响应流写结果。

支持的表格头(支持左边合并的样子,右边只是说明真实的表格中的数据): image

service ExcelFileResourceDubboGrpc {
  // 读取
  rpc read(ReadRequest) returns (stream ReadResult) {}
}

// ----------------- 读取 -----------------

message ReadRequest {
  string key = 1;                 // 唯一标识
  bool stopWhenError = 2;         // 遇到异常是否停止
}

message ReadResult {
  ReadStatus status = 1;          // 状态
  string message = 2;             // 消息
  int32 rowIndex = 3;             // 行号
  repeated ReadCol col = 4;      // 列数据
  repeated ReadHead head = 5;    // 头数据,每一行都会携带
}

message ReadHead {
  int32 index = 1;                // 列号
  string name = 2;                // 列名
  optional string groupName = 3;  // 分组名
}

message ReadCol {
  int32 index = 1;                // 列号
  string name = 2;                // 列名
  string value = 3;               // 值
}

enum ReadStatus {
  RS_SUCCESS = 0;                 // 读成功
  RS_ERROR = 1;                   // 读失败
}

@Override
    public void read(ReadRequest readRequest, StreamObserver<ReadResult> responseObserver) {
        try (InputStream is = objectStorageService.getObject(readRequest.getKey())) {
            /**
             * 大概流程:
             * 解析头
             * 判断是否有明细
             * 解析数据行
             * 一行一行往流推
             */
            EasyExcel.read(is, new AnalysisEventListener<Map<Integer, String>>() {

                /**
                 * 头
                 */
                private final Map<Integer, String> headGroupMap = new HashMap<>();
                private final Map<Integer, String> headMap = new HashMap<>();
                private boolean hasNext = true;


                @Override
                public boolean hasNext(AnalysisContext context) {
                    return hasNext;
                }

                @Override
                public void onException(Exception exception, AnalysisContext context) {
                    ReadResult.Builder readResult = ReadResult.newBuilder().setStatus(ReadStatus.RS_ERROR);

                    if (exception instanceof ExcelDataConvertException excelDataConvertException) {
                        String colName = I18n.get("Resource.Msg.UnknownExcelColName");
                        // 读取列头
                        if (MapUtil.isNotEmpty(headMap) && headMap.containsKey(excelDataConvertException.getColumnIndex())) {
                            colName = headMap.get(excelDataConvertException.getColumnIndex());
                        }
                        String msg = I18n.get("Excel.Msg.ParsingException", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex(), colName, excelDataConvertException.getCellData());
                        // log.error(msg, exception);

                        // 补充错误信息
                        readResult.setRowIndex(excelDataConvertException.getRowIndex())
                                .setMessage(msg)
                                .addCol(ReadCol.newBuilder()
                                        .setIndex(excelDataConvertException.getColumnIndex())
                                        .setName(colName)
                                        .setValue(excelDataConvertException.getCellData().getStringValue())
                                );
                    } else {
                        log.error("解析失败:", exception);
                        // 补充错误信息
                        readResult.setRowIndex(-1);
                        readResult.setMessage(ExceptionUtil.getSimpleMessage(exception));
                        readResult.addCol(ReadCol.newBuilder()
                                .setIndex(-1)
                                .setName(StrUtil.EMPTY)
                                .setValue(StrUtil.EMPTY)
                                .build());
                    }

                    // 响应错误
                    responseObserver.onNext(readResult.build());

                    // 当遇到异常是是否结束读取
                    if (readRequest.getStopWhenError()) {
                        // 停止读取
                        hasNext = false;
                    }
                }

                @Override
                public void invoke(Map<Integer, String> data, AnalysisContext context) {
                    System.err.println("excel ddddddddd");
                    if (!hasNext) {
                        return;
                    }


                    // 判断第一行是否有合并单元格“明细”列(判断依据:有相同的标题,或者有空的标题)(按理来说判断合并单元格是更合适的,奈何invoke中不支持判断。)
                    if (context.readRowHolder().getRowIndex() == 0) {
                        if (CollUtil.count(data.values(), StrUtil::isEmpty) > 0 || hasDuplicates(data.values())) {
                            String last = "";
                            // 处理合并单元格后只有最左侧的有值的问题
                            for (Integer index : data.keySet()) {
                                last = StrUtil.emptyToDefault(data.get(index), last);
                                headGroupMap.put(index, last);
                            }
                        } else {
                            headMap.putAll(data);
                        }
                        // 第一行肯定不是数据,直接返回
                        return;
                    }
                    // 判断第二行是否是标题
                    if (context.readRowHolder().getRowIndex() == 1) {
                        // 如果headMap是空的,那么就是定位到第二行才是真正的标题行
                        if (MapUtil.isEmpty(headMap)) {
                            data.forEach((index, name) -> {
                                // 兼容合并单元格后只在第一行有值的问题
                                name = StrUtil.emptyToDefault(name, headGroupMap.getOrDefault(index, StrUtil.EMPTY));
                                headMap.put(index, name);
                            });
                            return;
                        }
                    }
                    // 构建读取信息
                    ReadResult.Builder analysed = ReadResult.newBuilder().setStatus(ReadStatus.RS_SUCCESS).setRowIndex(context.readRowHolder().getRowIndex());
                    // 头
                    headMap.forEach((index, name) -> {
                        Assert.notEmpty(name, () -> new BizRuntimeException(ExcelError.READ_HEAD_HAS_EMPTY));
                        analysed.addHead(ReadHead.newBuilder()
                                .setIndex(index)
                                .setName(name)
                                // 当没有明细的时候,分组就是头的值
                                .setGroupName(headGroupMap.getOrDefault(index, name))
                                .build());
                    });
                    // 数据
                    data.forEach((index, value) -> analysed.addCol(ReadCol.newBuilder()
                            .setIndex(index)
                            .setName(headMap.getOrDefault(index, StrUtil.EMPTY))
                            .setValue(StrUtil.emptyToDefault(value, StrUtil.EMPTY))
                            .build()
                    ));
                    // 响应行信息
                    responseObserver.onNext(analysed.build());
                }

                @Override
                public void doAfterAllAnalysed(AnalysisContext context) {
                    // 完成后结束GRPC
                    responseObserver.onCompleted();
                }
            }).headRowNumber(0).sheet().doRead();


        } catch (Exception e) {
            log.error("读取Excel(" + readRequest.getKey() + ")失败", e);
            // 构建错误信息
            ReadResult.Builder analysed = ReadResult.newBuilder()
                    .setRowIndex(-1)
                    .setStatus(ReadStatus.RS_ERROR)
                    .setMessage(ExceptionUtil.getSimpleMessage(e));
            responseObserver.onNext(analysed.build());
            responseObserver.onCompleted();
        }
    }

-- 2024-5-28 11:24:03 --

补充:我的思路是基于不创建对象的读,如果说想更好的配合基于对象的特性,我提供的例子确实不太适合(但是官方提供的基于对象的读,不知道能不能满足你们需求,反正是满足不了我的)。

补充:我这个方法,支持index,还支持name。支持name就是支持列任意排序!

Alleninggx avatar May 27 '24 07:05 Alleninggx

这样是每行读取了吧 然后判断是否是表头 还是数据行。感觉还是有点麻烦诶,,主要原因还是他这个合并单元后只会读取到第一行的表头数据 后面行是读取是空的 才导致数据行没匹配到对应的表头上,目前easyexcel只支持读取到最后一行的表头 https://github.com/alibaba/easyexcel/issues/1430

我试过可以在AnalysisEventListener的invokeHeadMap或者invokeHead方法中补齐合并表头,然后补齐AnalysisContext.readSheetHolder().excelReadHeadProperty().getHeadMap()的表头映射关系就可以找到对应关系,这样就可以用name而不是index。

是不是可以给加一个AnalysisContext.readSheetHolder().excelReadHeadProperty().getHeadMap()加一个回调方便修改。

采用这个思路好像也行,补全表头,然后映射对应列的数据行。

目前我这边列是固定的,采用index搭配也还能解决,持续关注中。。

cloudlessa avatar May 28 '24 02:05 cloudlessa