1. 程式人生 > >SAX解析excel,避免oom

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);
    }
}