您的位置:首页 > 其它

poi sax 方式 导入excel海量数据

2014-04-29 21:31 162 查看
这几天做excel导入数据.刚开始使用jxl导入03版的,因为数据量超过65536,03版装不下,所以就用poi来支持07版的.

直接网上copy了个代码过去,搞了2条数据测试下成功了.

结果在导入正式数据的时候,才20M的excel文件就内存溢出了.网上搜了下,这情况很常见,需要使用sax方式来解析才可以.

但是在使用中,历经磨难,所以记录下来,以增强记忆.

还有好几个问题没解决,也一起记录下来,看哪位帮忙解决.

代码总共三段. 1.解析.2.抽象接口.3.业务类

1.解析类,继承DefaultHandler

/**
* 抽象Excel2007读取器,excel2007的底层数据结构是xml文件,采用SAX的事件驱动的方法解析
* xml,需要继承DefaultHandler,在遇到文件内容时,事件会触发,这种做法可以大大降低
* 内存的耗费,特别使用于大数据量的文件。
*
*/
public class Excel2007Reader extends DefaultHandler {
//共享字符串表
private SharedStringsTable sst;
//上一次的内容
private String lastContents;
private boolean nextIsString;

private int sheetIndex = -1;
private List<String> rowlist = new ArrayList<String>();
//当前行
private int curRow = 0;
//当前列
private int curCol = 0;

// 当前遍历的Excel单元格列索引
protected int thisColumnIndex = -1;

private String defaultStr = "";
private boolean listIsNull = true;

protected int total = 0;  //总行数
private int totalCol = 0;//总列数

private String sheetName;

private IRowReader rowReader;
public void setRowReader(IRowReader rowReader){
this.rowReader = rowReader;
}

/**
* 根据sheetid 解析sheet
* @param stream
* @param sheetId
* @throws Exception
*/
public void processOneSheetByIndex(String filename,int sheetId) throws Exception {
OPCPackage pkg = OPCPackage.open(filename);
XSSFReader r = new XSSFReader(pkg);
SharedStringsTable sst = r.getSharedStringsTable();
XMLReader parser = fetchSheetParser(sst);

// 根据 rId# 查找sheet
InputStream sheet2 = r.getSheet("rId"+sheetId);
sheetIndex++;
InputSource sheetSource = new InputSource(sheet2);
parser.parse(sheetSource);
sheet2.close();
}

/**
* 根据sheet名称,解析sheet
* @param stream
* @param name
* @throws Exception
*/
public void process(InputStream stream) throws Exception {
OPCPackage pkg = OPCPackage.open(stream);
XSSFReader r = new XSSFReader(pkg);
SharedStringsTable sst = r.getSharedStringsTable();
XMLReader parser = fetchSheetParser(sst);
XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) r.getSheetsData();
while (iter.hasNext()) {
InputStream ist = iter.next();
sheetName = iter.getSheetName();
InputSource sheetSource = new InputSource(ist);
parser.parse(sheetSource);
ist.close();
}
}

public XMLReader fetchSheetParser(SharedStringsTable sst)
throws SAXException {
XMLReader parser = XMLReaderFactory .createXMLReader("org.apache.xerces.parsers.SAXParser");
this.sst = sst;
parser.setContentHandler(this);
return parser;
}

public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {

// c => 单元格
if ("c".equals(name)) {
// 如果下一个元素是 SST 的索引,则将nextIsString标记为true
String cellType = attributes.getValue("t");
if ("s".equals(cellType)) {
nextIsString = true;
} else {
nextIsString = false;
}

String r = attributes.getValue("r");
int firstDigit = -1;
for (int c = 0; c < r.length(); ++c) {
if (Character.isDigit(r.charAt(c))) {
firstDigit = c;
break;
}
}
thisColumnIndex = nameToColumn(r.substring(0, firstDigit)); ;
} else if (name.equals("row")) {
// 设置行号
if(listIsNull){
totalCol = getColumns(attributes.getValue("spans"));
listIsNull = false;
}
}else if (name.equals("dimension")){
//获得总计录数
String d = attributes.getValue("ref");
total = getNumber(d.substring(d.indexOf(":")+1,d.length()));
}

// 置空
lastContents = "";
}

public void endElement(String uri, String localName, String name)
throws SAXException {
// 根据SST的索引值的到单元格的真正要存储的字符串
// 这时characters()方法可能会被调用多次
if (nextIsString) {
try {
int idx = Integer.parseInt(lastContents);
lastContents = new XSSFRichTextString(sst.getEntryAt(idx))
.toString();
} catch (Exception e) {
//				e.printStackTrace();
}
}
if ("v".equals(name)) {
paddingNullCell();
String value = lastContents.trim();
value = value.equals("")?" ":value;

rowlist.add(curCol, value);
curCol++;
}else {
//如果标签名称为 row ,这说明已到行尾,调用 optRows() 方法
if (name.equals("row")) {
//对最后的空列做填充
for(int i=rowlist.size();i<totalCol;i++){
rowlist.add(i,defaultStr);
}
//				optRows(sheetIndex,curRow,rowlist,total);
try {
rowReader.getRows(sheetIndex,curRow,rowlist,total,sheetName);
} catch (UIException e) {
throw new SAXException(e.getMessage());
}
rowlist.clear();
curRow++;
curCol = 0;
thisColumnIndex = 0;
}
}
}

