`
cats_tiger
  • 浏览: 273961 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

解决大批量数据导出Excel产生内存溢出的方案

阅读更多
POI或者JXL在导出大量数据的时候,由于它们将每一个单元格生都成一个Cell对象,所以很容易导致内存溢出。解决这个问题,唯一的办法是弄清楚Excel的二进制格式(汗),并且用流的方式读写Excel。POI和JXL其实提供了二进制方式读写Excel的API,只是因为缺少文档和实例,所以使用的人不多。我编写了这个简单的合并Excel的类,它只适合合并结构相同的多个Excel文件。好在这个功能已经可以解决数据导出产生OOM的问题:将数据分批导出然后合并。
下面的代码使用POI3.1,合并11个3000多行的文档用时约6秒,我实在找不到更多的测试用的文档了。
@SuppressWarnings("unchecked")
public class XlsMergeUtil {
  private static Logger logger = LoggerFactory.getLogger(XlsMergeUtil.class);

  /**
   * 将多个Xls文件合并为一个,适用于只有一个sheet,并且格式相同的文档
   * @param inputs 输入的Xls文件
   * @param out 输出文件
   */
  public static void merge(InputStream[] inputs, OutputStream out) {
    if (inputs == null || inputs.length <= 1) {
      throw new IllegalArgumentException("没有传入输入流数组,或只有一个输入流.");
    }

    List<Record> rootRecords = getRecords(inputs[0]);
    Workbook workbook = Workbook.createWorkbook(rootRecords);
    List<Sheet> sheets = getSheets(workbook, rootRecords);
    if(sheets == null || sheets.size() == 0) {
      throw new IllegalArgumentException("第一篇文档的格式错误,必须有至少一个sheet");
    }
    //以第一篇文档的最后一个sheet为根,以后的数据都追加在这个sheet后面
    Sheet rootSheet = sheets.get(sheets.size() - 1); 
    int rootRows = getRowsOfSheet(rootSheet); //记录第一篇文档的行数,以后的行数在此基础上增加
    rootSheet.setLoc(rootSheet.getDimsLoc());
    Map<Integer, Integer> map = new HashMap(10000);

    for (int i = 1; i < inputs.length; i++) { //从第二篇开始遍历
      List<Record> records = getRecords(inputs[i]);
      int rowsOfCurXls = 0;
      //遍历当前文档的每一个record
      for (Iterator itr = records.iterator(); itr.hasNext();) {
        Record record = (Record) itr.next();
        if (record.getSid() == RowRecord.sid) { //如果是RowRecord
          RowRecord rowRecord = (RowRecord) record;
          //调整行号
          rowRecord.setRowNumber(rootRows + rowRecord.getRowNumber());
          rootSheet.addRow(rowRecord); //追加Row
          rowsOfCurXls++; //记录当前文档的行数
        }
        //SST记录,SST保存xls文件中唯一的String,各个String都是对应着SST记录的索引
        else if (record.getSid() == SSTRecord.sid) {
          SSTRecord sstRecord = (SSTRecord) record;
          for (int j = 0; j < sstRecord.getNumUniqueStrings(); j++) {
            int index = workbook.addSSTString(sstRecord.getString(j));
            //记录原来的索引和现在的索引的对应关系
            map.put(Integer.valueOf(j), Integer.valueOf(index));
          }
        } else if (record.getSid() == LabelSSTRecord.sid) {
          LabelSSTRecord label = (LabelSSTRecord) record;
          //调整SST索引的对应关系
          label.setSSTIndex(map.get(Integer.valueOf(label.getSSTIndex())));
        }
        //追加ValueCell
        if (record instanceof CellValueRecordInterface) {
          CellValueRecordInterface cell = (CellValueRecordInterface) record;
          int cellRow = cell.getRow() + rootRows;
          cell.setRow(cellRow);
          rootSheet.addValueRecord(cellRow, cell);
        }
      }
      rootRows += rowsOfCurXls;
    }
    byte[] data = getBytes(workbook, sheets.toArray(new Sheet[0]));
    write(out, data);
  }

  static void write(OutputStream out, byte[] data) {
    POIFSFileSystem fs = new POIFSFileSystem();
    // Write out the Workbook stream
    try {
      fs.createDocument(new ByteArrayInputStream(data), "Workbook");
      fs.writeFilesystem(out);
      out.flush();
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        out.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  static List<Sheet> getSheets(Workbook workbook, List records) {
    int recOffset = workbook.getNumRecords();
    int sheetNum = 0;

    // convert all LabelRecord records to LabelSSTRecord
    convertLabelRecords(records, recOffset, workbook);
    List<Sheet> sheets = new ArrayList();
    while (recOffset < records.size()) {
      Sheet sh = Sheet.createSheet(records, sheetNum++, recOffset);

      recOffset = sh.getEofLoc() + 1;
      if (recOffset == 1) {
        break;
      }
      sheets.add(sh);
    }
    return sheets;
  }

  static int getRows(List<Record> records) {
    int row = 0;
    for (Iterator itr = records.iterator(); itr.hasNext();) {
      Record record = (Record) itr.next();
      if (record.getSid() == RowRecord.sid) {
        row++;
      }
    }
    return row;
  }
  
  static int getRowsOfSheet(Sheet sheet) {
    int rows = 0;
    sheet.setLoc(0);
    while(sheet.getNextRow() != null) {
      rows++;
    }
    return rows;
  }

  @SuppressWarnings("deprecation")
  static List<Record> getRecords(InputStream input) {
    try {
      POIFSFileSystem poifs = new POIFSFileSystem(input);
      InputStream stream = poifs.getRoot().createDocumentInputStream("Workbook");
      return org.apache.poi.hssf.record.RecordFactory.createRecords(stream);
    } catch (IOException e) {
      logger.error("IO异常:{}", e.getMessage());
      e.printStackTrace();
    }
    return Collections.EMPTY_LIST;
  }

  static void convertLabelRecords(List records, int offset, Workbook workbook) {

    for (int k = offset; k < records.size(); k++) {
      Record rec = (Record) records.get(k);

      if (rec.getSid() == LabelRecord.sid) {
        LabelRecord oldrec = (LabelRecord) rec;

        records.remove(k);
        LabelSSTRecord newrec = new LabelSSTRecord();
        int stringid = workbook.addSSTString(new UnicodeString(oldrec.getValue()));

        newrec.setRow(oldrec.getRow());
        newrec.setColumn(oldrec.getColumn());
        newrec.setXFIndex(oldrec.getXFIndex());
        newrec.setSSTIndex(stringid);
        records.add(k, newrec);
      }
    }
  }

  public static byte[] getBytes(Workbook workbook, Sheet[] sheets) {
    // HSSFSheet[] sheets = getSheets();
    int nSheets = sheets.length;

    // before getting the workbook size we must tell the sheets that
    // serialization is about to occur.
    for (int i = 0; i < nSheets; i++) {
      sheets[i].preSerialize();
    }

    int totalsize = workbook.getSize();

    // pre-calculate all the sheet sizes and set BOF indexes
    int[] estimatedSheetSizes = new int[nSheets];
    for (int k = 0; k < nSheets; k++) {
      workbook.setSheetBof(k, totalsize);
      int sheetSize = sheets[k].getSize();
      estimatedSheetSizes[k] = sheetSize;
      totalsize += sheetSize;
    }

    byte[] retval = new byte[totalsize];
    int pos = workbook.serialize(0, retval);

    for (int k = 0; k < nSheets; k++) {
      int serializedSize = sheets[k].serialize(pos, retval);
      if (serializedSize != estimatedSheetSizes[k]) {
            throw new IllegalStateException("Actual serialized sheet size (" + serializedSize
            + ") differs from pre-calculated size (" + estimatedSheetSizes[k] + ") for sheet (" + k
            + ")");
        Sheet.serializeIndexRecord() does not
      }
      pos += serializedSize;
    }
    return retval;
  }

  public static void main(String[] args) throws Exception {
    final String PATH = "E:\\projects\\java\\ws_0\\export\\data\\";
    InputStream[] inputs = new InputStream[10];
    inputs[0] = new java.io.FileInputStream(PATH + "07_10.xls");
    for(int i = 1; i <= 9; i++) {
      inputs[i] = new java.io.FileInputStream(PATH + "07_0" + i + ".xls");
    }
    OutputStream out = new FileOutputStream(PATH + "xx.xls");
    long t1 = System.currentTimeMillis();
    merge(inputs, out);
    System.out.println(System.currentTimeMillis() - t1);//简陋的测试一下时间
  }

}

分享到:
评论
31 楼 juluren 2011-10-18  
参考 http://cnluntan.net/blog/
30 楼 bluseli 2011-09-16  
我想问一下
InputStream stream = poifs.getRoot().createDocumentInputStream("Workbook"); 

中的
createDocumentInputStream方法怎么没有。请帖出来。参数Workbook是写死的吗。谢谢
29 楼 sjpsega 2009-07-02  
cats_tiger 写道
写入的时候flush一下试试,任何优化都是有局限的,没有万金油。
如果用户对格式要求不严格,建议还是用csv格式,然后打包成zip比较好。我的方案是被BT客户逼出来的。

我需要的格式不多,只要分sheet存储就行。
csv格式不能分sheet的吧,郁闷……
28 楼 cats_tiger 2009-07-02  
其实在fins的GT-Grid的例子中有一个很好的方案,写出的也是纯正的excel。只是不知道能否导出多个sheet,以后有时间了研究一下。
27 楼 cats_tiger 2009-07-02  
写入的时候flush一下试试,任何优化都是有局限的,没有万金油。
如果用户对格式要求不严格,建议还是用csv格式,然后打包成zip比较好。我的方案是被BT客户逼出来的。
26 楼 sjpsega 2009-07-02  
你好,看了你的这个帖子对我帮助很大。
但在我的项目中又碰到了问题……
我在实验11个文件,每个文件1W行,5列,速度很快,大概就11秒的样子。
但是当11个文件,每个文件1W行,但列变为20列的时候,数据要8W多条的时候又报OOM了......
看了你的程序想改动,但又无从下手...
我想问题是,你的程序,最后一次写入,inputStream中的数据过大,OOM...
我暂时的思路是,从inputStream[1]开始,每个读取完毕都写入一次,如此循环,不知道这样行不?
25 楼 cats_tiger 2009-06-10  
flyinglife 写道
cats_tiger 写道
flyinglife 写道
呵呵,没看到这些

/** 
   * 将多个Xls文件合并为一个,适用于只有一个sheet,并且格式相同的文档 
   * @param inputs 输入的Xls文件,第一个XLS文件必须给出足够sheet空间 
   * 例如,总共200000行数据,第一个文件至少3个空白sheet 
   * @param out 输出文件 
   */


我新建了一个空的包含4个sheet的空xls后,导入数据还是内存溢出了,不知道楼主有没有测试到这种情况。

没有遇到。
我所说的情况,是客户对格式要求很严格,比如表头加粗什么的,如果没有这样的要求,导出为CSV格式就可以,这样肯定不会溢出的。



楼主,您的程序可不可以改动之后支持特别大的数据量呢,还有多sheet.我们客户要求xls格式。所以这个问题挺棘手的。

应该是可以的,但是我现在没有时间改了

24 楼 flyinglife 2009-06-09  
cats_tiger 写道
flyinglife 写道
呵呵,没看到这些

/** 
   * 将多个Xls文件合并为一个,适用于只有一个sheet,并且格式相同的文档 
   * @param inputs 输入的Xls文件,第一个XLS文件必须给出足够sheet空间 
   * 例如,总共200000行数据,第一个文件至少3个空白sheet 
   * @param out 输出文件 
   */


我新建了一个空的包含4个sheet的空xls后,导入数据还是内存溢出了,不知道楼主有没有测试到这种情况。

没有遇到。
我所说的情况,是客户对格式要求很严格,比如表头加粗什么的,如果没有这样的要求,导出为CSV格式就可以,这样肯定不会溢出的。



楼主,您的程序可不可以改动之后支持特别大的数据量呢,还有多sheet.我们客户要求xls格式。所以这个问题挺棘手的。
23 楼 cats_tiger 2009-06-09  
flyinglife 写道
呵呵,没看到这些

/** 
   * 将多个Xls文件合并为一个,适用于只有一个sheet,并且格式相同的文档 
   * @param inputs 输入的Xls文件,第一个XLS文件必须给出足够sheet空间 
   * 例如,总共200000行数据,第一个文件至少3个空白sheet 
   * @param out 输出文件 
   */


我新建了一个空的包含4个sheet的空xls后,导入数据还是内存溢出了,不知道楼主有没有测试到这种情况。

没有遇到。
我所说的情况,是客户对格式要求很严格,比如表头加粗什么的,如果没有这样的要求,导出为CSV格式就可以,这样肯定不会溢出的。
22 楼 flyinglife 2009-06-08  
呵呵,没看到这些

/** 
   * 将多个Xls文件合并为一个,适用于只有一个sheet,并且格式相同的文档 
   * @param inputs 输入的Xls文件,第一个XLS文件必须给出足够sheet空间 
   * 例如,总共200000行数据,第一个文件至少3个空白sheet 
   * @param out 输出文件 
   */


我新建了一个空的包含4个sheet的空xls后,导入数据还是内存溢出了,不知道楼主有没有测试到这种情况。
21 楼 flyinglife 2009-06-08  
楼主,为什么都用你第二个类也不能合并成一个多sheet的xls文件?

我说下我的理解:表中有17w条数据,每5000条一个文件,现在有35个文件。现在我用你的第二程序,期望生成一个包含35个sheet的xls文件。但是现在合并之后是一个65000多行的一个sheet的xls文件。

20 楼 lwqmrs 2008-12-15  
请问你用的是哪个版本的poi
19 楼 fjlyxx 2008-12-09  
是不是可以考虑  用追加你EXCEL工作表单来作呢 sheet追加的方式。我不知道POI有没有这个功能。如果有你可以现把数据分散放在sheet 0  1 2 3 4 5 6中。
不要意思POI不了解。胡乱说的,不过看你的代码内存构建是从sheet开始的。
还有一点建议,代码要重构了,维护起来难度太大。
18 楼 cats_tiger 2008-11-26  
reci 写道

- -两段代码都编译不过...InputStream stream = poifs.getRoot().createDocumentInputStream("Workbook");这个没找到createDocumentInputStream这个方法

我用的是poi3.1
17 楼 reci 2008-11-21  
- -两段代码都编译不过...
InputStream stream = poifs.getRoot().createDocumentInputStream("Workbook");
这个没找到createDocumentInputStream这个方法
16 楼 grandboy 2008-11-08  
真是变态。那么多数据导出来干什么? 不可能看吧。应该是数据共享使用来导入其他系统吧。

我以前就是xml的格式扩展名改成xls来做的。没有碰到这种变态客户。
15 楼 yunsong 2008-11-07  
楼主的方法不错,值得参考。我以前也遇到过这样的问题。经常内存溢出,后来也是生成N个文件,再打成一个rar包让客户下载,不过就是没有合并成一个excel文件
14 楼 cats_tiger 2008-11-03  
kimmking 写道

直接用dsoframer,页面上就是excel,纯正的excel
不需要导出,调用另存就成了。

能处理多少数据呢?我们的要求是50万行每行20多列。
13 楼 laowood 2008-10-31  
留名关注一下
12 楼 kimmking 2008-10-31  
直接用dsoframer,页面上就是excel,纯正的excel
不需要导出,调用另存就成了。

相关推荐

    java解决大批量数据导出Excel产生内存溢出的方案

    java解决大批量数据导出Excel产生内存溢出的方案

    java多线程导出excel(千万级别)优化

    轻松解决普通poi形式导出Excel的中出现的栈溢出问题,此资源可实现千万级数据分批导出csv文件,csv大数据量导出(千万级别,不会内存溢出),多线程导出 ,生产环境已经很稳定的使用着

    java中使用poi导出Excel大批量数据到客户端

    在java web系统应用中我们经常会用到大批量数据的导出,动辄就上几十万几百万的数据让我们的程序感觉压力很大,甚至都出现无法导出的情况,如内存溢出等。 java中使用poi导出Excel大批量数据到客户端 存在两个导出...

    poi_模板导出excel,支持百万级数据模板导出

    poi导入、导出,支持百万级数据模板导出、合并excel。项目为spring-boot-2上开发。resource里面有模板,在junit...注意此版本不支持分页导出,一次性导出大批量数据也会出现内存溢出问题,最新上传的版本支持分页导出,

    Java 导出大批量数据excel(百万级).pdf

    目前java框架中能够生成excel文件的的确不少,但是,能够生成大数据量的excel框架,我倒是没发现,一般数据量大了都会出现内存溢出,所以,生成大数据量的excel文件要返璞归真,用java的基础技术,IO流来实现。...

    easyExcel实现大数据导出

    阿里巴巴easyExcel实现大数据导出!!

    [rar文件] java导出100万以上excel大数据样例

    1、 大数据导出excel文件; 2、 Excel导出大数据时内存溢出; 二、思路:将数据存储到一个.xls的文件内,实际写入的是可以通过excel打开的html文本文件。由于文本文件可以进行续写,可以避免内存溢出。 三、优点:...

    真香!Java 导出 Excel 表格竟变得如此简单优雅

    poi 和 jxl 对内存的消耗很大,在处理大批量的数据时,容易造成内存溢出。比如处理一个 3M 的 Excel,poi 和 jxl 可能需要上百兆的内存,但 easyexcel 可能只需要几百或几千 KB(内存消耗对比有些夸张)。在性能这...

    java实现csv导出千万级数据实例

    轻松解决普通poi形式导出Excel的中出现的栈溢出问题,此资源可实现千万级数据分批导出csv文件,测试实现16500000条数据大概80秒左右;具体表里内容。

    CSV大数据分批并压缩导出

    使用POI、JXL导出大量的数据到Excel很容易造成内存溢出,而CSV采用流的方式将大数据分批并压缩导出

Global site tag (gtag.js) - Google Analytics