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解析出来的结果
A1 -
mysql file_info
A2 -
id
B2 -
BIGINT(11)
A3 -
file_url
B3 -
varchar(64)
C3 -
导入文件的url,存七牛
A4 -
batch_no
B4 -
varchar(64)
大概就是一些标签,有开头就有结尾。
那么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 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 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 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 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 pamMap;
private SheetHandler(SharedStringsTable sst, Map 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("");
}
/**
* 补充最后的数据处理,此步很重要
*/
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 pamMap = new HashMap();
pamMap.put("sheetName", "sheet3");
//根据sheetName 获取指定的sheetid 信息
example.getSheetName(filePath, pamMap);
String rid = (String) pamMap.get(RID);
Map pamMap2 = new HashMap();
pamMap2.put(RID, rid);
//执行指定的sheet
example.processOneSheet(filePath, pamMap2);
//执行所有的sheets
// example.processAllSheets(filePath);
}
}