private static int getNumber(String column) {
String c = column.toUpperCase().replaceAll("[A-Z]", "");
return Integer.parseInt(c);
}

public void characters(char[] ch, int start, int length)
throws SAXException {
//得到单元格内容的值
lastContents += new String(ch, start, length);
}

private static int getColumns(String spans) {
String number = spans.substring(spans.lastIndexOf(':') + 1,
spans.length());
return Integer.parseInt(number);
}

/**
* 空的单元个填充
*/
private void paddingNullCell() {
int index = curCol;
if(thisColumnIndex > index){
for(int i = index; i < thisColumnIndex; i++){
rowlist.add(curCol, "");
curCol++;
}
}
}
/**
* 从列名转换为列索引
* @param name
* @return
*/
private static int nameToColumn(String name) {
int column = -1;
for (int i = 0; i < name.length(); ++i) {
int c = name.charAt(i);
column = (column + 1) * 26 + c - 'A';
}
return column;
}

@SuppressWarnings("unused")
private void optRows(int sheetIndex,int curRow, List<String> rowlist, int total2) {
for (int i = 0; i < rowlist.size(); i++) {
System.out.print("'" + rowlist.get(i) + "',");
}
System.out.println("");
}

public static void main(String[] args) throws Exception {
long begintime,endtime;
Excel2007Reader howto = new Excel2007Reader();
howto.processOneSheetByIndex("E:/2012.xlsx",1);
}
}


2.接口

public interface IRowReader {
public  void getRows(int sheetIndex,int curRow, List<String> rowlist, int total, String sheetName) throws UIException;
}


3.业务类

public class ContractTempManagerImpl implements IRowReader {

//判断是07版则执行07版解析
public void importContract(UploadFile uploadFile) throws Exception{
String fileName = uploadFile.getFile().getOriginalFilename();
num=1;
long begintime,endtime;
//执行插入,1000条提交一次
begintime = new Date().getTime();
String type = fileName.substring(fileName.lastIndexOf('.')+1, fileName.length());
if(StringUtil.isNotBlank(type)&&type.equals("xlsx")){
Excel2007Reader rea = new Excel2007Reader();
rea.setRowReader(this);
rea.process(uploadFile.getFile().getInputStream());
}else{
//03版处理
}
endtime = new Date().getTime();
System.out.println("插入所需时间为 : " + (endtime - begintime)/1000 + "秒!");

}

/**
* 实现接口
* @param sheetIndex
* @param curRow	当前行
* @param rowlist	行结果
* @param total	总行数()
* @param sheetName	sheet名称
* @throws UIException
*/
public void getRows(int sheetIndex, int curRow, List<String> rowlist,int total,String sheetName) throws UIException {
if(sheetName.equals("合同信息")){
this.saveContractTemp(sheetIndex,curRow,rowlist,total,sheetName);
}else if(sheetName.equals("合同对方")){
this.saveContractProviderTemp(sheetIndex,curRow,rowlist,total,sheetName);
}else{
throw new UIException("sheet名称有误,请核对!");
}
}

}


代码说明

1.解析里面的,大部分都是网上copy的,很多都不太懂.主要是要继承DefaultHandler, 和重写startElement,endElement.

然后在 endElement中调用业务类的操作.

这里说下我遇到的问题和解决方法及没有解决的:

1. 多个模版,每个模版多个sheet,解析类怎么公用.

解决: 在解析类中用了个接口的属性rowReader,endElement()调用rowReader.getRows(),然后业务类实现这个接口,就解决了多个模版复用代码的问题.

然后在getRows()中将sheet名称作为参数传递,则在同个业务类中根据sheet不同执行不同操作也解决.

实践中才更能理解面向接口编程的意义.

2.根据sheet名称解析指定sheet

解决: 网上没找到根据sheet名称解析的,都是根据id和全部解析.所以就用了全部解析,然后拿到sheet名称.

3.获取sheet的总行数.

解决:因为是用的批处理,业务类的list每2000条提交一次,最后一批可能不到2000,所以需要总行数用来判断是否最后一批,

这个很简单.代码中dimension类型,getNumber()方法.(这个方法没看太懂,不过无所谓了.)

4.空值的处理

解决:如果excel中某列为空,那么解析时候是读不到的,因为xml中根本就没有这列.(将excel后缀名修改为.zip,即可看xml文件)

网上copy了个方法paddingNullCell(). 应该是根据当前列号和上个列号做比较,来填充.

5.最后一列空值的处理

解决:上面说网上copy的方法解决了中间的空列,但是最后一列空的话却没有解决,所以我就自己写了个.

在startElement()中,第一行的时候,获得总列数(第一行是标题,以标题列数位置)

在endElement()中,行结束前,先看list的size是否是总列数,如果不是则补充

6.数字 3995.992, 读取到为 3995.9920000000002

未解决: 这个在编辑栏看到也是3995.992,并且设置很多小数位后,还是3995.9920000000000 ,没有出现2

在修改为zip查看xml文件中,xml显示为3995.9920000000002

很莫名其妙,设置格式为文本,并且双击然后让单元格前面出现文本标识那样,则xml中正常.

网上找了很多,也没找到解决办法. 例如截取,正则什么的, 都不适用.

最后只有用最笨的办法,手动一列一列的 分列为文本格式.


解析xml不是很懂,晕晕乎乎的就拿来用了.一定要好好补习基础.

OK.就这么多了.第一次写博客,希望以后能坚持.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: