SXSSFWorkbook.write to FileOutputStream 写入大文件

Mir*_* D. 6 java openxml apache-poi kotlin

我正在尝试使用 SXSSFWorkbook 从头开始​​编写 Excel 电子表格。

      wb = SXSSFWorkbook(500)
      wb.isCompressTempFiles = true
      sh = streamingWorkbook.createSheet(t.getMessage("template.sheet.name"))
Run Code Online (Sandbox Code Playgroud)

一切都很好,但是当我调用最终代码时:

    val out = FileOutputStream(localPath)
    wb.write(out)
    out.close()
    // dispose of temporary files backing this workbook on disk
    wb.dispose()
Run Code Online (Sandbox Code Playgroud)

我得到了一个巨大的 excel 文件,而不是我期待的压缩的 XLSX。我尝试手动压缩文件,从 120MB 的文件中我可以将其压缩到 9MB。那么我错过了什么?

使用:Kotlin 和

    implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '4.1.2'  // For `.xlsx` files
Run Code Online (Sandbox Code Playgroud)

-- 更新 1

我的印象是 xlsx 本质上是包含 xml 数据的压缩文件[1]。通过 XSSFWorkbook 和 SXSSFWorkbook 输出的 POI 至少可以压缩 10 个数量级。 我已经使用这个简单的代码来演示:

fun main() {
  val workbook = XSSFWorkbook()
  writeRowsAndSave(workbook, "test.xlsx")
  workbook.close()

  val streamingWorkbook = SXSSFWorkbook(IN_MEMORY_ROWS_WINDOW_SIZE)
  streamingWorkbook.isCompressTempFiles = true
  writeRowsAndSave(streamingWorkbook, "test-streaming.xlsx")
  streamingWorkbook.dispose()
}

private fun writeRowsAndSave(workbook: Workbook, fileName: String) {
  val ROWS_COUNT = 2_000
  val COLS_COUNT = 1_000

  val sheet = workbook.createSheet("Test Sheet 1")
  for (i in 1..ROWS_COUNT) {
    val row = sheet.createRow(i)
    println("Row $i")
    for(j in 1..COLS_COUNT) {
        row.createCell(j).setCellValue("Test $i")
    }
  }

  FileOutputStream("./$fileName").use {
      workbook.write(it)
  }
}
Run Code Online (Sandbox Code Playgroud)

这会产生 5MB-每个文件,压缩后大约有 439KB (?!)。

Axe*_*ter 6

SXSSFWorkbook默认使用内联字符串而不是共享字符串表。这意味着SXSSFWorkbook直接在工作表中写入文本,即使它是同一文本的多次。XSSFWorkbook和 Excel 的 GUI 都使用共享字符串表,其中文本获取索引,相同的文本仅存储一次,然后在工作表中使用索引。但这不会对生成的文件大小产生那么大的影响*.xlsx

SXSSFWorkbook以及创建的所有其他Office Open XML格式文件apache poi均使用org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream. 它使用 deflate 作为压缩算法和Deflater.DEFAULT_COMPRESSION默认压缩级别。人们可以覆盖protected ZipArchiveOutputStream createArchiveOutputStream(OutputStream out)SXSSFWorkbook设置另一压缩级别。但这也不会对生成的文件大小产生那么大的影响*.xlsx

示例Java代码:

import java.io.File;
import java.io.OutputStream;
import java.io.FileOutputStream;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import java.util.zip.Deflater;

class CreateSXSSFDifferentCompression {

 static SXSSFWorkbook createSXSSFWorkbook(int compressionLevel, int rowAccessWindowSize, 
                                          boolean compressTmpFiles, boolean useSharedStringsTable) {
  SXSSFWorkbook workbook = null;
  if (compressionLevel != Deflater.DEFAULT_COMPRESSION) {
   workbook = new SXSSFWorkbook(null, rowAccessWindowSize, compressTmpFiles, useSharedStringsTable) {
    protected ZipArchiveOutputStream createArchiveOutputStream(OutputStream out) {
     ZipArchiveOutputStream zos = new ZipArchiveOutputStream(out);
     zos.setUseZip64(Zip64Mode.AsNeeded);  
     zos.setLevel(compressionLevel);
     return zos;
    }    
   }; 
  } else {
   workbook = new SXSSFWorkbook(null, rowAccessWindowSize, compressTmpFiles, useSharedStringsTable);
  }
  return workbook;
 }

