POIのhssfを使って、巨大なエクセルファイルを読み込むには。

日本企業の皆様におかれましては、エクセルが大好きかと、存じます。
いや、僕も大好きなのだけどもさ、表計算かわいいよ、表計算
ウルトラハイパフォーマンスで、データストアとしても超一流。
コードの自動生成してもいいし、まかり間違ったりすると、方眼紙としても使えちゃったり…とか。


と、まぁ、色々使い道が多いので、ついJavaでもエクセルを読書きしたくなったりとかするヨネ。
で、そこでApache POIでつよ、となる訳だけど、POIのHSSFは普通に使うと、
ちょっと大きいエクセルファイルを読もうとしただけで、アフォ程メモリを食ってしまうのですな。
例えば、こんなコードで読み込むのが、まぁ普通ダヨネ。

@Override
protected void readExcel(File excel) {
  BufferedInputStream bi = null;
  try {
    bi = new BufferedInputStream(new FileInputStream(excel));
    HSSFWorkbook workbook = new HSSFWorkbook(bi);
    for (int i = 0, seetSize = workbook.getNumberOfSheets(); i < seetSize; i++) {
      HSSFSheet sheet = workbook.getSheetAt(i);
      LOG.debug(sheet.toString());
      for (int j = 0, rowSize = sheet.getPhysicalNumberOfRows(); j < rowSize; j++) {
        HSSFRow row = sheet.getRow(j);
        LOG.debug(row.toString());
        for (int cellNum = row.getFirstCellNum(), last = row
            .getLastCellNum() + 1; cellNum < last; cellNum++) {
          HSSFCell cell = row.getCell(cellNum);
          LOG.debug(cell);
        }
      }
    }
  } catch (Exception e) {
    LOG.error(e.getMessage(), e);
  } finally {
    FileUtil.close(bi);
  }
}

このコードだと、実は、100MBとかあるエクセルファイルを読むのに500MB程度のヒープサイズじゃ、全然足りなかったりする。
クマった。


で、どうするのかと言うと、イベントAPIなるものが存在するのでつ。

こやつを使うと、大体、元のエクセルファイルと同じか少し多めのヒープサイズがあれば、エクセルファイルを処理できまつ。
ドキュメント内に丁度いい感じのコードが無かったので、僕も少し書いてみたり。


すげぇでっかくなっちゃったけど、まぁこんな感じ。
謎のインナークラスWorkbookScopeが、味噌だったりする。
何でこんなコードなのかは各自考えてクダサシ。

public class HssfEventMain {
  static final Log LOG = LogFactory.getLog(HssfEventMain.class);

  public static void main(String[] args) {
    File excel = new File("datas/Excel2000/cell/100_256.xls");
    new HssfEventMain().readExcel(excel);
  }

  class WorkbookScope {
    FormatTrackingHSSFListener tracker;
    SheetRecordCollectingListener collector;
  }

  protected void readExcel(File excel) {
    BufferedInputStream in = null;
    try {
      final WorkbookScope t = new WorkbookScope();
      MissingRecordAwareHSSFListener missingAware = new MissingRecordAwareHSSFListener(
          new HSSFListener() {
            @Override
            public void processRecord(Record record) {
              handle(record, t);
            }
          });
      t.tracker = new FormatTrackingHSSFListener(missingAware);
      t.collector = new SheetRecordCollectingListener(t.tracker);

      HSSFRequest request = new HSSFRequest();
      request.addListenerForAllRecords(t.collector);

      in = new BufferedInputStream(new FileInputStream(excel));
      POIFSFileSystem fs = new POIFSFileSystem(in);
      HSSFEventFactory factory = new HSSFEventFactory();

      factory.processWorkbookEvents(request, fs);
    } catch (Exception e) {
      LOG.error(e.getMessage(), e);
    } finally {
      FileUtil.close(in);
    }
  }

  protected void handle(Record record, WorkbookScope ws) {
    switch (record.getSid()) {
    case BoundSheetRecord.sid: {
      // Bound Sheet Record (aka BundleSheet) (0x0085)
      handle((BoundSheetRecord) record, ws);
      break;
    }
    case BOFRecord.sid: {
      // Beginning Of File (0x0809)
      handle((BOFRecord) record, ws);
      break;
    }
    case BlankRecord.sid: {
      // Blank cell record (0x0201)
      // Represents a column in a row with no value but with styling.
      handle((BlankRecord) record, ws);
      break;
    }
    case FormulaRecord.sid: {
      // Formula Record (0x0006)
      handle((FormulaRecord) record, ws);
      break;
    }
    case StringRecord.sid: {
      handle((StringRecord) record, ws);
      break;
    }
    case LabelRecord.sid: {
      handle((LabelRecord) record, ws);
      break;
    }
    case LabelSSTRecord.sid: {
      handle((LabelSSTRecord) record, ws);
      break;
    }
    case NumberRecord.sid: {
      handle((NumberRecord) record, ws);
      break;
    }
    case RowRecord.sid: {
      break;
    }
    case -1: {
      handleDummyRecord(record, ws);
      break;
    }
    default: {
      LOG.debug(String.format("sid:[%s] record:[%s]", record.getSid(),
          record.toString()));
      break;
    }
    }
  }

