seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

JavaからExcelの起動なしでExcelファイルを読み書きする

Jakarta POIと、OLE構造化ストレージ(OLE構造化ファイル)

MS-OfficeのWORD/EXCELなどのファイル形式はOLE構造化ドキュメントで、簡単にいえば、ファイルシステム上のファイルの中に、ファイルシステムがあるようなものである。そのため、OLE構造化ドキュメントの中には、更に別のドキュメントや画像などのリソースを入れることが可能である。いまでこそ(あたりまえすぎて)重要とは思われていない感もあるが、まだ16ビットアプリケーション全盛期のアプリケーション間に深い断絶があった時代において、インプレースOLEとしてドキュメントの中に埋め込んだ別のドキュメントのアプリケーションが、その場で起動できるような仕組みを実現するために考えられた、相当複雑なファイル形式である。
Windowsには当然、この機構はAPIとして用意されているのだが、この複雑さが災いして、Windows APIを使わずに、このファイルを利用することは非常に困難であった。当然、Pure JAVAで実装しようとすれば、その複雑さを自前で実装しなければならないことになる。
Excelファイルを扱うにしても、Excelの表データのフォーマット自身はBIFF形式と呼ばれるもので比較的簡単に扱えるものらしいが*1、このBIFF形式のデータがOLE構造化ファイルの中に格納されていることがネックなのである。
そして、その困難を解決したのがPOI(Poor Obfuscation Implementation(貧しく曖昧な実装) )である。(よくまあ、こんな蔑称を授けていただいたものだと思う。*2 )
POIは名前はどうであれ、非常にいい仕事をしてくれるようだ。

POIを入手する

POIはApache Jakartaプロジェクトの1つである。
http://jakarta.apache.org/poi/
この中には構造化ドキュメント形式を扱うためのサブプロジェクトがいくつかあるが、その中のHSSFExcelの読み書きを行うためのサブプロジェクトである。
といっても、HSSF個別で入手するわけではなくPOI一式になっている。
projectの下にあるdownloadのリンクからバイナリパッケージをダウンロードする。
たとえば、poi-bin-2.5.1-final-20040804.zipを取得すると、中に

  • poi-2.5.1-final-20040804.jar
  • poi-contrib-2.5.1-final-20040804.jar
  • poi-scratchpad-2.5.1-final-20040804.jar

といったjarファイルがある。
Excelを読み書きする上で必要なのはpoi-2.5.1-final-20040804.jarである。
これをプロジェクトのクラスパスに通しておけばよい。

Excelファイルを作成してみる。

ExcelファイルをExcelを使わずにJAVAから作成してみるテスト。

package jp.seraphyware.hssftest;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;

import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFDataFormat;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.HSSFColor;

public class ExcelWriteTest {

    public static void main(final String[] args) throws Exception {
        // 新規にワークブックをメモリ上に作成
        final HSSFWorkbook workbook = new HSSFWorkbook();

        int cnt = 0;

        // シートの作成
        for (int sheetNo = 0; sheetNo < 10; sheetNo++) {
            final HSSFSheet worksheet = workbook.createSheet();
            // シート名に日本語を使う場合は明示的にUTF-16を指定する必要あり。
            workbook.setSheetName(sheetNo,
                    "シート" + sheetNo, HSSFWorkbook.ENCODING_UTF_16);

            // 行 x 列で埋める
            for (int rowIdx = 0; rowIdx < 10; rowIdx++) {
                final HSSFRow row = worksheet.createRow(rowIdx);
                for (short colIdx = 0; colIdx < 20; colIdx++) {
                    final HSSFCell cell = row.createCell(colIdx);
                    // 日本語をセットするためにはUTF-16を指定する必要あり
                    cell.setEncoding(HSSFCell.ENCODING_UTF_16);
                    final String val = String.format(
                            "%1$d表%2$d行%3$d列", sheetNo, rowIdx, colIdx);
                    // 引数の型を認識してセルに値をセットする。
                    cell.setCellValue(val);

                    cnt += 1;

                    // スタイルの設定
                    final HSSFCellStyle style = workbook.createCellStyle();
                    final HSSFDataFormat format = workbook.createDataFormat();
                    style.setDataFormat(format.getFormat("@")); // セルの書式を文字列にする

                    // 罫線の作成
                    if (cnt % 3 == 0) {
                        style.setBorderBottom(HSSFCellStyle.BORDER_MEDIUM);
                        style.setBorderLeft(HSSFCellStyle.BORDER_THIN);
                    }

                    // セルのフォントの色、サイズの指定
                    if (cnt % 7 == 0) {
                        final HSSFFont font = workbook.createFont();
                        font.setColor(HSSFColor.RED.index);
                        font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
                        font.setFontHeight((short)(16 * 20)); // 16pt (1/20pt単位)
                        style.setFont(font);
                    }
                    else {
                        // セルの水平・垂直揃えを中央にする。
                        style.setAlignment(HSSFCellStyle.ALIGN_CENTER);
                        style.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);
                    }

                    // スタイルのセット
                    cell.setCellStyle(style);
                }
            }

            // カラム幅の設定
            for (short colIdx = 0; colIdx < 20; colIdx++) {
                // 15文字幅。デフォルトフォントの「0」に対する1/256を単位とする。
                final short width = 256 * 15;
                worksheet.setColumnWidth(colIdx, width); 
            }
        }