 public static void main(String[] args) throws Exception {

  SXSSFWorkbook workbook = null;

  // uses Deflater.DEFAULT_COMPRESSION and inline strings
  //workbook = createSXSSFWorkbook(Deflater.DEFAULT_COMPRESSION, 500, true, false); 

  // uses Deflater.DEFAULT_COMPRESSION and shared strings table
  //workbook = createSXSSFWorkbook(Deflater.DEFAULT_COMPRESSION, 500, true, true); 

  // uses Deflater.BEST_COMPRESSION and inline strings
  workbook = createSXSSFWorkbook(Deflater.BEST_COMPRESSION, 500, true, false); 

  // uses Deflater.BEST_COMPRESSION and shared strings table
  //workbook = createSXSSFWorkbook(Deflater.BEST_COMPRESSION, 500, true, true); 

  int ROWS_COUNT = 2000;
  int COLS_COUNT = 1000;

  Sheet sheet = workbook.createSheet("Test Sheet 1");
  for (int i = 1 ; i <= ROWS_COUNT; i++) {
   Row row = sheet.createRow(i);
   //System.out.println("Row " + i);
   for(int j = 1; j <= COLS_COUNT; j++) {
    row.createCell(j).setCellValue("Test " + i);
   }
  }

  FileOutputStream out = new FileOutputStream("./Excel.xlsx");
  workbook.write(out);
  out.close();
  workbook.close();
  workbook.dispose();

  File file = new File("./Excel.xlsx");
  System.out.println(file.length());

 }
}
Run Code Online (Sandbox Code Playgroud)

这导致Excel.xlsx文件大小为:

使用 Deflater.DEFAULT_COMPRESSION 和内联字符串时为 5,031,034 字节。

使用 Deflater.DEFAULT_COMPRESSION 和共享字符串表时为 4,972,663 字节。

使用 Deflater.BEST_COMPRESSION 和内联字符串时为 4,972,915 字节。

使用 Deflater.BEST_COMPRESSION 和共享字符串表时为 4,966,749 字节。

用过的:Java 12apache poi 4.1.2Ubuntu Linux

对于 2,000 行 x 1,000 列的电子表格,我既不会称其为巨大,也不认为不同设置的影响很大。

而且条目压缩得非常好。

如果您查看Excel.xlsxZIP 存档,您会发现xl/worksheets/sheet1.xml使用内联字符串时未压缩的大小为 112,380,273 字节。未压缩的大小xl/sharedStrings.xml为 138 字节,仅包含非常基本的 XML。

如果使用共享字符串表,则未压缩的大小xl/worksheets/sheet1.xml为 68,377,273 字节,未压缩的大小xl/sharedStrings.xml为 49,045 字节,包含 2,000 个条目。

如果Excel它本身保存*.xlsx文件,那么当内容相等时,它会创建具有大致相同文件大小的文件。所以Excel它本身使用相同的压缩级别。

当然,当再次将文件存储到存档中时,可以*.xlsx进一步压缩文件。但这并不是我们所期望的文件。 Excel.xlsx*.zipExcel*.xlsx

Microsoft指出Open XML 格式有哪些好处?:

压缩文件 文件会自动压缩,在某些情况下最多可缩小 75%。Open XML 格式使用 zip 压缩技术来存储文档,可以节省潜在的成本,因为它减少了存储文件所需的磁盘空间,并减少了通过电子邮件、网络和 Internet 发送文件所需的带宽。当您打开文件时,它会自动解压缩。当您保存文件时,它会自动再次压缩。您无需安装任何特殊的 zip 实用程序即可在 Office 中打开和关闭文件。

这里重要的部分是:

当您打开文件时,它会自动解压缩。当您保存文件时,它会自动再次压缩。

这意味着,如果apache poi使用其他方法或压缩级别来压缩文件Microsoft Office,则Microsoft Office无法使用apache poi已创建的文件来执行此操作。

因此,由于创建( ) 能够直接打开的apache poi文件,因此它使用与( ) 相同的压缩方法和压缩级别。ExcelMicrosoft OfficeExcelMicrosoft Office