EasyExcel - 行合并策略(二级列表)

😼前言:博主在工作中又遇到了新的excel导出挑战:需要导出多条文章及其下联合作者的信息,简单的来说是一个二级列表的数据结构。
🕵️‍♂️思路:excel导出实际上是一行一行的记录,再根据条件对其进行合并。

目录

  • 最终效果图📌
  • 一、数据格式及处理📚
    • 1.文章对象(处理前)
    • 2.文章及作者对象(处理后)
    • 3.未合并的效果图
  • 二、通用行合并策略🔍
    • 1.源码学习
    • 2.通用行合并后的效果图
  • 三、二级通用行合并策略✍
    • 1.源码改造
    • 2.设置excel输出策略
    • 3.延伸
  • 四、问题☔
    • 1.问题描述
    • 2.问题原因
    • 3.解决办法
  • 参考文章📒

最终效果图📌

最终导出格式

一、数据格式及处理📚

首先,需要先将一条文章按联合作者数量,拆分为指定数量的文章及作者导出记录的集合。

以文章《牧区歌与马》为例,一篇文章有三名联合作者,生成三条导出记录。

1.文章对象(处理前)

// 文章记录对象 Acticle.class
[{"contentId": "1","contentTitle": "牧区歌与马","contentCount": 940,"releaseTime": "2025-01-09 11:21:16","readNum": 1,"auditor": "小李","orgName": "办公室","authorList": [{"userName": "小A ","orgName": "单位A"},{"userName": "小B ","orgName": "/"},{"userName": "小C","orgName": "单位C"}]
}]

2.文章及作者对象(处理后)

可以看到记录由一条变为三条,除了作者名称和单位,其余字段内容均一致。

// 文章处理后记录对象 ActicleAuthor.class
[{"contentId": "1","contentTitle": "牧区歌与马","contentCount": 940,"releaseTime": "2025-01-09 11:21:16","readNum": 1,"auditor": "小李","orgName": "办公室","author":"小A""authorUnit":"单位A"
},{"contentId": "1","contentTitle": "牧区歌与马","contentCount": 940,"releaseTime": "2025-01-09 11:21:16","readNum": 1,"auditor": "小李","orgName": "办公室","author":"小B""authorUnit":"/"
},{"contentId": "1","contentTitle": "牧区歌与马","contentCount": 940,"releaseTime": "2025-01-09 11:21:16","readNum": 1,"auditor": "小李","orgName": "办公室","author":"小C""authorUnit":"单位C"
}]

3.未合并的效果图

未设置行合并策略直接导出时,表格的格式内容如下:
未合并前的导出格式
👆图中的E、F列示例错误,应分别为6行记录

二、通用行合并策略🔍

此处学习了csdn博主xiao谢同学分享的通用行合并策略源码
链接:EasyExcel 通用行合并策略实现

1.源码学习

🐱‍👓该策略以列的行数作为主键,每次遍历记录列的最新合并区域信息。将同列且相邻行的单元格内容进行比较:
(1)一致:则仅更新endRow和endCell,继续遍历;
(2)不一致:则将已有区域进行合并,再将MergeRange所有字段进行更新。

MergeRange.class

  • startRow :合并开始行
  • endRow:合并结束行
  • startCell:合并开始单元格
  • endCell:合并结束单元格
  • lastValue:列最新单元格内容
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.commons.collections.map.HashedMap;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;import java.util.*;public class MergeStrategy extends AbstractMergeStrategy {// 合并的列编号,从0开始,指定的index或自己按字段顺序数private Set<Integer> mergeCellIndex = new HashSet<>();// 数据集大小,用于区别结束行位置private Integer maxRow = 0;// 禁止无参声明private MergeStrategy() {}public MergeStrategy(Integer maxRow, int... mergeCellIndex) {Arrays.stream(mergeCellIndex).forEach(item -> {this.mergeCellIndex.add(item);});this.maxRow = maxRow;}// 记录上一次合并的信息private Map<Integer, MergeRange> lastRow = new HashedMap();// 每行每列都会进入,绝对不要在这写循环@Overrideprotected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {int currentCellIndex = cell.getColumnIndex();// 判断该列是否需要合并if (mergeCellIndex.contains(currentCellIndex)) {String currentCellValue = cell.getStringCellValue();int currentRowIndex = cell.getRowIndex();if (!lastRow.containsKey(currentCellIndex)) {// 记录首行起始位置lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));return;}//有上行这列的值了,拿来对比.MergeRange mergeRange = lastRow.get(currentCellIndex);if (!(mergeRange.lastValue != null && mergeRange.lastValue.equals(currentCellValue))) {// 结束的位置触发下合并.// 同行同列不能合并,会抛异常if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));}// 更新当前列起始位置lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));}// 合并行 + 1mergeRange.endRow += 1;// 结束的位置触发下最后一次没完成的合并if (relativeRowIndex.equals(maxRow - 1)) {MergeRange lastMergeRange = lastRow.get(currentCellIndex);// 同行同列不能合并,会抛异常if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));}}}}
}class MergeRange {public int startRow;public int endRow;public int startCell;public int endCell;public String lastValue;public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell) {this.startRow = startRow;this.endRow = endRow;this.startCell = startCell;this.endCell = endCell;this.lastValue = lastValue;}
}