        // ファイルへ保存
        final OutputStream os = new BufferedOutputStream(
                new FileOutputStream("テスト1.xls"));
        try {
            workbook.write(os);
        }
        finally {
            os.close();
        }
    }

}

とくに難しい点はなかったと思う。
カラム幅の設定やフォントサイズの設定方法で多少困惑した。
日本語を使う場合にはUTF-16を指定しないと文字化けしてしまうので注意が必要である。

Excelファイルを読み込んでみる

ExcelファイルをExcelを使わずにJAVAから読み込んでみるテスト。

package jp.seraphyware.hssftest;

import java.io.FileInputStream;

import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;

public class ExcelReadTest {

    public static void main(final String[] args) throws Exception {
        // ファイルからOLE構造化ドキュメントにバインド
        final POIFSFileSystem fs = new POIFSFileSystem(
                new FileInputStream("テスト1.xls"));
        
        // ワークブックの読み込み
        final HSSFWorkbook workbook = new HSSFWorkbook(fs);

        // シート数の取得
        final int sheetMax = workbook.getNumberOfSheets();
        for (int sheetNo = 0; sheetNo < sheetMax; sheetNo++) {
            final HSSFSheet sheet = workbook.getSheetAt(sheetNo);
            final String sheetName = workbook.getSheetName(sheetNo);
            System.out.println("シート名: " + sheetName);
            
            // 定義されている最後の行番号を取得する
            final int rowMax = sheet.getLastRowNum();
            for (int rowIdx = 0; rowIdx <= rowMax; rowIdx++) {
                final HSSFRow row = sheet.getRow(rowIdx);
                if (row == null) {
                    // 空行はスキップ
                    System.out.println("*空行");
                    continue;
                }
                // 定義されている最後の列番号を取得する
                final short colMax = row.getLastCellNum();
                for (short colIdx = 0; colIdx <= colMax; colIdx++) {
                    final HSSFCell cell = row.getCell(colIdx);
                    if (cell == null) {
                        // 空セルはスキップ
                        System.out.println("空セル");
                        continue;
                    }
                    
                    // セルの値を文字列として取得
                    final String value = cell.getStringCellValue();
                    System.out.println(String.format(
                            "%1$d行%2$d列=%3$s", rowIdx, colIdx, value));
                }
            }
        }
    }
    
}

こちらはエンコードの指定とかも必要なく、普通に読んでくれた。
セルの位置を指定して読み込む形になるので、どこが終わりであるのか判断するためにsheet.getLastRowNum()row.getLastCellNum()を使っている。
また、セルが飛び地になっている場合にはデータが存在せず行やセルがnullの場所もありえる。

結論

一般的な「表」の入出力という用途であれば、Excelで書き込む上でも読み込む上でも十分な能力があると思う。
さすがに図形の張り込み、グラフもろもろの見た目にこだわり始めると大変そうな気がするが、私にとって、そんな機能は必要ない。
これは非常に良いと思った。

*1:チャートや、そのほかのオブジェクトは難しいらしいが。

*2:歴史的経緯からすれば、あのWin31時代のメモリとハードウェアの制約から、今日的な複合アプリケーション連携のまともなデータを扱おうとするには、あのような形式にならざるをえなかったのだと思うが、たしかに今日的にはメモリも大きくディスクも速くCPUパワーもふんだんに使えるから、もっとシンプルな方式が望まれるのもわかる。OLE構造化ファイルが「太りやすい」のは高速保存の機能などがあることからも明らかで、これはデータを書き換えず、ただ追記することで高速化しようとしたのである。今となっても意味のないことである。