POI で巨大なExcelファイルを操作する


はじめに

POI による Excel 操作はメモリ消費が激しいため、大きなファイルを操作する場合には注意が必要です。

xlsx 形式のファイル読み込みには SAX Event API 、書き込みには Streaming Usermodel API を使うことで、メモリフットプリントを改善することができます。代償として、通常の Usermodel API と比べて低レベルな API を扱う必要があります。

以下に各APIの利用方法について見ていきます。


Usermodel

Usermodel はPOIが提供する一般的で高レベルなAPIです。

以下のようにスプレッドシートのメンタルモデルとマッチした操作が可能です。

Workbook workbook = WorkbookFactory.create(file);
Sheet sheet = workbook.getSheetAt(i);
Row row = CellUtil.getRow(0, sheet);
Cell cell = CellUtil.getCell(row, 0);

Usermodel は HSSF(xls) と XSSF(xlsx) ファイルを透過的に扱うことができ、ほとんどのケースで有用です。

Usermodel の利用が適さないのは、大きなファイルを扱うケースです。

POI は十分に高速ですが、対象ファイルを全てメモリに展開して操作するため、大きなファイル操作ではメモリを大量に消費し、操作できなくなることがしばしばあります。


低メモリフットプリントなAPI

大きなファイルを扱うために、POI では以下のAPIが提供されています。

読み込み 書き込み
HSSF(xls) Event API -
XSSF(xlsx) SAX Event API Streaming Usermodel API

各APIは、比較的低レベルなAPIとなっているため、Excelファイルの内部構造をある程度理解した上で利用する必要があります。

それぞれ以下に見ていきましょう。


Event API

Event API は HSSF(xls) で、Usermodel と比較して比較的小さなメモリフットプリントで xls ファイルを読むことができます。

Event API は xls ファイル中のレコード(シート上の1行の意味ではない)読み取りに応じたリスナーを使います。

以下のように HSSFListener を実装したクラスを作成し、レコードの sid に応じた処理を定義します。

public class EventListener implements HSSFListener {
    @Override
    public void processRecord(Record record) {
        switch (record.getSid()) {
            case BoundSheetRecord.sid: // ..
            case BOFRecord.sid:      // ..
            case SSTRecord.sid:      // ..
            case BlankRecord.sid:    // ..
            case BoolErrRecord.sid:  // ..
            case FormulaRecord.sid:  // ..
            case StringRecord.sid:   // ..
            case LabelRecord.sid:    // ..
            case LabelSSTRecord.sid: // ..
            case NoteRecord.sid:     // ..
            case NumberRecord.sid:   // ..
            case RKRecord.sid:       // ..
        }
    }
}


xls ファイルは以下のように読み取り、前述のリスナーを登録してイベントを得ます。

try (POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(file));
     InputStream din = fs.createDocumentInputStream("Workbook")) {
    HSSFRequest req = new HSSFRequest();
    req.addListenerForAllRecords(new EventListener());
    HSSFEventFactory factory = new HSSFEventFactory();
    factory.processEvents(req, din);
} catch (IOException e) { }


注意点としては、上記例では未編集のセルについてイベントが得られない点です。

この場合は、MissingRecordAwareHSSFListener 使い、ダミーのイベントを得ることができます。

MissingRecordAwareHSSFListener listener
        = new MissingRecordAwareHSSFListener(new EventListener());
FormatTrackingHSSFListener formatListener
        = new FormatTrackingHSSFListener(listener);
req.addListenerForAllRecords(formatListener);


Streaming Usermodel API

Streaming Usermodel API は、大きな XSSF(xlsx) の書き込みが必要な場合に利用できます。

前述の Usermodel と同様に扱えますが、メモリには一部の行(Row)のみを保持し、他の部分はディスクの一時ファイルに退避することで、大きなファイルの書き込みを可能にしています。

以下のように SXSSFWorkbook のインスタンスに対して処理を行います。

SXSSFWorkbook wb = new SXSSFWorkbook(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);
    }
}

wb.write(new FileOutputStream("foo.xlsx"));
out.close();
wb.dispose();

この例では、メモリには100行(Row)のみを保持します。処理終了時には、最後の100行(Row)のみをメモリに保持し、それ以前の行はディスクにフラッシュされます。

ですので、既にディスクにフラッシュされた行にアクセスしても null となります。

SXSSFWorkbook の引数に既存のXSSFワークブックを渡せば、既存ファイルへの追記ができます。この場合、既存の行はディスクにフラッシュされた状態となります。

new SXSSFWorkbook(-1) のようにインスタンスを生成し、flushRows(100) のようにマニュアルでディスクへのフラッシュを行うこともできます。


SAX Event API

SAX Event API は、XSSF(xlsx) の XML を SAX で扱うことで、メモリフットプリントの小さい読み込みができます。

以下のようなハンドラを作成し、それそれのイベントに応じた処理を定義します。

public class SheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler {

    @Override
    public void startRow(int rowNum) {
        // ...
    }

    @Override
    public void endRow(int rowNum) {
        // ...
    }

    @Override
    public void cell(String cellReference, String formattedValue, XSSFComment comment) {
        // ...
    }
}

読み込みには、最初に OPCPackage (Open Packaging Conventions) を開きます。

OPCPackage pkg = OPCPackage.open(xlsx, PackageAccess.READ);

これは、zip で固められた xlsx ファイル形式を表します。

続いて OPCPackageから Shared Strings Table と Styles Table を取得します。

ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg, false);
XSSFReader xssfReader = new XSSFReader(pkg);
StylesTable styles = xssfReader.getStylesTable();

Shared Strings Table は、セルに入力した共通した文字を共有テーブルとして保持したもので、シートファイルから参照されます。

Styles Table は各種スタイルの定義となります。

これらを用いて、以下のように XMLReader にハンドラを登録することで読み込み処理を行います。

XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) xssfReader.getSheetsData();
while (sheets.hasNext()) {
    InputStream stream = sheets.next();
    final XMLReader reader = XMLHelper.newXMLReader();
    reader.setContentHandler(new XSSFSheetXMLHandler(
            styles,
            null,
            strings,
            new SheetHandler(),
            new DataFormatter(),
            false));
    reader.parse(new InputSource(stream));
}

なお、上記では DataFormatter() はデフォルトのものを利用していますが、日本語版の Excel を使っている場合は、日付の書式がうまく扱えないため注意が必要です。


SAX Event API にて XSSF(xlsx) を CSV 形式で出力するサンプルを以下に用意したので、興味のある方は参考としてください。

github.com