2.通用行合并后的效果图

可以发现,这种仅根据相邻行单元格内容进行合并的方式,还未能完全满足博主想要二级列表的效果。
简单行合并的导出格式
👆黄色代表非理想合并的区域

三、二级通用行合并策略✍

🐱‍💻改造思路:列A是文章标题,以列A的内容作为第一层级的标识(tip:不如contentId准确)。即使相邻行单元格内容相等,对应行的A列内容不相等也不能合并。

处理步骤:
(1)在合并区域对象类MergeRange中,增加A列内容的值字段lastValueRowa
(2)遍历单元格构造合并区域对象时,记录A列内容值以此来作为附加的合并条件。
(3)当同一列字段相邻行内容相等且A列内容值相等时,再进行合并。

1.源码改造

import cn.hutool.json.JSONUtil;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.map.HashedMap;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;import java.util.*;/*** 行合并策略*/
@Slf4j
public class MergeStrategy extends AbstractMergeStrategy {// 合并的列编号,从0开始,指定的index或自己按字段顺序数private Set<Integer> mergeCellIndex = new HashSet<>();// 数据集大小,用于区别结束行位置private Integer maxRow = 0;// 禁止无参声明private MergeStrategy() {}public MergeStrategy(Integer maxRow, int... mergeCellIndex) {Arrays.stream(mergeCellIndex).forEach(item -> {this.mergeCellIndex.add(item);});this.maxRow = maxRow;}// 记录上一次合并的信息private Map<Integer, MergeRange> lastRow = new HashedMap();// 每行每列都会进入,绝对不要在这写循环@Overrideprotected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {// 获取单元格当前列int currentCellIndex = cell.getColumnIndex();log.info("遍历单元格:{}行,{}列 >>>>>>>>>>>>>>>>>", cell.getRowIndex(), currentCellIndex);// 判断该列是否需要合并if (mergeCellIndex.contains(currentCellIndex)) {// 获取当前单元格内容值String currentCellValue = new DataFormatter().formatCellValue(cell);// 获取当前单元格的行rowint currentRowIndex = cell.getRowIndex();// 获取当前行A列单元格内容 newString currentValueRowa = "";// 如果最后合并行的map,不包括当前列if (!lastRow.containsKey(currentCellIndex)) {log.info("lastRow添加第{}列【前】,当前lastRow={}", currentCellIndex, JSONUtil.toJsonStr(lastRow));// 获取当前行的列A内容 newif (currentCellIndex == 0) {currentValueRowa = currentCellValue;} else {currentValueRowa = getRowaValue();}// 记录首行起始位置? 记录当前列及合并范围lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex, currentValueRowa));log.info("lastRow添加第{}列【后】,当前行列A内容={},新lastRow={}", currentCellIndex, currentValueRowa, JSONUtil.toJsonStr(lastRow));return;} else {// 该列已存在lastRow中,则取最新的列A内容值 newcurrentValueRowa = getRowaValue();}//有上行这列的值了,拿来对比.MergeRange mergeRange = lastRow.get(currentCellIndex);// 判断条件:增加A列内容判断 newlog.info("合并比对1>>>>>>>>>:第{}列最新内容lastValue = {},当前内容currentCellValue={},", currentCellIndex, mergeRange.lastValue, currentCellValue);log.info("合并比对2>>>>>>>>>第{}列最新列A内容lastValueRowa={}, 当前列A内容currentValueRowa={}", currentCellIndex, mergeRange.lastValueRowa, currentValueRowa);if (!(mergeRange.lastValue != null&& mergeRange.lastValue.equals(currentCellValue) && mergeRange.lastValueRowa.equals(currentValueRowa))) {// 结束的位置触发下合并.// 同行同列不能合并,会抛异常if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));}// 更新当前列起始位置lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex, currentValueRowa));log.info("比对不一致,确认合并!!>>>>>>>>>:第{}列最新列A内容={},最新lastRow = {}", currentCellIndex, currentValueRowa, JSONUtil.toJsonStr(lastRow));}// 合并行 + 1mergeRange.endRow += 1;// 结束的位置触发下最后一次没完成的合并if (relativeRowIndex.equals(maxRow - 1)) {MergeRange lastMergeRange = lastRow.get(currentCellIndex);// 同行同列不能合并,会抛异常if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));}}}}/*** 获取列A最新一行的内容值 new*/private String getRowaValue() {// 获取当前行A列单元格内容String currentValueRowa = "";if (lastRow.get(0) != null) {currentValueRowa = lastRow.get(0).lastValue;}return currentValueRowa;}
}class MergeRange {public int startRow;public int endRow;public int startCell;public int endCell;public String lastValue;// 最后一个合并值得A列值 newpublic String lastValueRowa;public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell, String lastValueRowa) {this.startRow = startRow;this.endRow = endRow;this.startCell = startCell;this.endCell = endCell;this.lastValue = lastValue;this.lastValueRowa = lastValueRowa;}
}

2.设置excel输出策略

  // 设置excel输出策略EasyExcel.write(fileName, ActicleAuthor.class)// 0,1 表示 对1,2列启用合并策略.registerWriteHandler(new MergeStrategy(dataList.size(),0,1)) .sheet(0).doWrite(dataList);

3.延伸

🐱‍🚀如果需要做三级、四级等列表,可以将指定多个字段的拼接值当作列A来处理。可修改getRowaValue()方法实现逻辑。

四、问题☔

在开发的过程中,不可避免地碰到了一些问题……

1.问题描述

💁‍♀️在获取列A内容值时,曾尝试从Sheet对象中获取。
因为存在Sheet.getRow(0)获取第一行的row对象是null的问题,所以用从lastRow中获取列A内容的方法进行替代。

/*** 从sheet.getRow(0)中获取列A内容值*/private String getRowaValue(Sheet sheet, int rowId) {// 获取当前行A列单元格内容String currentValueRowa = "";Row row = sheet.getRow(rowId);if (row != null) {Cell cell = row.getCell(0);if (cell != null) {currentValueRowa = cell.getStringCellValue();}}return currentValueRowa;}

2.问题原因

👩‍💻经过面向百度查询,从csdn博主吾乃南华老仙分享的文章sheet.getRow(0)获取的row为null?中得知:

new SXSSFWorkbook(new XSSFWorkbook(inputStream)) 创建Workbook的时候,
SXSSFWorkbook对象内部会维护一个HashMap(反编译后的名称为_xFromSxHash)。

而当使用workBook.getSheetAt(0)的时候,其实是从_xFromSxHash中获取新创建的Sheet对象,从而导致sheet.getRow(0)获取的row为null。

😸文中提供的解决方法:

将获取首行代码:
Workbook workBook = new SXSSFWorkbook(new XSSFWorkbook(inputStream));
Sheet sheet = workBook.getSheetAt(0);
Row row = sheet.getRow(0);修改为👇:Workbook workBook = new SXSSFWorkbook(new XSSFWorkbook(inputStream));
Sheet sheet;
if (workBook instanceof SXSSFWorkbook) {SXSSFWorkbook sxssfWorkbook = (SXSSFWorkbook) workBook;sheet = sxssfWorkbook.getXSSFWorkbook().getSheetAt(sheetIndex);
} else {sheet = workBook.getSheetAt(sheetIndex);
}
Row row = sheet.getRow(0);

😧而我们使用的导出是基于EasyExcel的,并没有单独的使用流去创建对象,应该怎么办呢?

👇可以看到debug过程中,显示sheet对象类型是SXSSFSheet。
在这里插入图片描述

3.解决办法

😾再次经过一番查询,在EasyExcel语雀文档的QA:EasyExcel 我想在导出excel文件的时候添加水印,要怎么做,请给出代码示例和解释中找到了解释:

  • inMemory(true):
    EasyExcel默认使用SXSSFWorkbook以减少内存消耗,但它不支持复杂的样式设置(如水印)。通过设置inMemory(true),我们改用XSSFWorkbook,它提供了更全面的样式支持。

在设置excel输出策略时加上inMemory(true)设置:

  // 设置excel输出策略EasyExcel.write(fileName, ActicleAuthor.class)// 必须设置,以便使用XSSFWorkbook而非SXSSFWorkbook new.inMemory(true)// 0,1 表示 对1,2列启用合并策略.registerWriteHandler(new MergeStrategy(dataList.size(),0,1)) .sheet(0).doWrite(dataList);

🙆‍♀️执行debug后,发现可以获取到sheet.getRow(0)的第一行对象了,切换类型的问题成功解决。
在这里插入图片描述

参考文章📒

EasyExcel 通用行合并策略实现-xiao谢同学
sheet.getRow(0)获取的row为null?
EasyExcel 我想在导出excel文件的时候添加水印,要怎么做,请给出代码示例和解释

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/1481.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

软件测试面试题整理

一、人格相关问题 1、自我介绍结构 姓名工作年限简单介绍上家公司的行业主要负责内容个人优势短期内的职业规划应聘该岗位的原因 2、对未来的发展方向怎么看&#xff1f; 没有标准答案&#xff0c;职业规划来讲&#xff0c;可以分为技术层面和管理层面去说&#xff0c;技术…

.NET framework、Core和Standard都是什么?

对于这些概念一直没有深入去理解&#xff0c;以至于经过.net这几年的发展进化&#xff0c;概念越来越多&#xff0c;越来越梳理不容易理解了。内心深处存在思想上的懒惰&#xff0c;以为自己专注于Unity开发就好&#xff0c;这些并不属于核心范畴&#xff0c;所以对这些概念总是…

CNN张量输入形状和特征图

CNN张量输入形状和特征图 这个是比较容易理解的张量的解释&#xff0c;比较直观 卷积神经网络 在这个神经网络编程系列中&#xff0c;我们正在逐步构建一个卷积神经网络&#xff08;CNN&#xff09;&#xff0c;所以让我们看看CNN的张量输入。 ​ ​ 在最后两篇文章中&…

【数据可视化-12】数据分析岗位招聘分析

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

(12)springMVC文件的上传

SpringMVC文件上传 首先是快速搭建一个springMVC项目 新建项目mvn依赖导入添加webMoudle添加Tomcat运行环境.在配置tomcat时ApplicationContext置为"/"配置Artfact的lib配置WEB-INF配置文件&#xff08;记得添加乱码过滤&#xff09;配置springmvc-servlet文件&…

Ubuntu中双击自动运行shell脚本

方法1: 修改文件双击反应 参考: https://blog.csdn.net/miffywm/article/details/103382405 chmod x test.sh鼠标选中待执行文件&#xff0c;在窗口左上角edit菜单中选择preference设计双击执行快捷键&#xff0c;如下图&#xff1a; 方法2: 设置一个应用 参考: https://blo…

Linux(Centos7)安装Mysql/Redis/MinIO

安装Mysql 安装Redis 搜索Redis最先版本所在的在线安装yum库 查看以上两个组件是否是开机自启 安装MinIO 开源的对象存储服务&#xff0c;存储非结构化数据&#xff0c;兼容亚马逊S3协议。 minio --help #查询命令帮助minio --server --help #查询--server帮助minio serve…

金融项目实战 01|功能测试分析与设计

前置内容&#xff1a;金融项目准备的内容笔记可直接看如下笔记 只看&#xff1a;一、投资专业术语 和 二、项目简介 两部分文章浏览阅读2.3k次&#xff0c;点赞70次&#xff0c;收藏67次。安享智慧理财金融系统测试项目&#xff0c;测试用例&#xff0c;接口测试&#xff0c;金…

【Rust】控制流

目录 思维导图 一、选择结构 1. if表达式 2. 处理多个条件的else if 3. 使用if在let语句中 二、循环结构 1. loop 2. while循环 3. for循环 4. 使用范围Range进行循环 思维导图 一、选择结构 控制流是编程语言的基本构建块&#xff0c;Rust使用if表达式和循环来控制代…

FastDDS安装测试记录

1、安装依赖的软件 sudo apt install cmake g python3-pip wget git sudo apt install libasio-dev libtinyxml2-dev sudo apt install libssl-dev sudo apt install libp11-dev libengine-pkcs11-openssl sudo apt install softhsm22、安装foonathan_memory_vendor cd ~/Fas…

浅谈云计算01 | 云计算服务的特点

在当今数字化时代&#xff0c;云计算作为一种强大的技术解决方案&#xff0c;正逐渐改变着企业和个人对信息技术的使用方式。本文将详细探讨云计算的五个主要特点&#xff0c;包括按需自助服务、广泛的网络接入、资源池化、快速弹性伸缩以及可计量服务。 一、按需自助服务 云…

《使用 YOLOV8 和 KerasCV 进行高效目标检测》

《使用 YOLOV8 和 KerasCV 进行高效目标检测》 作者&#xff1a;Gitesh Chawda创建日期&#xff1a;2023/06/26最后修改时间&#xff1a;2023/06/26描述&#xff1a;使用 KerasCV 训练自定义 YOLOV8 对象检测模型。 &#xff08;i&#xff09; 此示例使用 Keras 2 在 Colab 中…

vue3+ts+element-plus 对话框el-dialog设置圆角

对话框el-dialog设置圆角&#xff0c;实现的需求效果&#xff1a; 目前只能通过行内样式&#xff08;style"border-radius: 20px"&#xff09;来实现圆角效果&#xff1a;

pycharm-pyspark 环境安装

1、环境准备&#xff1a;java、scala、pyspark、python-anaconda、pycharm vi ~/.bash_profile export SCALA_HOME/Users/xunyongsun/Documents/scala-2.13.0 export PATH P A T H : PATH: PATH:SCALA_HOME/bin export SPARK_HOME/Users/xunyongsun/Documents/spark-3.5.4-bin…

UnityXR Interaction Toolkit 如何检测HandGestures

前言 随着VR设备的不断发展,从最初的手柄操作,逐渐演变出了手部交互,即头显可以直接识别玩家的手部动作,来完成手柄的交互功能。我们今天就来介绍下如何使用Unity的XR Interaction Toolkit 来检测手势Hand Gesture。 环境配置 1.使用Unity 2021或者更高版本,创建一个项…

thinkphp 5.0 结合redis 做延迟队列,队列无法被消费

目录 一、Linux 环境下 二、如何验证消息队列被正确监听 一、Linux 环境下 项目部署在Linux 环境下&#xff0c;首先找到项目的部署路径&#xff0c;接着输入命令,这个命令是以守护进程方式进行监听你的队列&#xff0c;只要redis 不关闭 就可以一直监听这个队列 nohup php …

E10.【C语言】练习:编写一个猜数字游戏

目录 1.规则 2.准备 3.游戏代码 1.规则 1.程序生成1-100间的随机数 2.用户猜数字 猜对了&#xff1a;游戏结束 猜错了&#xff1a;程序会告知猜大了或猜小了&#xff0c;继续进行游戏&#xff0c;直到猜对 3.游戏可以一直玩除非退出游戏 2.准备 1.框架&#xff1a;循…

【HTML+CSS+JS+VUE】web前端教程-31-css3新特性

圆角 div{width: 100px;height: 100px;background-color: saddlebrown;border-radius: 5px;}阴影 div{width: 200px;height: 100px;background-color: saddlebrown;margin: 0 auto;box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.5);}

【高阶数据结构】位图

位图 一.位图相关面试题二.位图的设计及实现三.C库中的位图bitset四.位图的优缺点五.位图相关考察题目 一.位图相关面试题 问题&#xff1a;给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数是否在这40亿个数中&#xff08;本…

解决Qt打印中文字符出现乱码

在 Windows 平台上&#xff0c;默认的控制台编码可能不是 UTF-8&#xff0c;这可能会导致中文字符的显示问题。 下面是在 Qt 应用程序中设置中文字体&#xff0c;并确保控制台输出为 UTF-8 编码&#xff1a; 1. Qt 应用程序代码 在 Qt 中&#xff0c;我们可以使用 QApplic…