SAX解析excel,避免oom
poi官網的文件:
背景介紹:
今天看到最近有同事有個excel匯入oom的情況,然後使用easyExcel解決了,然後沒幾天,出現了活動金額匯入錯誤的情況,造成了資損,故分享這樣一篇文章。
去年12月份,做的專案是批量匯入商品釋出工作(後續有機會將整體設計分享出來),商品中有打欄位詳情內容,圖片內容。當時拿到需求,第一考慮到的就是會發生oom的情況,然後在內外搜尋了一下關於excel匯入的情況。發現內部有個easyExcel,當時看了一下這個的原始碼,及寫這個東西的開發,發現easyExcel並不是有一個基礎業務團隊同學在維護,只是一個業務團隊同學自己寫的。然後好奇的讀了一下原始碼,發現並不靠譜,其實也是基於SAX解析的。然後發現有觀察者模式各種在裡面,發現過於複雜,就果斷放棄使用(不僅我們沒用,其他很多業務團隊也是沒有使用的,用的人比較少。一個跟我比較熟的同學聊起,感覺也是說的不大靠譜。也不知道是怎麼就放出來開源了的)。然後自己根據官網給的例子,寫了一個自己解析的,自己根據業務進行一行一行拆解,最終不僅成功避免各種oom,程式碼量其實也跟官網給出來的那段差不多,維護起來也很輕鬆。
一、DOM解析是啥?
DOM解析,就是你去百度一下,95%以上(甚至更多)的excel讀取都是用的這種方式
1.好處,不用多說,簡單
2. 一目瞭然,所見即所得,要什麼直接可以get獲取到
所謂有力必有弊,弊端就是一個幾M的excel檔案,解析的結果將會佔用大概幾百M的記憶體,(如此高併發請求必有問題)。相關記憶體佔用問題可以見 (SAX解析excel與DOM解析excel佔用記憶體對比)。因為你每次要用什麼都可以直接get到,字型格式,及各個不同的sheet都可以隨意取
然後再百度一下,引起的oom事件就更多了。pio官方文件給出的oom解決辦法是建議使用SAX解析來讀取excel
二、SAX解析是什麼呢?
不知道大家是否瞭解SAX解析?
我們可以在window系統下,對一個excel解壓,應該是可以解壓成很多個xml的檔案。而xml格式,說到xml大家應該就不陌生了,就類似html標籤。
下面貼一下我這邊對excel解析出來的結果
<row> <c> A1 - <v> mysql file_info </v> </c> </row> <row> <c> A2 - <v> id </v> </c> <c> B2 - <v> BIGINT(11) </v> </c> </row> <row> <c> A3 - <v> file_url </v> </c> <c> B3 - <v> varchar(64) </v> </c> <c> C3 - <v> 匯入檔案的url,存七牛 </v> </c> </row> <row> <c> A4 - <v> batch_no </v> </c> <c> B4 - <v> varchar(64) </v> </c>
大概就是一些標籤,有開頭就有結尾。
那麼SAX解析記憶體佔用是多少呢。SAX解析是基於流來讀取檔案的,流是讀取,每次佔用記憶體就是非常小的了,如果能做好解析完的資料就直接處理掉,基本是不怎麼耗記憶體的
在上面這個裡面,我們可以清楚的看到有A3,A2這樣的,這個不就是excel中的座標位置麼?你要讀取的資料完全可以對改座標進行拆解讀取。(後面會放一個我這邊的例子)
三、對比
SAX是Simple API for XML的縮寫,它並不是由W3C官方所提出的標準,雖然如此,使用SAX的還是不少,幾乎所有的XML解析器都會支援它。
與DOM比較而言,SAX是一種輕量型的方法。我們知道,在處理DOM的時候,我們需要讀入整個的XML文件,然後在記憶體中建立DOM樹,生成DOM樹上的每個Node物件。當文件比較小的時候,這不會造成什麼問題,但是一旦文件大起來,處理DOM就會變得相當費時費力。特別是其對於記憶體的需求,也將是成倍的增長,以至於在某些應用中使用DOM是一件很不划算的事(比如在applet中)。
SAX在概念上與DOM完全不同。它不同於DOM的文件驅動,它是事件驅動的,它並不需要讀入整個文件,而文件的讀入過程也就是SAX的解析過程。所謂事件驅動,是指一種基於回撥(callback)機制的程式執行方法。
四、原始碼,寫的一個簡單例子,根據sheetNam獲取rid,然後讀取改sheetName的Sheet裡面的資料資訊,以及讀取所有sheet內容
裡面有些TODO, 需要自己的業務取處理的內容,如果需要看具體excel列印內容,可以開啟列印註釋,會將標籤打印出來。
已經貼出來的內容應該讀取是沒有大問題。如果有下來的情況,資料輸出可以全部列印一下,在看詳細的資料取值
SAX解析資料可能會丟失精度,需要保留一下有效數字
import org.apache.poi.hssf.util.CellReference;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.springframework.util.StringUtils;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class SaxToReadExcel {
//TODO 其他靜態變數請自定義
private static final String RID = "r:id";
/**
* 根據sheetname獲取rid資訊
* @param filename
* @param pamMap
* @throws Exception
*/
public void getSheetName(String filename, Map<String, Object> pamMap) throws Exception {
OPCPackage pkg = OPCPackage.open(filename);
XSSFReader r = new XSSFReader(pkg);
SharedStringsTable sst = r.getSharedStringsTable();
XMLReader parser = fetchSheetParser(sst, pamMap);
// To look up the Sheet Name / Sheet Order / rID,
// you need to process the core Workbook stream.
// Normally it's of the form rId# or rSheet#
// getWorkbookData()獲取的workbook資料
InputStream workbookData = r.getWorkbookData();
InputSource workbookSource = new InputSource(workbookData);
parser.parse(workbookSource);
workbookData.close();
}
/**
* 指定rid獲取sheet內的內容資訊
* @param filename
* @param pamMap
* @throws Exception
*/
public void processOneSheet(String filename, Map<String, Object> pamMap) throws Exception {
OPCPackage pkg = OPCPackage.open(filename);
XSSFReader r = new XSSFReader(pkg);
SharedStringsTable sst = r.getSharedStringsTable();
XMLReader parser = fetchSheetParser(sst, pamMap);
//one sheet
InputStream sheet2 = r.getSheet(pamMap.get(RID).toString());
InputSource sheetSource = new InputSource(sheet2);
parser.parse(sheetSource);
sheet2.close();
}
/**
* 執行所有的sheets資料
* @param filename
* @throws Exception
*/
public void processAllSheets(String filename) throws Exception {
OPCPackage pkg = OPCPackage.open(filename);
XSSFReader r = new XSSFReader( pkg );
SharedStringsTable sst = r.getSharedStringsTable();
XMLReader parser = fetchSheetParser(sst, null);
Iterator<InputStream> sheets = r.getSheetsData();
while(sheets.hasNext()) {
System.out.println("Processing new sheet:\n");
InputStream sheet = sheets.next();
InputSource sheetSource = new InputSource(sheet);
parser.parse(sheetSource);
sheet.close();
System.out.println("");
}
}
public XMLReader fetchSheetParser(SharedStringsTable sst, Map<String, Object> pamMap) throws SAXException {
XMLReader parser =
XMLReaderFactory.createXMLReader(
"org.apache.xerces.parsers.SAXParser"
);
ContentHandler handler = new SheetHandler(sst, pamMap);
parser.setContentHandler(handler);
return parser;
}
/**
* See org.xml.sax.helpers.DefaultHandler javadocs
*/
private static class SheetHandler extends DefaultHandler {
private SharedStringsTable sst;
private String lastContents;
private boolean nextIsString;
private Map<String, Object> pamMap;
private SheetHandler(SharedStringsTable sst, Map<String, Object> pamMap) {
this.pamMap = pamMap;
this.sst = sst;
}
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
// c => cell
// TODO 想看格式開啟此處
// System.out.print("<" + name + ">");
if (name.equals("c")) {
// Print the cell reference
//cellRef = A10
String cellRef = attributes.getValue("r");
CellReference cellReference = new CellReference(cellRef);
//col
short col = cellReference.getCol();
int row = cellReference.getRow();
//TODO 所有的資料資訊都可以個那就此處來進行識別處理。處理方式如輸出
//TODO 最好的處理方式是執行一行即可處理資料,所有的業務請根據此處的
//TODO cellRef = A10來識別行列處理
System.out.println("cellRef = " + cellRef + "; col = " + col
+ "; row = " + row + "; convertNumToColString = " + CellReference.convertNumToColString(col));
// Figure out if the value is an index in the SST
String cellType = attributes.getValue("t");
if (cellType != null && cellType.equals("s")) {
nextIsString = true;
} else {
nextIsString = false;
}
}
if(name.equals("sheet")){
if(pamMap != null){
String sheetName = (String) pamMap.get("sheetName");
if(!StringUtils.isEmpty(sheetName) && attributes.getValue("name").equals(sheetName)){
pamMap.put(RID, attributes.getValue(RID));
}
}
System.out.print(" name=" + attributes.getValue("name"));
System.out.print("; r:id=" + attributes.getValue("r:id"));
}
// Clear contents cache
lastContents = "";
}
public void endElement(String uri, String localName, String name)
throws SAXException {
// Process the last contents as required.
// Do now, as characters() may be called more than once
if (nextIsString) {
int idx = Integer.parseInt(lastContents);
lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
nextIsString = false;
}
// v => contents of a cell
// Output after we've seen the string contents
if (name.equals("v")) {
System.out.println(lastContents);
}
// TODO 想看格式開啟此處
// System.out.print("</" + name + ">");
}
/**
* 補充最後的資料處理,此步很重要
*/
public void endDocument(){
//TODO 此處為最後的檔案輸出地方
//TODO 如果此處不處理,可能會丟失最後的一行資料,如果是自己寫邏輯按照行處理的話
//TODO 最後一行一定要處理
}
public void characters(char[] ch, int start, int length)
throws SAXException {
lastContents += new String(ch, start, length);
}
}
public static void main(String[] args) throws Exception {
String filePath = "/Users/xxxx/Desktop/saxData.xlsx";
SaxToReadExcel example = new SaxToReadExcel();
Map<String, Object> pamMap = new HashMap<String, Object>();
pamMap.put("sheetName", "sheet3");
//根據sheetName 獲取指定的sheetid 資訊
example.getSheetName(filePath, pamMap);
String rid = (String) pamMap.get(RID);
Map<String, Object> pamMap2 = new HashMap<String, Object>();
pamMap2.put(RID, rid);
//執行指定的sheet
example.processOneSheet(filePath, pamMap2);
//執行所有的sheets
// example.processAllSheets(filePath);
}
}