Apache POI 是 Java 操作 Microsoft Office 文件的標準庫。讀取 Excel 分為 HSSF(.xls,BIFF8 格式)與 XSSF(.xlsx,OOXML 格式)兩套 API,另提供 SXSSF(流式寫入)與 XSSFEventFactory(流式讀取)解決大檔案記憶體問題。本文從基礎讀取到百萬行高效能處理全面解析。
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.3.0</version>
</dependency>
<!-- 大檔案流式處理可選 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>5.3.0</version>
</dependency>
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellReference;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
public class ExcelReader {
/** 讀取 Excel 回傳 List<List<String>>(每行一個 List) */
public static List> read(String filePath) throws Exception {
List> result = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(new File(filePath));
Workbook wb = WorkbookFactory.create(fis)) { // 自動判斷 .xls/.xlsx
Sheet sheet = wb.getSheetAt(0); // 讀取第一張工作表
int lastRowNum = sheet.getLastRowNum();
for (int i = 0; i <= lastRowNum; i++) {
Row row = sheet.getRow(i);
if (row == null) continue; // 空行跳過
List rowData = new ArrayList<>();
int lastCellNum = row.getLastCellNum();
for (int j = 0; j < lastCellNum; j++) {
Cell cell = row.getCell(j);
rowData.add(getCellValueAsString(cell));
}
result.add(rowData);
}
}
return result;
}
/** 將任意型別儲存格轉為字串 */
private static String getCellValueAsString(Cell cell) {
if (cell == null) return "";
return switch (cell.getCellType()) {
case STRING -> cell.getStringCellValue().trim();
case NUMERIC -> {
if (DateUtil.isCellDateFormatted(cell)) {
yield cell.getLocalDateTimeCellValue().toString(); // 日期格式
}
// 數值去除科學記號、尾隨 .0
yield new DataFormatter().formatCellValue(cell);
}
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
case FORMULA -> {
// 公式儲存格:嘗試取快取值,否則回傳公式字串
try {
yield getCellValueAsString(cell.getCachedFormulaResultType() != CellType.FORMULA
? cell : null); // 遞歸處理快取結果
} catch (Exception e) {
yield cell.getCellFormula();
}
}
case BLANK, _NONE, ERROR -> "";
default -> "";
};
}
}
當 Excel 超過 10 萬行,DOM 模式會導致 OOM。使用 SAX 事件驅動模式逐行處理,記憶體佔用恆定:
import org.apache.poi.xssf.eventusermodel.*;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
public class StreamingExcelReader extends DefaultHandler {
private SharedStringsTable sst;
private String lastContents;
private boolean nextIsString;
private int rowCount = 0;
public void process(String filePath) throws Exception {
try (OPCPackage pkg = OPCPackage.open(filePath)) {
XSSFReader reader = new XSSFReader(pkg);
sst = reader.getSharedStringsTable();
XMLReader parser = XMLReaderFactory.createXMLReader();
parser.setContentHandler(this);
parser.parse(new InputSource(reader.getSheetsData().next()));
}
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attrs) {
if ("c".equals(qName)) { // 儲存格
String cellType = attrs.getValue("t");
nextIsString = "s".equals(cellType); // 共享字串表
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if ("v".equals(qName)) { // 值
String value = nextIsString ? sst.getItemAt(Integer.parseInt(lastContents)).toString() : lastContents;
System.out.print(value + "\t");
nextIsString = false;
} else if ("row".equals(qName)) {
System.out.println();
rowCount++;
}
}
@Override
public void characters(char[] ch, int start, int length) {
lastContents = new String(ch, start, length);
}
}
| 問題 | 原因 | 解決 |
|---|---|---|
| 數值讀成科學記號 (1.23E7) | 預設格式化 | 用 DataFormatter 或 BigDecimal 處理 |
| 日期讀成數字 (44927) | Excel 日期存為序數 | DateUtil.isCellDateFormatted(cell) 判斷後轉換 |
| 公式儲存格回傳 null | 未計算公式 | 用 FormulaEvaluator 強制計算,或讀取快取值 |
| 大檔案 OOM | DOM 全載入記憶體 | 改用 SAX 流式讀取(上文範例) |
| .xls 格式不支援超過 65536 行 | BIFF8 格式限制 | 轉換為 .xlsx 或分多檔處理 |
wb.setForceFormulaRecalculation(false)new SXSSFWorkbook(100) 保留 100 行在記憶體,其餘刷入磁碟| 庫 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Apache POI | 功能最全、社群活躍、支援所有格式 | 記憶體佔用大、API 複雜 | 企業級專案、複雜報表 |
| EasyExcel (Alibaba) | 記憶體極低、API 簡單、註解驅動 | 不支援 .xls、樣式功能弱 | 大量資料匯入匯出、簡單報表 |
| jxl (JExcelApi) | 輕量、支援 .xls | 已停止維護、不支援 .xlsx | 舊專案維護 |
| Fastexcel | 極速寫入、低記憶體 | 唯寫、功能受限 | 高效能報表生成 |
Apache POI 雖然 API 複雜,但只要掌握 WorkbookFactory 統一入口、DataFormatter 格式化、SAX 流式讀取 三大核心,即可應對 90% 的 Excel 讀取需求。對於百萬行以上的大檔案,強烈建議改用 EasyExcel 或 SAX 模式,避免記憶體溢位風險。