專案需解析 20MB+、萬行級的 Excel 2007 (.xlsx) 檔案。POI 預設的 XSSFWorkbook 模式會將整份檔案載入記憶體,極易 OOM。改用 SAX 事件驅動模式(XSSFReader + ContentHandler)流式讀取,記憶體佔用極低。但 .xlsx 內部採用共享字串表與稀疏儲存:空儲存格在 XML 中完全不存在節點,直接導致列解析時列號跳躍、資料錯位。
關鍵邏輯在 endElement("v") 與 endElement("row") 中:
public class MyXSSFSheetHandler extends DefaultHandler {
// 關鍵狀態
private int lastColumnNumber = -1; // 上一個實際讀到的列索引
private int thisColumn = -1; // 當前儲存格列索引(由 r="A3" 解析)
private StringBuffer rowString = new StringBuffer(); // 整行 CSV 字串緩衝
@Override
public void startElement(String uri, String localName, String name, Attributes attrs) {
if ("c".equals(name)) { // 儲存格開始
String r = attrs.getValue("r"); // 如 "A3", "C5"
thisColumn = nameToColumn(r.substring(0, firstDigitIndex(r))); // 字母轉列號 0-based
// ... 型別/樣式解析略 ...
}
}
@Override
public void endElement(String uri, String localName, String name) {
if ("v".equals(name)) { // 儲存格值結束
String cellValue = parseCellValue(); // 依型別解析 thisStr
// === 核心補齊邏輯 ===
if (lastColumnNumber == -1) lastColumnNumber = 0;
for (int i = lastColumnNumber; i < thisColumn; i++) {
rowString.append(','); // 補齊中間空儲存格
}
rowString.append(cellValue); // 追加當前值
lastColumnNumber = thisColumn;
} else if ("row".equals(name)) { // 整行結束
// 補齊行尾空儲存格(若有 minColumns 限制)
if (minColumns > 0) {
for (int i = lastColumnNumber; i < minColumns; i++)
rowString.append(',');
}
// 以逗號分割得到完整欄位陣列/Map
Map<Integer, String> rowMap = rowReader.getRowMapByString(rowString.toString());
// 批次收集、落庫邏輯...
rowString.setLength(0); // 重置緩衝
lastColumnNumber = -1;
}
}
}
List.add() 丟失空值問題,CSV 字串天然保留空欄位順序。for (i=last; i<this; i++) 精確補齊 last+1 ... this-1 之間的空列。| 檔案大小 | 資料行數 | 耗時 | 記憶體峰值 |
|---|---|---|---|
| 20 MB | 10,000+ | ~2 分鐘 | < 100 MB |
| 40 MB | 20,000+ | ~4 分鐘 | < 150 MB |
List<Map> 入庫,減少資料庫交互。StylesTable、SharedStringsTable 單例複用,避免重複解析。org.apache.poi.xssf.eventusermodel.XSSFReader「SAX 解析 Excel 的本質是『流式掃描 XML』,空儲存格不存在節點是 XML 結構決定的,唯有『列號追蹤 + 字串緩衝』才能還原真實表格結構。」