SAX解析excel,避免oom

poi官网的文档:

https://poi.apache.org/components/spreadsheet/how-to.html#xssf_sax_api

背景介绍:

       今天看到最近有同事有个excel导入oom的情况,然后使用easyExcel解决了,然后没几天,出现了活动金额导入错误的情况,造成了资损,故分享这样一篇文章。

       去年12月份,做的项目是批量导入商品发布工作(后续有机会将整体设计分享出来),商品中有打字段详情内容,图片内容。当时拿到需求,第一考虑到的就是会发生oom的情况,然后在内外搜索了一下关于excel导入的情况。发现内部有个easyExcel,当时看了一下这个的源码,及写这个东西的开发,发现easyExcel并不是有一个基础业务团队同学在维护,只是一个业务团队同学自己写的。然后好奇的读了一下源码,发现并不靠谱,其实也是基于SAX解析的。然后发现有观察者模式各种在里面,发现过于复杂,就果断放弃使用(不仅我们没用,其他很多业务团队也是没有使用的,用的人比较少。一个跟我比较熟的同学聊起,感觉也是说的不大靠谱。也不知道是怎么就放出来开源了的)。然后自己根据官网给的例子,写了一个自己解析的,自己根据业务进行一行一行拆解,最终不仅成功避免各种oom,代码量其实也跟官网给出来的那段差不多,维护起来也很轻松。

另外,如果还是比较懒,想用easyExcel(https://github.com/alibaba/easyexcel),也行,多帮忙实践一下,多暴漏问题。

一、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)机制的程序运行方法。

官方给出来的对比:https://poi.apache.org/components/spreadsheet/index.html

四、源码,写的一个简单例子,根据sheetNam获取rid,然后读取改sheetName的Sheet里面的数据信息,以及读取所有sheet内容

里面有些TODO, 需要自己的业务取处理的内容,如果需要看具体excel打印内容,可以打开打印注释,会将标签打印出来。

已经贴出来的内容应该读取是没有大问题。如果有下来的情况,数据输出可以全部打印一下,在看详细的数据取值

SAX解析数据可能会丢失精度,需要保留一下有效数字

另外贴一下excel相关的schemas:http://www.datypic.com/sc/ooxml/ss.html

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

猜你喜欢

转载自blog.csdn.net/weixin_42330218/article/details/81368034