这几天需要读取excel的内容,但是excel中存在多个sheet页,每个sheet页的标题不同,数据不同,而且多个excel文件。决定使用easyexcel处理,感觉这样的excel文件和内容无法使用对象接收excel的数据,所以决定使用easyexceld的"不创建对象的读"来接受数据。
在测试使用的过程中,发现如果excel存在合并单元格的情况,读取出来的数据不是自己想要的,话不多说,直接上图说明,假如有一个excel文件,存在一个sheet页,数据如下:
可以看出,存在几个地方合并单元的数据,如果什么都不处理,直接读取当前数据,结果如下:
很明显这并不是我想要的,因为按照excel合并单元格之后,例如张三和李四的年龄,都应该是20才对,但实际的数据只有张三的年龄是20,李四的年龄是null。
我在网上查询的时候,看到有人说,easyexcel只有合并单元格的左上角单元格才有值,其他的单元格没有值。
所以我的想法就是根据官方文档,获取到excel中的所有行数据,以及合并单元格的数据信息,最后整合这两部分数据,根据单元格的信息,去更新行数据。
具体的easyexcel用法自行参考官方,代码是我自己写的,绝对不是随便copy过来的,亲测有效。
我写代码测试的时候是按照多个sheet页来的(返回多个sheet页的所有数据),但实际测试的excel只创建了一个sheet页。因为我只是测试这个过程,并没有涉及实际的业务,后面会根据这个demo来实现实际业务中的需求。
具体实现如下:
java
package demo.jiangkd.easyexcel.controller;
import demo.jiangkd.easyexcel.service.EasyExcelMergeDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* @author jiangkd
* @date 2024/8/9 9:16
*/
@RequiredArgsConstructor
@RequestMapping("/easyexcel")
@RestController
public class EasyExcelDemoController {
private final EasyExcelMergeDemoService easyExcelMergeDemoService;
/**
* 如果excel中带有合并单元格, 又是不创建对象的读取
*
* @param filename
*/
@GetMapping("/merge/demo")
public List<Map<Integer, String>> EasyExcel(String filename) {
//
return easyExcelMergeDemoService.mergeDemo(filename);
}
}
java
package demo.jiangkd.easyexcel.service;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.excel.read.metadata.ReadSheet;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author jiangkd
* @date 2024/8/9 14:49
*/
@Slf4j
@Service
public class EasyExcelMergeDemoService {
public List<Map<Integer, String>> mergeDemo(String filename) {
// 所有的sheet, 不用管excel有几个sheet, 数据全部返回
@Cleanup final ExcelReader build = EasyExcel.read(filename).build();
final List<ReadSheet> readSheets = build.excelExecutor().sheetList();
// 最终要返回的数据
final List<Map<Integer, String>> datas = new ArrayList<>();
// 从excel文件的第几行开始读取,默认是从第二行开始的(认为第一行是标题行)
final int headRowNumber = 0;
@Cleanup
ExcelReader excelReader = EasyExcel.read(filename)
// 从sheet页的第一行开始读取,默认是从第二行开始的
.headRowNumber(headRowNumber)
// 需要读取合并单元格信息 默认不读取(官方文档有说明)
.extraRead(CellExtraTypeEnum.MERGE)
.build();
// 循环所有sheet页,读取数据,都放入datas集合中
readSheets.forEach(readSheet -> {
log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 开始读取sheet, {}, {}", readSheet.getSheetNo(),
readSheet.getSheetName());
// 读取数据的监听
final NoModelMergeDataListener noModelMergeDataListener = new NoModelMergeDataListener(headRowNumber);
final ReadSheet sheet = EasyExcel.readSheet(readSheet.getSheetNo())
.registerReadListener(noModelMergeDataListener)
.build();
excelReader.read(sheet);
// 获取行数据 和 合并的单元格信息
final List<CellExtra> extraMergeInfoList = noModelMergeDataListener.getExtraMergeInfoList();
final List<Map<Integer, String>> excelDatas = noModelMergeDataListener.getDatas();
if (CollectionUtils.isEmpty(extraMergeInfoList)) {
// 如果不存在合并单元格的数据
datas.addAll(excelDatas);
} else {
// 否则处理行数据
datas.addAll(this.explainMergeData(excelDatas, extraMergeInfoList));
}
});
return datas;
}
private List<Map<Integer, String>> explainMergeData(List<Map<Integer, String>> excelDatas,
List<CellExtra> extraMergeInfoList) {
// easyexcel默认处理合并单元格, 只有左上角的有值, 其他的没值
// 先找出合并的单元格的值
for (CellExtra cellExtra : extraMergeInfoList) {
// 通过cellExtra的rowIndex和columnIndex来确定该单元格的值
final Integer rowIndex = cellExtra.getRowIndex();
final Integer columnIndex = cellExtra.getColumnIndex();
// 通过rowIndex确定第几行数据
final Map<Integer, String> rowData = excelDatas.get(rowIndex);
// 通过columnIndex确定这一行数据的第几列
final Set<Map.Entry<Integer, String>> entries = rowData.entrySet();
final List<Map.Entry<Integer, String>> entriesList = new ArrayList<>(entries);
final Map.Entry<Integer, String> entry = entriesList.get(columnIndex);
final String value = entry.getValue();
log.info("当前合并的单元格的值是:{}", value);
// 获取合并单元格的范围Index数据, 也就是跨越了那些单元格
final Integer firstRowIndex = cellExtra.getFirstRowIndex();
final Integer lastRowIndex = cellExtra.getLastRowIndex();
final Integer firstColumnIndex = cellExtra.getFirstColumnIndex();
final Integer lastColumnIndex = cellExtra.getLastColumnIndex();
// 纵使easyexcel合并单元只有左上角的单元格有值, 为了方便, 这里直接把范围单元格的值全部替换掉为value
// 获取要处理的行数据都有哪些, 也就是[firstRowIndex, lastRowIndex]的行数据
final List<Map<Integer, String>> mergeRowDatas = new ArrayList<>();
final int size = excelDatas.size();
for (int i = 0; i < size; i++) {
if (firstRowIndex <= i && i <= lastRowIndex) {
mergeRowDatas.add(excelDatas.get(i));
}
}
// 确定了哪些行数据中存在合并的单元格, 接下来更新这些行数据的那些列
for (Map<Integer, String> mergeRowData : mergeRowDatas) {
// 这一行数据的所有列
final Set<Map.Entry<Integer, String>> mergeRowDataEntries = mergeRowData.entrySet();
final List<Map.Entry<Integer, String>> mergeRowDataEntriyList = new ArrayList<>(mergeRowDataEntries);
// 确定列并且更新列的值
final int columnSize = mergeRowDataEntriyList.size();
for (int i = 0; i < columnSize; i++) {
if (firstColumnIndex <= i && i <= lastColumnIndex) {
final Map.Entry<Integer, String> columnEntry = mergeRowDataEntriyList.get(i);
columnEntry.setValue(value);
}
}
}
}
return excelDatas;
}
}
java
package demo.jiangkd.easyexcel.service;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @author jiangkd
* @date 2024/8/9 14:52
*/
@Slf4j
public class NoModelMergeDataListener implements ReadListener<Map<Integer, String>> {
/**
* excel解析的数据
*/
final List<Map<Integer, String>> datas = new ArrayList<>();
/**
* 正文起始行
*/
private Integer headRowNumber;
/**
* 合并单元格
*/
private List<CellExtra> extraMergeInfoList = new ArrayList<>();
public List<Map<Integer, String>> getDatas() {
return datas;
}
public List<CellExtra> getExtraMergeInfoList() {
return extraMergeInfoList;
}
public NoModelMergeDataListener(Integer headRowNumber) {
this.headRowNumber = headRowNumber;
}
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
//
log.info("解析到一条数据:{}", JSON.toJSONString(data));
datas.add(data);
}
@Override
public void extra(CellExtra extra, AnalysisContext context) {
//
log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));
if (Objects.requireNonNull(extra.getType()) == CellExtraTypeEnum.MERGE) {
log.info("额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",
extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
extra.getLastColumnIndex());
if (extra.getRowIndex() >= headRowNumber) {
extraMergeInfoList.add(extra);
}
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
//
log.info("======================================");
log.info("所有数据解析完成!");
log.info("======================================");
}
}
针对如上代码,说明几点:
- 里面没有说明注释的代码,翻看easyexcel官方文档就会明白了。
- 读取了excel中的所有sheet页,循环sheet读取数据,最后一块全部返回,如果你只读取第一个sheet页,略改代码即可(或者直接不改)。
- 从监听中获取excel的行数据和合并单元格的数据信息。
- 合并单元格的信息主要就是单元格的位置,行列的index,可以确定那些范围内的单元格是合并的。
然后再次执行,结果如下:
现在数据是我想要的了。
马上就要下班了,文章写的有点乱,格式不是很清晰,但是代码其实没有很多,结合官方文档,很容易理解的。
至于代码中的CellExtra,以及get出来的几个Index数据,执行代码测试的时候,查看控制台打印结果,对比excel,你就明白了。