POI读取合并单元格内容
POI 读取 Excel 合并单元格内容:完整解决方案
在处理包含合并单元格的 Excel 时,直接读取非左上角的单元格会返回空值(因为合并单元格仅左上角单元格保存数据)。本文将详细讲解如何识别合并单元格、读取合并区域的内容,并提供通用工具类,确保在各种合并场景下准确获取数据。
合并单元格的特性与读取难点
合并单元格的存储规则
Excel 中合并单元格(如 CellRangeAddress 定义的区域)仅在左上角第一个单元格中存储数据,其他被合并的单元格均为空。例如:
- 合并
A1:A3(3 行 1 列)后,仅A1有值,A2、A3为空; - 合并
B2:D4(3 行 3 列)后,仅B2有值,B3、C2等均为空。
直接读取的问题
若直接通过 row.getCell(column) 读取合并区域内的非左上角单元格,会返回 null 或空值,导致数据丢失。因此,需要:
- 先判断单元格是否属于合并区域;
- 若属于,则读取合并区域左上角单元格的值。
核心工具类实现
以下工具类提供合并单元格的判断、内容读取等功能,兼容 .xls 和 .xlsx 格式。
工具类代码
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
public class MergedCellReader {
/**
* 判断单元格是否属于合并区域
* @param sheet 工作表
* @param rowIndex 行索引(0 开始)
* @param colIndex 列索引(0 开始)
* @return 是合并区域返回 true,否则 false
*/
public static boolean isMergedCell(Sheet sheet, int rowIndex, int colIndex) {
// 获取工作表中所有合并区域
int mergedRegionCount = sheet.getNumMergedRegions();
for (int i = 0; i < mergedRegionCount; i++) {
CellRangeAddress region = sheet.getMergedRegion(i);
// 判断当前单元格是否在合并区域内
boolean isInRowRange = rowIndex >= region.getFirstRow() && rowIndex <= region.getLastRow();
boolean isInColRange = colIndex >= region.getFirstColumn() && colIndex <= region.getLastColumn();
if (isInRowRange && isInColRange) {
return true;
}
}
return false;
}
/**
* 读取合并区域的内容(返回左上角单元格的值)
* @param sheet 工作表
* @param rowIndex 行索引
* @param colIndex 列索引
* @return 合并区域的内容,非合并区域返回 null
*/
public static String getMergedCellValue(Sheet sheet, int rowIndex, int colIndex) {
int mergedRegionCount = sheet.getNumMergedRegions();
for (int i = 0; i < mergedRegionCount; i++) {
CellRangeAddress region = sheet.getMergedRegion(i);
// 判断当前单元格是否在合并区域内
if (rowIndex >= region.getFirstRow() && rowIndex <= region.getLastRow()
&& colIndex >= region.getFirstColumn() && colIndex <= region.getLastColumn()) {
// 获取合并区域左上角单元格
Row firstRow = sheet.getRow(region.getFirstRow());
if (firstRow == null) {
return ""; // 合并区域左上角行不存在
}
Cell firstCell = firstRow.getCell(region.getFirstColumn(), Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
// 转换单元格值为字符串(处理不同数据类型)
return getCellValue(firstCell);
}
}
return null; // 非合并区域
}
/**
* 将单元格值转换为字符串(兼容各种数据类型)
* @param cell 单元格
* @return 单元格内容的字符串形式
*/
private static String getCellValue(Cell cell) {
if (cell == null) {
return "";
}
CellType cellType = cell.getCellType();
switch (cellType) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
// 处理日期和数字
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
// 避免数字自动转为科学计数法
return String.valueOf(cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
// 公式单元格:获取计算结果
return getCellValue(cell.getSheet().getWorkbook().getCreationHelper().createFormulaEvaluator().evaluateInCell(cell));
default:
return "";
}
}
/**
* 读取单元格内容(自动处理合并区域)
* @param sheet 工作表
* @param rowIndex 行索引
* @param colIndex 列索引
* @return 单元格内容(合并区域返回左上角值,普通单元格返回自身值)
*/
public static String readCellValue(Sheet sheet, int rowIndex, int colIndex) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
return "";
}
// 判断是否为合并单元格
if (isMergedCell(sheet, rowIndex, colIndex)) {
return getMergedCellValue(sheet, rowIndex, colIndex);
} else {
// 普通单元格:直接读取
Cell cell = row.getCell(colIndex, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
return getCellValue(cell);
}
}
}
工具类核心功能
isMergedCell:判断单元格是否属于合并区域;getMergedCellValue:获取合并区域的内容(仅对合并单元格有效);readCellValue:通用读取方法,自动判断是否为合并单元格并返回正确内容;getCellValue:处理单元格的各种数据类型(字符串、数字、日期、公式等),统一转为字符串。
使用示例
以下示例读取包含合并单元格的 Excel,并打印所有单元格内容:
import org.apache.poi.ss.usermodel.*;
import java.io.FileInputStream;
import java.io.IOException;
public class ReadMergedCellDemo {
public static void main(String[] args) {
String filePath = "合并单元格示例.xlsx";
try (Workbook workbook = WorkbookFactory.create(new FileInputStream(filePath))) {
// 读取第一个工作表
Sheet sheet = workbook.getSheetAt(0);
// 遍历所有行(从第 0 行到最后一行)
for (int rowIndex = 0; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
continue;
}
// 遍历当前行的所有列(从第 0 列到最大列)
int lastColIndex = row.getLastCellNum();
for (int colIndex = 0; colIndex < lastColIndex; colIndex++) {
// 使用工具类读取单元格内容(自动处理合并区域)
String value = MergedCellReader.readCellValue(sheet, rowIndex, colIndex);
System.out.printf("行[%d], 列[%d]:%s\t", rowIndex, colIndex, value);
}
System.out.println(); // 换行
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
常见问题与优化技巧
1. 性能优化
- 问题:若工作表包含大量合并区域(如几百个),
isMergedCell循环判断可能耗时; - 优化:
- 缓存合并区域信息(如转为
Map存储行范围与列范围的映射); - 对于固定格式的 Excel,可提前记录合并区域位置,避免每次读取时遍历。
- 缓存合并区域信息(如转为
2. 合并区域嵌套问题
- 问题:Excel 支持合并区域嵌套(如大合并区域包含小合并区域);
- 解决:
CellRangeAddress的遍历顺序不影响结果,工具类会优先匹配最精确的合并区域(因嵌套区域的范围更小)。
3. 空行或空单元格处理
- 问题:工作表中可能存在空行(
Row为null)或空单元格; - 解决:
- 使用
Row.MissingCellPolicy.CREATE_NULL_AS_BLANK确保空单元格返回非null对象; - 遍历行时跳过
null行(或视为空行处理)。
- 使用
4. 公式单元格处理
- 问题:合并区域的左上角单元格若为公式,直接读取会返回公式字符串(如
SUM(A1:A3)); - 解决:工具类中
getCellValue方法通过FormulaEvaluator自动计算公式结果,返回最终值。