POI为我们提供了很方便的文件解析功能,而且使用起来也非常方便。
对于简单的获取Excel全部内容的功能来说,根据POI官网和网上的代码,于是就有了下面的实现:
FileInputStream inputStream = null;
StringBuilder xlsFileContent = new StringBuilder();
try {
inputStream = new FileInputStream(file);
Workbook wb;
if (type.equals(FileTypeUtil.Type.XLS)) {
wb = new HSSFWorkbook(inputStream);
} else if (type.equals(FileTypeUtil.Type.XLSX)) {
wb = new XSSFWorkbook(inputStream);
} else {
return "";
}
int sheetCount = wb.getNumberOfSheets();
for (int i = 0; i < sheetCount; i++) {
....balabaa
}
}
上面的代码不是重点,所以,上面代码中不全的部分我没有给全。满心欢喜的写完了代码,运行发现没有问题。但是,很不幸的是,如果我们要解析的文件比较大,我试过一个50M的Excel文件,结果不出意外的报错了。OutOfMemoryError,没错,内存溢出,其实也没有什么以外的,就算是我们自己写代码解析最普遍的文本文件的时候,在解析大文件时不注意也会出现这个错误,更何况这个比文本文件更复杂的Excel文件。
在网上搜索了一下解决的方案,比较多的是给出的官方的例子代码,官方是给的一个实例代码将Excel的信息转换成CSV文件内容的格式来进行返回的。具体的网上的博客中的能不能实现我不是很清楚,因为我也没试, 因为我的需求是能够解析xls以及xlsx文件,可网上的代码毫无意外的全部都是引用官方的解析xlsx的代码,另外还有应用一个第三方库的,但是也是只能解析xlsx文件,这就很蛋疼了。
没办法只能再去官网碰一碰运气了,知道网上博客里的思路后,在官网的demo列表里面我发现了两个文件。XLSX2CSV, XLS2CSVmra。简答看一下代码,发现正是自己需要的。官方原来不仅给出了XLSX大文件的解析方案,也给出了LSX大文件的解析方案。
查看官方的代码,发现,它是把每一步解析后的信息通过System.out给打印出来了。
就像这样:
/**
* Creates a new XLS -> CSV converter
* @param filename The file to process
* @param minColumns The minimum number of columns to output, or -1 for no minimum
*/
public XLS2CSVmra(String filename, int minColumns) throws IOException, FileNotFoundException {
this(
new POIFSFileSystem(new FileInputStream(filename)),
System.out, minColumns
);
}
上面传给了一个System.out对象,在后面它用这个System.out对象去打印的Excel内容,就像下面的:
// If we got something to print out, do so
if(thisStr != null) {
if(thisColumn > 0) {
output.print(',');
}
output.print(thisStr);
}
那么我们就有了一个思路了,我们可以在官方的类里面添加一个StringBuilder对象,在所有使用output.print(“”)打印的地方,将Excel的内容信息用StringBuilder给接收起来,最后再返回这个StringBuilder就可以了。同理对于XLSX文件就是把官方处理XLSX文件的代码按照上面的思路改一下就可以了。
然而这个也不是重点。。。,所以我也不给代码了,这样做其实是没有问题的,我以这种方式实现后,发现至少读取50M的文件是没有一点问题的,而且估计更大一点的文件也是没有问题的,然而如果,比如说大数据情况下,一个Excel文件可能就要几百M甚至上千M,那么我们在代码里面使用StringBuilder去接受存储Excel中的信息,会有什么问题?,很简单同样会发生OOM,因为它同样吃内存。
那么为了彻底避免OOM,就需要再换一种解析方式。
既然出现OOM是因为内存中的数据太多,那么有什么方式可以让内存中的数据不多?这时候就要想到Java读取文本文件了。读取文本文件怎么防止OOM?很简单,多次读取,每次读取一部分就可以了。那么怎么把Excel文件和文本文件结合起来呢?
下面提供一种思路:
1. 上面已经知道,官方代码里面使用System.out来打印每读取一个单元格的数据的,那么我们将读取过程中的数据存到一个缓存文件里面,而不让它存在在内存里面,显然是可行的
2. 如果每读取一个单元格的话就要进行一次文件的写入,那样写入会太频繁,会严重影响性能,我们可以等积累到一定数量后再进行文件的写入
3. 对于写完的文件,就好办了,我们可以用Java自带的IO流分批次去读取数据,这样,无论多大的文件就都不会出现内存溢出问题了。
4. 当然,对于上面的读取方式,事实上如果我们想在程序里面使用这个数据,那只能是每取出一块数据处理一块数据,这样的处理方式就是为了方式OOM,如果非得想一次性读取完所有的数据,那我想,无论是采用哪种方式,OOM是不可避免的。
明白上面的思路后,我们的第一步就是修改官方的代码,下面以XLS文件为例:
上面已经给出了官方代码的连接了,这里再给一次:。XLSX2CSV(读取XLSX文件), XLS2CSVmra(读取XLS文件)。
那么第一步,我们要定义我们需要的东西:
1. 既然需要将读取过程中的的数据存到一个缓存文件里面,那么我们就得需要一个OutputStream,然后在官方代码里面所有用到PrintStream打印数据的地方要用这个OutputStream去进行数据的存储。
2. 上面说过,为了避免频繁的进行IO读写,我们要等读取的数据达到一定程度后才写缓存文件,因此需要定义两个变量,一个是StringBuilder用来存储读取的数据,一个是int类型的length,用来存储当数据长度达到多少的时候再进行写操作
因此,类中我们重新定义下面的属性:
public class XLS2CSVmra implements XLSFileReader, HSSFListener {
...
private OutputStream outputStream;
private StringBuilder writeResStr;
public XLS2CSVmra(POIFSFileSystem fs, OutputStream outputStream) {
this.writeResStr = new StringBuilder();
this.outputStream = outputStream;
...
}
...
}
第二步,我们应该定义一个方法,用于将程序运行过程中的数据存储到缓存文件里面,并且要在这个方法里面规定什么时候应该写文件:这里我规定了当数据量达到1M的时候才进行文件的写操作。
private void writeStr(String string) {
// 将要写进缓存文件的数据现在程序中缓存
writeResStr.append(string);
// 定义数据达到多少时进行写操作
int writeResLength = 1024 * 1024;
if (writeResStr.length() >= writeResLength) {
try {
if (outputStream != null) {
outputStream.write(writeResStr.toString().getBytes());
}
// 进行写操作后,清空程序中的数据
writeResStr.delete(0, writeResStr.length());
} catch (IOException e) {
e.printStackTrace();
}
}
}
另外,因为对数据大小什么时候写进行了限制,可能最后仍有一部分数据会存在writeResStr里面,最后的时候需要在把他里面的数据检查一遍,因此,我定义了一个close方法,用于做后续工作和资源的销毁,当然,为了使用方便,可以让类继承Closeable接口。
@Override
public void close() throws IOException {
if (outputStream != null) {
outputStream.write(writeResStr.toString().getBytes());
outputStream.flush();
}
writeResStr.delete(0, writeResStr.length());
fs.close();
}
第三步,就是把我们上面定义的方法,把官方代码中的output.println(..)全部替换成我们的writeStr(..),这样把解析文件的数据存储到缓存文件里面就完成了。
第四步,根据上面的几个步骤,将XLSX文件解析的也进行更改。
那么,对于改好的文件,我们怎么使用?
其实使用起来就是Java读取最简单的文本文件了。Java文本文件怎么分批次读取?
这个就简单了,我们定义一个变量,当读取的文件达到这个变量值的时候就返回一次性读取的数据。
下面是简单的代码:
为了使用方便,我定义了一个接口。以及一个返回数据的类。
/**
* 读取文档的解析类
* 在设置单次解析文本的大小后
* 可以调用readNext进行下一次的文本读取
*/
public interface ReadFileParser extends Closeable {
void setReadSize(int readSize);
ReadResult readNext();
}
public class ReadResult {
ReadResult(String content, boolean isEnd) {
this.content = content;
this.isEnd = isEnd;
}
private String content;// 读取的内容
private boolean isEnd;// 是否读到了文件结尾
public String getContent() {
return content;
}
public boolean isEnd() {
return isEnd;
}
}
下面就定义文本类文档解析的类
import cn.beimingkun.searchengine.Utils.FileUtils;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class ReadDefaultFileParser implements ReadFileParser {
private static Log log = LogFactory.getLog(ReadDefaultFileParser.class);
private int readSize = 1024 * 1024;
private FileInputStream fileInputStream;
private BufferedReader bufferedReader;
public ReadDefaultFileParser(String filePath) throws IOException {
this.fileInputStream = new FileInputStream(filePath);
this.bufferedReader = new BufferedReader(
new InputStreamReader(fileInputStream, FileUtils.resolveCode(filePath)),
10 * 1024 // Default Read Once Size is 1M.
);
}
@Override
public void setReadSize(int readSize) {
this.readSize = readSize;
}
@Override
public ReadResult readNext() {
StringBuilder builder = new StringBuilder();
boolean isEnd = false;
char[] c = new char[10 * 1024];
while (true) {
int length;
try {
length = bufferedReader.read(c);
if (length == -1) {
isEnd = true;
break;
} else {
builder.append(c, 0, length);
// 只读取设定大小的数据
if (builder.length() >= readSize) {
break;
}
}
} catch (IOException e) {
log.error(e);
}
}
return new ReadResult(builder.toString(), isEnd);
}
@Override
public void close() {
FileUtils.closeAllStream(bufferedReader, fileInputStream);
}
}
最后,我们首先使用修改过的官方代码将读取的Excel数据存到一个文本文件里面,然后使用上面定义的读取文本文件的类进行分批次读取。因此,下面也定义一个读取Excel数据的类,和读取文本文件的类实现同一个接口。
public class ReadXLSFileParser implements ReadFileParser {
private ReadFileParser parser;
private File tmpFile;
public ReadXLSFileParser(String filePath, String tmpFileDir) throws Exception {
// 预处理Tmp文件
FileTypeUtil.Type fileType = FileTypeUtil.getFileType(filePath);
XLSFileReader reader = null;
FileOutputStream fileOutputStream = null;
// 获取处理结果的中间缓存文件
tmpFile = new File(tmpFileDir,
FileUtils.getNameWithoutExtension(filePath)
+ String.valueOf(System.currentTimeMillis()).substring(5)
+ ".txt");
try {
if (FileUtils.createFile(tmpFile, true)) {
fileOutputStream = new FileOutputStream(tmpFile);
if (fileType.equals(FileTypeUtil.Type.XLS)) {
reader = new XLS2CSVmra(filePath, fileOutputStream);
} else if (fileType.equals(FileTypeUtil.Type.XLSX)) {
reader = new XLSX2CSV(filePath, fileOutputStream);
} else {
throw new FileSystemException("File Type not match:" + filePath);
}
reader.process();
} else {
throw new FileSystemException("Create TmpFile Error");
}
} finally {
FileUtils.closeAllStream(reader, fileOutputStream);
}
// 解析tmp文件,分批次读取
parser = new ReadDefaultFileParser(tmpFile.getPath());
}
@Override
public void setReadSize(int readSize) {
parser.setReadSize(readSize);
}
@Override
public ReadResult readNext() {
return parser.readNext();
}
@Override
public void close() throws IOException {
parser.close();
//noinspection ResultOfMethodCallIgnored
tmpFile.delete();
}
}
上面的代码基本上分为这几个步骤;
1. 使用修改过的官方代码将获取Excel文件内容,并保存到本地文本文件
2. 根据这个保存后的文本文件创建读取文本文件的读取类
3. 每次调用ReadNext的时候其实读取的就是本地的文本文件
4. 释放资源的时候就把本地文件给删除掉。
基本上的代码就是这样了,代码比较多,也没有办法一一说明,只好将主要思想表达一下了,查看具体的代码逻辑的话,可以下载代码来进行查看。
https://download.csdn.net/download/wangpengfei_p/10629834