  protected void handle(BoundSheetRecord r, WorkbookScope ws) {
    String fmt = "seetname :[%s] isHidden:[%s] isVeryHidden:[%s]";
    LOG.debug(String.format(fmt, r.getSheetname(), r.isHidden(), r
        .isVeryHidden()));
  }

  static final Map<Integer, String> types = new HashMap<Integer, String>();
  static {
    types.put(BOFRecord.TYPE_CHART, "CHART");
    types.put(BOFRecord.TYPE_EXCEL_4_MACRO, "EXCEL_4_MACRO");
    types.put(BOFRecord.TYPE_VB_MODULE, "VB_MODULE");
    types.put(BOFRecord.TYPE_WORKBOOK, "WORKBOOK");
    types.put(BOFRecord.TYPE_WORKSHEET, "WORKSHEET");
    types.put(BOFRecord.TYPE_WORKSPACE_FILE, "WORKSPACE_FILE");
  }

  protected void handle(BOFRecord r, WorkbookScope ws) {
    String fmt = "Build:[%s] BuildYear:[%s] RequiredVersion:[%s] Version:[%s] Type:[%s]";
    String type = types.get(r.getType());
    if (type == null) {
      type = "UnKnown Type";
    }
    LOG.debug(String.format(fmt, r.getBuild(), r.getBuildYear(), r
        .getRequiredVersion(), r.getVersion(), type));
  }

  protected void handle(BlankRecord r, WorkbookScope ws) {
    String fmt = "Column:[%s] Row:[%s] XFIndex:[%s]";
    LOG
        .debug(String.format(fmt, r.getColumn(), r.getRow(), r
            .getXFIndex()));
  }

  protected void handle(FormulaRecord r, WorkbookScope ws) {
    LOG.debug(ws.tracker.formatNumberDateCell(r));
  }

  protected void handle(StringRecord r, WorkbookScope ws) {
    LOG.debug(r.getString());
  }

  protected void handle(LabelRecord r, WorkbookScope ws) {
    LOG.debug(r.getValue());
  }

  protected void handle(LabelSSTRecord r, WorkbookScope ws) {
    SSTRecord sst = ws.collector.getSSTRecord();
    if (sst == null) {
      LOG.debug("\"(No SST Record, can't identify string)\"");
    } else {
      LOG.debug(sst.getString(r.getSSTIndex()));
    }
  }

  protected void handle(NumberRecord r, WorkbookScope ws) {
    LOG.debug(ws.tracker.formatNumberDateCell(r));
  }

  protected void handle(RowRecord r, WorkbookScope ws) {
    String fmt = "rowNumber[%s] firstCol[%s] lastCol[%s]";
    LOG.debug(String.format(fmt, r.getRowNumber(), r.getFirstCol(), r
        .getLastCol()));
  }

  protected void handleDummyRecord(Record r, WorkbookScope ws) {
    String fmt = "DummyRecord %s";
    LOG.debug(String.format(fmt, r.getClass().getName()));
  }
}


で、OLE2をJavaで読み書きできる様にする為なのか、良く分からないのだけど、

  • org.apache.poi.poifs.filesystem.POIFSFileSystem

って奴が、エクセルの中身を全てbyte[]で抱えちゃうんだよねぇ…。
これは、どうにもならんもんかねぇ…。


コンストラクタだけコードを抜粋しとくます。

public POIFSFileSystem(InputStream stream)
    throws IOException
{
    this();
    boolean success = false;

    HeaderBlockReader header_block_reader;
    RawDataBlockList data_blocks;
    try {
        // read the header block from the stream
        header_block_reader = new HeaderBlockReader(stream);
        bigBlockSize = header_block_reader.getBigBlockSize();
        
        // read the rest of the stream into blocks
        data_blocks = new RawDataBlockList(stream, bigBlockSize);
        success = true;
    } finally {
        closeInputStream(stream, success);
    }
    

    // set up the block allocation table (necessary for the
    // data_blocks to be manageable
    new BlockAllocationTableReader(header_block_reader.getBATCount(),
                                   header_block_reader.getBATArray(),
                                   header_block_reader.getXBATCount(),
                                   header_block_reader.getXBATIndex(),
                                   data_blocks);

    // get property table from the document
    PropertyTable properties =
        new PropertyTable(header_block_reader.getPropertyStart(),
                          data_blocks);

    // init documents
    processProperties(
            SmallBlockTableReader.getSmallDocumentBlocks(
                    data_blocks, properties.getRoot(), 
                    header_block_reader.getSBATStart()
            ), 
            data_blocks, 
            properties.getRoot().getChildren(), 
            null,
            header_block_reader.getPropertyStart()
    );
}