之前有一个处理10W条数据的excel文档,其大概有21列,若是采用普通的方式去读取一定会内存溢出,而去改变jvm的内存大小,当数据量达到100W条甚至1000W条时,所需的内存大小也不好计算。当时我在网上找到一篇博文,其采用的是SAX(Simple API for XML)的方式去解析excel,可以做到读取所有行的数据,但是它的缺点是会跳过为空值的单元格。当时我走了几遍代码,又查了一些资料,想到的方式就是获取单元格的坐标,以读取的前后两个单元格的坐标去判断是否跳过了单元格。比如如果读到的前一个单元格为D5,而紧接着读到的是D7,那么D6这个单元格一定就是空的,被跳过了,当然也会存在连续多个空的单元格的情况,所以计算出跳过了几个空的单元格,然后手动给这些空的单元格所对应的对象复制,并手动给索引加上数字,就可以完成解析为空值的单元格会跳过的缺憾了。
以下是我参考的博客的原文地址:https://blog.csdn.net/goodkuang2012/article/details/7350985
只需把改博客当中的所有类copy出去即可实现读取海量excel,我所修改的是Excel2007Reader这一个类。代码如下:
import java.io.InputStream; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import org.apache.poi.hssf.usermodel.HSSFDateUtil; 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.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; 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; //日期标志 private boolean dateFlag; //数字标志 private boolean numberFlag; //前一个单元格的xy private String preXy = ""; //当前单元格的xy private String currXy = ""; //前一个单元格的x private String preX = ""; //当前单元格的x private String currX = ""; //是否跳过了单元格 private boolean isSkipCeil = false; private boolean isTElement; //两个不为空的单元格之间隔了多少个空的单元格 private int flag = 0; private IRowReader rowReader; public void setRowReader(IRowReader rowReader){ this.rowReader = rowReader; } /**只遍历一个电子表格,其中sheetId为要遍历的sheet索引,从1开始,1-3 * @param filename * @param sheetId * @throws Exception */ public void processOneSheet(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# 或 rSheet# 查找sheet InputStream sheet2 = r.getSheet("rId"+sheetId); sheetIndex++; InputSource sheetSource = new InputSource(sheet2); parser.parse(sheetSource); sheet2.close(); } /** * 遍历工作簿中所有的电子表格 * @param filename * @throws Exception */ public void process(String filename) throws Exception { OPCPackage pkg = OPCPackage.open(filename); XSSFReader r = new XSSFReader(pkg); SharedStringsTable sst = r.getSharedStringsTable(); XMLReader parser = fetchSheetParser(sst); Iterator<InputStream> sheets = r.getSheetsData(); while (sheets.hasNext()) { curRow = 0; sheetIndex++; InputStream sheet = sheets.next(); InputSource sheetSource = new InputSource(sheet); parser.parse(sheetSource); sheet.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 cellDateType = attributes.getValue("s"); if ("1".equals(cellDateType)){ dateFlag = true; } else { dateFlag = false; } String cellNumberType = attributes.getValue("s"); if("2".equals(cellNumberType)){ numberFlag = true; } else { numberFlag = false; } //与判断空单元格有关 isSkipCeil = false; String cellXy = attributes.getValue("r"); if("".equals(preXy)) { preXy = cellXy; } currXy = cellXy; preX = preXy.replaceAll("\\d", "").trim(); currX = currXy.replaceAll("\\d", "").trim(); char pre; char curr; if(preX.length() == 2) { pre = preX.charAt(1); } else { pre = preX.charAt(0); } if(currX.length() == 2) { curr = currX.charAt(1); } else { curr = currX.charAt(0); } flag = curr - pre; if(flag != 0 && flag != 1 && flag > 0) { isSkipCeil = true; } preXy = cellXy; } //当元素为t时 if("t".equals(name)){ isTElement = true; } else { isTElement = false; } // 置空 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) { } } //t元素也包含字符串 if(isTElement){ String value = lastContents.trim(); rowlist.add(curCol, value); curCol++; isTElement = false; // v => 单元格的值,如果单元格是字符串则v标签的值为该字符串在SST中的索引 // 将单元格内容加入rowlist中,在这之前先去掉字符串前后的空白符 } else if ("v".equals(name)) { String value = lastContents.trim(); value = value.equals("")?" ":value; //日期格式处理 /*if(dateFlag){ Date date = HSSFDateUtil.getJavaDate(Double.valueOf(value)); SimpleDateFormat dateFormat = new SimpleDateFormat( "dd/MM/yyyy"); value = dateFormat.format(date); }*/ //数字类型处理 if(numberFlag){ BigDecimal bd = new BigDecimal(value); value = bd.setScale(3,BigDecimal.ROUND_UP).toString(); } //当某个单元格的数据为空时,其后边连续的单元格也可能为空 if(isSkipCeil == true) { for(int i = 0; i < (flag-1); i++) { rowlist.add(curCol + i, "***"); } curCol += (flag-1); } rowlist.add(curCol, value); curCol++; }else { //如果标签名称为 row ,这说明已到行尾,调用 optRows() 方法 if (name.equals("row")) { try { rowReader.getRows(sheetIndex,curRow,rowlist); } catch (Exception e) { e.printStackTrace(); } rowlist.clear(); curRow++; curCol = 0; } } } public void characters(char[] ch, int start, int length) throws SAXException { //得到单元格内容的值 lastContents += new String(ch, start, length); } }
该类中方法的思路就是先再startElement方法中获取单元格的类型,然后在endElement方法中获取单元格中的内容。