使用Apache POI处理XLSX:列交换与合并单元格行内容合并
Apache POI 处理XLSX文件的解决方案
咱们一步步来解决你提出的两个Apache POI相关问题:
问题1:交换XLSX文件中的列
交换列的核心逻辑是遍历工作表的每一行,交换指定列的单元格内容和样式。要注意POI的单元格索引是0基的,而且如果表格存在合并单元格,需要额外调整合并区域的范围(避免交换后合并区域错位)。
实现步骤
- 加载目标XLSX文件,获取要操作的工作表
- 确定需要交换的两个列的索引(比如要交换Name2和Name6,对应索引1和5)
- 遍历每一行,交换对应列的单元格值和样式
- (可选)如果涉及合并单元格,调整合并区域的列范围
代码示例
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class XLSXColumnSwapper { public static void main(String[] args) throws IOException { // 加载输入文件 try (Workbook workbook = new XSSFWorkbook(new FileInputStream("input.xlsx"))) { Sheet targetSheet = workbook.getSheetAt(0); // 取第一个工作表 // 定义要交换的两个列索引(0基) int columnA = 1; // 对应Name2列 int columnB = 5; // 对应Name6列 // 遍历所有行,交换单元格内容与样式 for (Row row : targetSheet) { // 获取或创建目标列的单元格(避免空单元格报错) Cell cellA = row.getCell(columnA, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); Cell cellB = row.getCell(columnB, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); // 交换单元格值 Object tempValue = getCellValue(cellA); setCellValue(cellA, getCellValue(cellB)); setCellValue(cellB, tempValue); // 交换单元格样式 CellStyle tempStyle = cellA.getCellStyle(); cellA.setCellStyle(cellB.getCellStyle()); cellB.setCellStyle(tempStyle); } // 调整涉及交换列的合并区域(如果有) for (int i = targetSheet.getNumMergedRegions() - 1; i >= 0; i--) { CellRangeAddress mergedRegion = targetSheet.getMergedRegion(i); boolean includesA = mergedRegion.getFirstColumn() <= columnA && mergedRegion.getLastColumn() >= columnA; boolean includesB = mergedRegion.getFirstColumn() <= columnB && mergedRegion.getLastColumn() >= columnB; if (includesA && includesB) continue; // 同时包含两列,无需调整 else if (includesA) { // 移除旧合并区域,替换为包含columnB的新区域 targetSheet.removeMergedRegion(i); targetSheet.addMergedRegion(new CellRangeAddress( mergedRegion.getFirstRow(), mergedRegion.getLastRow(), columnB, mergedRegion.getLastColumn() == columnA ? columnB : mergedRegion.getLastColumn() )); } else if (includesB) { targetSheet.removeMergedRegion(i); targetSheet.addMergedRegion(new CellRangeAddress( mergedRegion.getFirstRow(), mergedRegion.getLastRow(), columnA, mergedRegion.getLastColumn() == columnB ? columnA : mergedRegion.getLastColumn() )); } } // 保存输出文件 try (FileOutputStream fos = new FileOutputStream("output_swapped.xlsx")) { workbook.write(fos); } } } // 通用方法:获取单元格的实际值(兼容各种类型) private static Object getCellValue(Cell cell) { return switch (cell.getCellType()) { case STRING -> cell.getStringCellValue(); case NUMERIC -> DateUtil.isCellDateFormatted(cell) ? cell.getDateCellValue() : cell.getNumericCellValue(); case BOOLEAN -> cell.getBooleanCellValue(); case FORMULA -> cell.getCellFormula(); default -> ""; }; } // 通用方法:设置单元格值(兼容各种类型) private static void setCellValue(Cell cell, Object value) { if (value == null || value.toString().isEmpty()) { cell.setCellValue(""); return; } if (value instanceof String str) cell.setCellValue(str); else if (value instanceof Double num) cell.setCellValue(num); else if (value instanceof Boolean bool) cell.setCellValue(bool); else if (value instanceof java.util.Date date) cell.setCellValue(date); } }
问题2:处理合并单元格时的行内容追加问题
你遇到的"Part of a merged cell"错误,是因为合并单元格区域中只有左上角的单元格存储了实际值,其他单元格是合并的占位符,直接读取会返回这个提示。我们需要先判断单元格是否属于合并区域,再获取正确的值。
需求拆解
把Name6行下方的内容追加到该行,最终表头变为Name1、Name2……Name6admin|Name6loc、Name7,同时跳过下方行。
修改后实现逻辑
- 定位到Name6所在的目标行
- 遍历目标行下方的行,读取单元格值(处理合并单元格,获取实际值)
- 将读取到的值追加到目标行的对应列后(比如Name6列后新增列存储admin/loc)
- 从后往前删除目标行下方的行(避免索引错乱)
- (可选)调整表头的合并区域,适配新的列结构
代码示例
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class AppendRowWithMergedCells { public static void main(String[] args) throws IOException { try (Workbook workbook = new XSSFWorkbook(new FileInputStream("input.xlsx"))) { Sheet targetSheet = workbook.getSheetAt(0); int targetRowIdx = 5; // 假设Name6在第6行(0基索引为5) Row targetRow = targetSheet.getRow(targetRowIdx); if (targetRow == null) throw new RuntimeException("未找到目标行(Name6)"); // 收集目标行下方需要追加的行数据(这里假设只处理下方第一行) List<List<Object>> belowRowData = new ArrayList<>(); int startBelowRow = targetRowIdx + 1; int endBelowRow = targetRowIdx + 1; for (int rowIdx = startBelowRow; rowIdx <= endBelowRow; rowIdx++) { Row row = targetSheet.getRow(rowIdx); if (row == null) continue; List<Object> rowValues = new ArrayList<>(); // 遍历该行所有列,处理合并单元格 for (int colIdx = 0; colIdx < row.getLastCellNum(); colIdx++) { Cell cell = row.getCell(colIdx, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); rowValues.add(getMergedCellValue(targetSheet, cell)); } belowRowData.add(rowValues); } // 将下方行数据追加到目标行 int lastColOfTarget = targetRow.getLastCellNum(); for (List<Object> rowValues : belowRowData) { for (int colIdx = 0; colIdx < rowValues.size(); colIdx++) { Object value = rowValues.get(colIdx); if (colIdx == 5) { // Name6对应的列索引为5,追加新列存储admin/loc Cell newCell = targetRow.createCell(lastColOfTarget); setCellValue(newCell, "Name6" + value.toString()); // 设置表头名称 lastColOfTarget++; } else if (colIdx > 5) { // Name7及以后的列,直接后移追加 Cell newCell = targetRow.createCell(lastColOfTarget); setCellValue(newCell, value); lastColOfTarget++; } } } // 从后往前删除下方行,避免索引错乱 for (int rowIdx = endBelowRow; rowIdx >= startBelowRow; rowIdx--) { targetSheet.removeRow(targetSheet.getRow(rowIdx)); } // 调整表头的合并区域(示例:假设表头在第0行,Name6的合并区域需要扩展) adjustHeaderMergedRegions(targetSheet); // 保存输出文件 try (FileOutputStream fos = new FileOutputStream("output_appended.xlsx")) { workbook.write(fos); } } } // 处理合并单元格,获取实际值 private static Object getMergedCellValue(Sheet sheet, Cell cell) { // 检查当前单元格是否属于合并区域 for (CellRangeAddress mergedRegion : sheet.getMergedRegions()) { if (mergedRegion.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { // 获取合并区域的左上角主单元格,读取其值 Cell mainCell = sheet.getRow(mergedRegion.getFirstRow()).getCell(mergedRegion.getFirstColumn()); return getCellValue(mainCell); } } // 非合并单元格,直接读取值 return getCellValue(cell); } // 调整表头的合并区域(根据你的实际表头结构修改) private static void adjustHeaderMergedRegions(Sheet sheet) { for (int i = sheet.getNumMergedRegions() - 1; i >= 0; i--) { CellRangeAddress mergedRegion = sheet.getMergedRegion(i); // 假设Name6表头在第0行第5列,合并区域需要扩展到新追加的列 if (mergedRegion.getLastRow() == 0 && mergedRegion.getFirstColumn() == 5) { sheet.removeMergedRegion(i); sheet.addMergedRegion(new CellRangeAddress( mergedRegion.getFirstRow(), mergedRegion.getLastRow(), mergedRegion.getFirstColumn(), mergedRegion.getFirstColumn() + 1 // 扩展1列 )); } } } // 复用之前的getCellValue方法 private static Object getCellValue(Cell cell) { return switch (cell.getCellType()) { case STRING -> cell.getStringCellValue(); case NUMERIC -> DateUtil.isCellDateFormatted(cell) ? cell.getDateCellValue() : cell.getNumericCellValue(); case BOOLEAN -> cell.getBooleanCellValue(); case FORMULA -> cell.getCellFormula(); default -> ""; }; } // 复用之前的setCellValue方法 private static void setCellValue(Cell cell, Object value) { if (value == null || value.toString().isEmpty()) { cell.setCellValue(""); return; } if (value instanceof String str) cell.setCellValue(str); else if (value instanceof Double num) cell.setCellValue(num); else if (value instanceof Boolean bool) cell.setCellValue(bool); else if (value instanceof java.util.Date date) cell.setCellValue(date); } }
关键注意点
getMergedCellValue方法是核心:通过检查单元格是否在合并区域内,获取主单元格的值,彻底解决"Part of a merged cell"的问题。- 删除行必须从后往前操作:因为POI删除行后,后续行会自动上移,从前往后删会导致索引错位,漏删或删错行。
- 表头合并区域的调整需要根据你实际的表格结构修改代码中的判断条件,示例仅作参考。
内容的提问来源于stack exchange,提问作者Jeet




