(翻译)Java使用POI中的SXSSF处理大数据量Excel文档
SXSSF位于org.apache.poi.xssf.streaming包中,在兼容XSSF的同时,能够应对大数据量和内存空间有限的情况。SXSSF每次获取的行数是在一个数值范围内,这个范围被称为“滑动窗口”,在这个窗口内的数据均存在于内存中,超出这个窗口大小时,数据会被写入磁盘,由此控制内存使用,相比较而言,XSSF则每次都是获取全部行。
这个“滑动窗口”的大小在定义SXSSF实例的时候,可以由构造函数中的参数指定,方法如下:
1 new SXSSFWorkbook(int windowSize)
SXSSFWorkbook.DEFAULT_WINDOW_SIZE是一个默认的窗口大小,其值为100。窗口大小为-1,表示不限制窗口大小,这里普及一下,excel 2003最多只允许存储65536条数据,excel2007以上版本可以支持1048576条数据,单个sheet表就支持近104万条数据了。如果窗口大小=-1,则只有手动调用flushRows()时,数据才会被写入磁盘。
createRow()可以创建一个新的行,此时内存中的"窗口"大小就增加,如果超出了窗口限制,索引值最小的行会先被“刷入”磁盘中,一旦某行数据被写入磁盘,则不能使用getRow()方法获取该数据。
需要注意,SXSSF会自动分配临时文件,这些临时文件需要我们手动清除,清除的方式是使用dispose()方法,例如:
SXSSFWorkbook wb = new SXSSFWorkbook(100); wb.dispose();
SXSSFWorkbook默认使用内联字符(inline strings),而不是共享字符(shared strings),两者的区别在于,如果两个单元格存储了相同的字符串,inline strings把它们当做两个字符串对待,每个单元格都保留此字符串的值,而shared strings则只在内存中保留一个值,单元格中只保留这个字符串的引用。
inline strings的好处是比较高效,因为不需要在内存中保留字符串的内容,坏处是有可能存在兼容性问题。而shared strings的好处是当文档中存在很多重复的字符内容时,该方式能节省空间,产生的文档也会相对更小,坏处是需要把所有字符串内容保存在内存中,这样插入新的字符数据的时候才能知道是否已经存在。因此需要根据内存的实际情况,判断使用哪种string。
除了字符之外,诸如合并单元格、超链接、批注等是直接存储在内存里面的,因此如果文档中大量使用这些特性,需要留意内存的空间。
下面这个例子将“窗口”大小设置为100,当行数达到101时,第0行的数据被写入磁盘,然后当行数=102时,第1行的数据被写入磁盘,以此类推:
import junit.framework.Assert; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.streaming.SXSSFWorkbook; public static void main(String[] args) throws Throwable { SXSSFWorkbook wb = new SXSSFWorkbook(100); // 在内存中保留100行数据,其余的写入磁盘 Sheet sh = wb.createSheet(); for(int rownum = 0; rownum < 1000; rownum++){ Row row = sh.createRow(rownum); for(int cellnum = 0; cellnum < 10; cellnum++){ Cell cell = row.createCell(cellnum); String address = new CellReference(cell).formatAsString(); cell.setCellValue(address); } } // 行号小于900的数据被写入磁盘,在内存中无法访问 for(int rownum = 0; rownum < 900; rownum++){ Assert.assertNull(sh.getRow(rownum)); } // 最后的100行数据仍然在内存中 for(int rownum = 900; rownum < 1000; rownum++){ Assert.assertNotNull(sh.getRow(rownum)); } FileOutputStream out = new FileOutputStream("/temp/sxssf.xlsx"); wb.write(out); out.close(); // 将此workbook对应的临时文件删除 wb.dispose(); }
第二个例子演示,在关闭自动写入磁盘的属性(即将窗口大小设置为-1)时,手动控制何时写入磁盘:
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.streaming.SXSSFWorkbook; public static void main(String[] args) throws Throwable { SXSSFWorkbook wb = new SXSSFWorkbook(-1); // 关闭自动写入磁盘的功能,所有数据将放在内存中 Sheet sh = wb.createSheet(); for(int rownum = 0; rownum < 1000; rownum++){ Row row = sh.createRow(rownum); for(int cellnum = 0; cellnum < 10; cellnum++){ Cell cell = row.createCell(cellnum); String address = new CellReference(cell).formatAsString(); cell.setCellValue(address); } // 手动控制写入磁盘的时机 if(rownum % 100 == 0) { ((SXSSFSheet)sh).flushRows(100); // 保留最后100行数据,将其余数据写入磁盘中 // ((SXSSFSheet)sh).flushRows() 相当于 ((SXSSFSheet)sh).flushRows(0),表示将所有数据写入磁盘 } } FileOutputStream out = new FileOutputStream("/temp/sxssf.xlsx"); wb.write(out); out.close(); // 删除临时文件 wb.dispose(); }