POI读取合并单元格内容

POI 读取 Excel 合并单元格内容:完整解决方案

在处理包含合并单元格的 Excel 时,直接读取非左上角的单元格会返回空值(因为合并单元格仅左上角单元格保存数据)。本文将详细讲解如何识别合并单元格、读取合并区域的内容,并提供通用工具类,确保在各种合并场景下准确获取数据。

合并单元格的特性与读取难点

合并单元格的存储规则

Excel 中合并单元格(如 CellRangeAddress 定义的区域)仅在左上角第一个单元格中存储数据,其他被合并的单元格均为空。例如:

  • 合并 A1:A3(3 行 1 列)后,仅 A1 有值,A2A3 为空;
  • 合并 B2:D4(3 行 3 列)后,仅 B2 有值,B3C2 等均为空。

直接读取的问题

若直接通过 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. 空行或空单元格处理

  • 问题:工作表中可能存在空行(Rownull)或空单元格;
  • 解决
    • 使用 Row.MissingCellPolicy.CREATE_NULL_AS_BLANK 确保空单元格返回非 null 对象;
    • 遍历行时跳过 null 行(或视为空行处理)。

4. 公式单元格处理

  • 问题:合并区域的左上角单元格若为公式,直接读取会返回公式字符串(如 SUM(A1:A3));
  • 解决:工具类中 getCellValue 方法通过 FormulaEvaluator 自动计算公式结果,返回最终值。