Spreadsheet::ParseExcel でメモリを節約する

| | トラックバック(0)

カテゴリ:

最近はエクセルをWebサーバーに置いておいて、
それを直接読むCGIをおいて似非データベースみたいにして使ってます。

仕事だと、データはエクセルで作ってもらわざるを得なかったり、
会計処理ソフトなどはデータの書き出しがエクセルのみだったりとかで、
データはエクセルでしか存在することが多く、
それをいちいちCSVに変換したり、
データベースに突っ込んだりしていてはデータを最新に保ち続けるのは
難しいわけです。
なので、エクセルデータを直接置いて直接読むという蛮行をやっているわけです。
エクセルデータはPerlのモジュール「Spreadsheet::ParseExcel」で
読み込むことができます。
http://search.cpan.org/dist/Spreadsheet-ParseExcel/lib/Spreadsheet/ParseExcel.pm
小さなエクセルファイルならば問題ないのですが、
少し大きくなると、途端にCGIの動作が遅くなります。
そこがなんとかなんないかなと思っていたら、
よさそうな方法があったので書いておきます。

以下は、ParseExcelモジュールドキュメントの「Reducing the memory usage of Spreadsheet::ParseExcel」という一節の翻訳です。
===========

Spreadsheet::ParseExcel を使ったプログラムは、大きなエクセルファイルを処理するときは大量のメモリーを消費してしまうかもしれず、その結果処理が完了せずに終わってしまうかもしれない。下に、それがなぜ起こるのかと対処法を記す。

Spreadsheet::ParseExcelはエクセルファイルを2段階で処理します。第一段階ではエクセルのバイナリストリームをOLE::Storage_Liteを使ってOLEコンテナファイルから取り出します。第二段階ではworkbook・worksheet・cellのデータを読むためにバイナリストリームをパースし、メモリに蓄えます。cellのデータを蓄えるために大量のメモリーが使用されます。

エクセルファイルがパースされ、それぞれのcellがcell操作関数に読み込まれると、比較的大きくて入り組んだcellオブジェクトが生成されます。その中にはcellの値とcellの書式に関するすべての情報が含まれるために、大量のメモリが消費されるのです。大きなエクセルファイル(PCのメモリが256MBの場合のサイズが10MBのエクセルファイル)の場合、この過剰消費がシステムを停止させてしまいます。

しかし、多くの場合、必要とされる情報はcellの値だけです。そういう場合には、cell操作関数に独自の関数を指定し、Spreadsheet::ParseExcelにcellのデータを保持しないように指示すれば、メモリの過剰消費をかなり避けられます。そうするためには、パースオブジェクトを作る際に、new()にcell操作関数を渡せば済みます。以下がその例です。

    #!/usr/bin/perl -w

    use strict;
    use Spreadsheet::ParseExcel;

    my $parser = Spreadsheet::ParseExcel->new(
        CellHandler => \&cell_handler,
        NotSetCell  => 1
    );

    my $workbook = $parser->parse('file.xls');

    sub cell_handler {

        my $workbook    = $_[0];
        my $sheet_index = $_[1];
        my $row         = $_[2];
        my $col         = $_[3];
        my $cell        = $_[4];

        # Do something useful with the formatted cell value
        print $cell->value(), "\n";

    }

ユーザーの独自のcell操作関数はコードリファレンスとしてnew()に渡され、NotSetCellも一緒に渡されることで、 Spreadsheet::ParseExcel にパースしたcellを保持しないよう伝達します。その際に、行と列を反復する必要がないことに注意してください。cellを読み込むための行と列の反復はパースの一部として自動的に行われます。

cell操作関数には5個の引数が渡されます。1番目($workbook)は、Spreadsheet::ParseExcel::Workbookのオブジェクトへのリファレンスで、パースしたワークブックを表現します。このオブジェクトはSpreadsheet::ParseExcel::Workbookのメソッドを使ってアクセスすることができます。"Workbook"の部分を参照して下さい。2番目($sheet_index)は、パースされたworksheetの番号です(0はじまり)。3番目と四番目( $row 、$col)は、(0から始まる)cellの行と列の番号です。5番目($cell)は Spreadsheet::ParseExcel::Cellオブジェクトのリファレンスです。これはcellからデータを引き出すのに使います。詳しくは"Cell"を見て下さい。

この独自cell操作関数を使う方法は、データベースフィルターにエクセルを書き込む場合に便利です。(?)DBの呼び出しをcell操作関数の中に書いて置けるからです。

スプレッドシートのデータの全てが必要なわけではない場合は、cell操作関数に制御コマンドを加えます。例えば、先に出した例を拡張して、いくつかif文を加えるだけで、はじめの2つのワークシートの最初の10行だけを表示するように変更できます。
#!/usr/bin/perl -w

    use strict;
    use Spreadsheet::ParseExcel;

    my $parser = Spreadsheet::ParseExcel->new(
        CellHandler => \&cell_handler,
        NotSetCell  => 1
    );

    my $workbook = $parser->parse('file.xls');

    sub cell_handler {

        my $workbook    = $_[0];
        my $sheet_index = $_[1];
        my $row         = $_[2];
        my $col         = $_[3];
        my $cell        = $_[4];

        # Skip some worksheets and rows (inefficiently).
        return if $sheet_index >= 3;
        return if $row >= 10;

        # Do something with the formatted cell value
        print $cell->value(), "\n";

    }

しかし、このままだと結局はワークブック全体を処理することになります。さらに処理時間を短縮したい時は、「workbook ParseAbortメソッド」を使って、必要なデータを読み込んだ後に、パースを(強制)終了します。

    #!/usr/bin/perl -w

    use strict;
    use Spreadsheet::ParseExcel;

    my $parser = Spreadsheet::ParseExcel->new(
        CellHandler => \&cell_handler,
        NotSetCell  => 1
    );

    my $workbook = $parser->parse('file.xls');

    sub cell_handler {

        my $workbook    = $_[0];
        my $sheet_index = $_[1];
        my $row         = $_[2];
        my $col         = $_[3];
        my $cell        = $_[4];

        # Skip some worksheets and rows (more efficiently).
        if ( $sheet_index >= 1 and $row >= 10 ) {
            $workbook->ParseAbort(1);
            return;
        }

        # Do something with the formatted cell value
        print $cell->value(), "\n";

    }
comments powered by Disqus

トラックバック(0)

このブログ記事を参照しているブログ一覧: Spreadsheet::ParseExcel でメモリを節約する

このブログ記事に対するトラックバックURL: http://nozawashinichi.sakura.ne.jp/MT-4.25/mt-tb.cgi/1005

comments powered by Disqus

このブログ記事について

このページは、Shinichi Nozawaが2012年6月15日 22:30に書いたブログ記事です。

ひとつ前のブログ記事は「OpenPNEでインターフェースをいじる2」です。

次のブログ記事は「Google AnalyticsのAPI仕様変わった?」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。