Excel 合併單元格是報表導入的常見痛點:合併區域內只有左上角儲存格有值,其餘為空。本文提供基於 Apache POI 的完整工具類,實現 合併區域識別、任意儲存格取真實值、流式處理大文件 三大功能,代碼可直接投入生產使用。
| 類/方法 | 用途 |
|---|---|
Sheet.getMergedRegions() | 獲取所有合併區域列表 |
CellRangeAddress | 表示一個合併區域(首行、末行、首列、末列) |
sheet.getMergedRegion(i) | 獲取第 i 個合併區域 |
ca.getFirstRow() / getLastRow() / getFirstColumn() / getLastColumn() | 區域邊界 |
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.*;
public class ExcelMergeUtil {
/** 獲取 Sheet 所有合併區域 */
public static List getMergedRegions(Sheet sheet) {
List list = new ArrayList<>();
int count = sheet.getNumMergedRegions();
for (int i = 0; i < count; i++) {
list.add(sheet.getMergedRegion(i));
}
return list;
}
/**
* 判斷儲存格是否屬於合併區域,是則返回左上角儲存格的值
* @param mergedRegions 合併區域列表
* @param cell 待判斷儲存格
* @param sheet 所在 Sheet
* @return 真實值(字符串),非合併區域返回空串
*/
public static String getMergedCellValue(List mergedRegions,
Cell cell, Sheet sheet) {
int rowIdx = cell.getRowIndex();
int colIdx = cell.getColumnIndex();
for (CellRangeAddress ca : mergedRegions) {
if (rowIdx >= ca.getFirstRow() && rowIdx <= ca.getLastRow() &&
colIdx >= ca.getFirstColumn() && colIdx <= ca.getLastColumn()) {
// 命中合併區域,取左上角儲存格
Row firstRow = sheet.getRow(ca.getFirstRow());
if (firstRow == null) return "";
Cell firstCell = firstRow.getCell(ca.getFirstColumn());
return getCellStringValue(firstCell);
}
}
return ""; // 非合併區域
}
/** 通用儲存格取字符串值(含公式、數值、日期、布爾) */
public static String getCellStringValue(Cell cell) {
if (cell == null) return "";
switch (cell.getCellTypeEnum()) {
case STRING: return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell))
return cell.getDateCellValue().toString();
// 數值去科學計數法
return new java.text.DecimalFormat("#.##########").format(cell.getNumericCellValue());
case BOOLEAN: return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
try { return getCellStringValue(cell.getCachedFormulaResultTypeEnum() == CellType.NUMERIC
? cell : cell); } catch (Exception e) { return cell.getCellFormula(); }
case BLANK: return "";
default: return "";
}
}
/** 讀取行時自動填充合併值的示例 */
public static Map readRowAsMap(Row row, Sheet sheet,
List mergedRegions,
List headers) {
Map map = new LinkedHashMap<>();
for (int i = 0; i < headers.size(); i++) {
Cell cell = row.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
String val = getCellStringValue(cell);
if (val.isEmpty()) // 嘗試合併值
val = getMergedCellValue(mergedRegions, cell, sheet);
map.put(headers.get(i), val);
}
return map;
}
}
// 僅示例關鍵點,完整代碼需處理 SharedStringsTable
try (OPCPackage pkg = OPCPackage.open(file);
XSSFReader reader = new XSSFReader(pkg);
InputStream sheetStream = reader.getSheet("rId1")) {
XMLReader parser = XMLReaderFactory.createXMLReader();
SheetHandler handler = new SheetHandler(mergedRegions); // 自定義 SAX 處理器
parser.setContentHandler(handler);
parser.parse(new InputSource(sheetStream));
}
注意:流式讀取時合併區域信息在 sheet.xml 的 <mergeCells> 中,需在 SAX 解析階段預先收集。
| 坑點 | 現象 | 解決 |
|---|---|---|
| 合併區域重疊 | POI 允許但 Excel 不建議 | 按區域面積排序,優先匹配最小區域 |
| 左上角儲存格為空 | 合併後刪除內容 | 向上/向左尋找最近非空儲存格,或業務端規範模板 |
| 日期/數值格式化丟失 | 直接 toString() 得到科學計數法 | 用 DataFormatter 或 DecimalFormat |
| 大文件 OOM | XSSF 全量載入 | 改用 SAX 事件流或 SXSSF 寫入 |
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
sheet.getMergedRegions() 一次性獲取sheet.getRow(firstRow).getCell(firstCol)DataFormatter 處理所有類型將上述工具類封裝為 Spring Bean,配合模板驗證註解,即可構建健壯的 Excel 導入服務。