Apache POI でサイズの大きいXLSXファイルをCSVに変換する


概要

サイズの大きいXLSXファイルをCSVに変換する必要があったため Apache POI を使用して対応しました。
気を付けた点を備忘録として残しておきます。
サンプルプロジェクトは【こちら

※サイズの大きいXLSXとは数万行、数十万行以上のデータがありXSSFWorkbookで開こうとするとOutOfMemoryErrorが発生してしまうサイズを想定しています。

環境

Java:1.8
POI:3.17

注意点

セルのフォーマットや書式に関しては対応していません。
xls形式(2003形式)には対応していません(公式にはxls対応のサンプルソースもありました)

対応方針

公式サイト の機能概要に Streaming(ala SAX) と書かれているAPIを使用します。
SAX(Simple API for XML)で読み込みを行うためあまりメモリを消費しないようです。

Spreadsheet API Feature Summary

ソース

以下のソースをコピペしてconvertメソッドを実行すればXLSX⇒CSVの変換が行えます。

XLSXからCSV変換のサンプル
    /**
     * XLSXファイルをCSVファイルへ変換する<br>
     * XLSXファイル内に複数シートある場合にも先頭のシートのみが対象となる
     *
     * @param fromXlsxPath 変換元となるXLSXファイルのパス
     * @param toCsvPath 変換先となるCSVファイルのパス(存在するパスを指定した場合は上書き)
     */
    public static void convert(Path fromXlsxPath, Path toCsvPath) {
        System.out.println("開始 XlsxToCsvUtil#convert");

        try (OPCPackage pkg = OPCPackage.open(fromXlsxPath.toAbsolutePath().toString(), PackageAccess.READ);
                BufferedWriter bw = Files.newBufferedWriter(toCsvPath, StandardCharsets.UTF_8)) {
            ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg);
            XSSFReader xssfReader = new XSSFReader(pkg);
            StylesTable styleTable = xssfReader.getStylesTable();

            try (InputStream is = xssfReader.getSheetsData().next()) {
                // パース用のハンドラ生成
                ContentHandler handler = new XSSFSheetXMLHandler(
                        styleTable, null, strings, new XlsxRowHandler(bw), new DataFormatter(), false);

                // パース用のXMLリーダー生成
                XMLReader sheetParser = SAXHelper.newXMLReader();
                sheetParser.setContentHandler(handler);
                System.out.println("パース開始");
                sheetParser.parse(new InputSource(is));
                System.out.println("パース終了");
            }

        } catch (InvalidOperationException | IOException | SAXException | OpenXML4JException
                | ParserConfigurationException e) {
            System.out.println("エラー XlsxToCsvUtil#convert");
        }

        System.out.println("終了 XlsxToCsvUtil#convert");
    }

    /**
     *
     */
    public static class XlsxRowHandler implements SheetContentsHandler {

        private final List<String> row = new ArrayList<>();

        private final BufferedWriter bw;

        public XlsxRowHandler(BufferedWriter bw) throws IOException {
            this.bw = bw;
        }

        @Override
        public void startRow(int rowNum) {
            System.out.println(rowNum + " 行目読み込み開始");
            row.clear();
        }

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

        @Override
        public void endRow(int rowNum) {
            try {
                bw.write(String.join(",", row.stream().map(c -> "\"" + c + "\"").collect(Collectors.toList())));
                bw.newLine();
            } catch (IOException e) {
            }
        }

        @Override
        public void headerFooter(String text, boolean isHeader, String tagName) {
        }
    }

以下のクラスはPOIが準備してくれているクラスでサンプルほぼそのままです。
詳細は理解できていません。

  • OPCPackage
  • ReadOnlySharedStringsTable
  • XSSFReader
  • StylesTable

重要なのは「XlsxRowHandler」です。
こちらはPOIが準備してくれているインターフェースを実装したクラスです。
このクラスでセルの中身を読み込みBufferedWriterで書き出すことでCSV出力しています。
メソッド名を見たら大体わかりますが以下のタイミングでメソッドが実行されます。

  • 行の読み込み開始時 ⇒ startRow
  • セルの中身を参照時 ⇒ cell
  • 行の読み込み終了時 ⇒ endRow

実装の注意点

SheetContentsHandler#cell メソッドは空白セルでは呼ばれません。
空白セルのあるXLSXファイルを扱う際にはそれに応じた実装を行う必要があります。

参考サイト

公式サイト
こちら のページに「XLSX2CSV」と記載されているものが公式のサンプルです。

作成したサンプルプロジェクト