2010年7月19日月曜日

PHPExcelのメモリ馬鹿食いについて

===============
2010-10-20追記
関連POST
PHPExcel1.7.4の不具合?について
===============

PHPExcelのシート追加でメモリをガンガン消費しちゃう件。
リンク先を参考に解析。

SE奮闘記: 【PHP】PHPExcelがループ内でメモリを使いすぎる

ワーク
シート数
メモリ使用量(Byte)
改善前 改善後
1 21,233,664 21,233,664
2 22,544,384 22,544,384
3 25,165,824 23,855,104
4 30,408,704 24,903,680
5 40,632,320 25,952,256
6 60,293,120 27,262,976
7 100,139,008 28,311,552
8 180,092,928 29,360,128
9 340,000,768(推定) 30,408,704
10 659,816,448(推定) 31,457,280

改善前

$aSheet = $this->excel->getActiveSheet();
for ($i = 0; $i < count($this->data); $i++) {
    if ($i != 0) {
        $this->excel->addSheet(clone $aSheet);
    }
}

改善後

$aSheet = $this->excel->getActiveSheet();
$newSheets = array();
for ($i = 0; $i < count($this->data); $i++) {
    if ($i != 0) {
        $newSheets[] = clone $aSheet;
    }
}
foreach ($newSheets as $newSheet) {
    $this->excel->addSheet($newSheet);
}

改善前は大体次の数式のようにメモリ使用量が増えていってるっぽくてO(2^n)のオーダー(かな?)
f(n+1) = 2.5MB * 2^(n+1) + f(n)
改善後は次の数式でO(n)のオーダー。
f(n) = 1.25MB * n + 20MB

リンク先で触れられていた「シートをコピーして追加する」の処理をやめて「テンプレートからシートを流し込むとよい」という内容は確認できませんでした。
リンク先とは状況が違うとは思いますが、こちらの解析ではPHPExcel_Worksheetを__cloneする時にメモリがどんどん消費されていっているらしくて、PHPExcel_Worksheet#copy()も内部的には__cloneを呼んでいる。

だからと言って、
$this->excel->addSheet(clone $aSheet);
が
$newSheets[] = clone $aSheet;
になったことでそんなに劇的に変わる理由がよく分かりませんが、
$aSheet = $this->excel->getActiveSheet();
した$aSheetが指しているactiveなシートの参照先が
$this->excel->addSheet(clone $aSheet);
したことによってどうにかなってしまったんではなかろうか、と想像。

PHPExcelの実装の問題なのか、cloneする変数の参照先の親オブジェクトにループ内でメンバ変数追加するとそうなってしまうPHPの仕様なのか、何とも言えませんが、今後の為にメモ。

2010年7月9日金曜日

zend + dojo : 画像のsubmitの挙動が変だったハナシ

Zend + Dojoで画像ボタンでsubmitしようとしたんですが、サーバからの返却が何やらおかしかった。
<input type="submit">を<input type="image" src="">にしたい場面はたくさんありますが、ajaxでの接続で挙動がおかしい場面があったのでメモ。

通常のボタンでsubmitする場合

※HTML

<form action="#" id="hoge">
    <button name="btn_submit" id="btn_hoge" type="button" value="hoge">hoge</button>
</form>

// dojo.connect
<script type="text/javascript">
    // イベントハンドラの登録
    dojo.addOnLoad(function() {
        // btn_hoge要素にイベントハンドラを設定する。
        // onclickイベントを引っ掛けて、hogeフォームの内容をsubmit
        // /fuga/piyoアクションをコールして、json形式でレスポンスを受け取る。
        dojo.connect(dojo.byId("btn_hoge"), "onclick", function() {
            dojo.xhrPost({
                url: "/fuga/piyo/format/json",
                content:{"a":"b"},
                form: "hoge",
                handleAs: "json",
                load: function(data){
                    alert('OK');
                },
                error: function(error, args) {
                    alert('NG');
                }
            });
        });
    });
</script>
※Zend

/**
 * Fugaコントローラ
 */
class FugaController extends Zend_Controller_Action
{
    /**
     * piyoアクションのコンテキストとしてjsonを登録
     */
    public function init()
    {
        parent::init();
        $contextSwitch = $this->_helper->getHelper('contextSwitch');
        $contextSwitch->addActionContext('piyo', 'json')
                      ->initContext();
    }

   /**
     * Piyoアクション
     */
    public function PiyoAction()
    {
        // View変数をシリアライズしてjsonとして返却
        // {"viewMessages":"abc"}
        $this->view->message = 'abc';
    }
}

上記は、普通にajax通信できますが、以下のようにボタンを画像にすると、dojoのイベントハンドラでerror関数がコールされ、error変数として「Error: Unable to load /fuga/piyo/format/json status:0」という良く分からないエラーが返却されます。

※HTML

<form action="#" id="hoge">
    <input type="image" src="" name="btn_submit" id="btn_hoge" value="btn_submit" />
</form>

サーバでログ取ってみても、postされてたりされてなかったり、規則性なくランダムにサーバにリクエストが到達しているようでした。
error関数も複数回コールされることがあり、1回のボタン押下で何回か/puga/piyoアクションがコールされることもありました。

何でそんなことになるのか分かりませんでしたが、dojo.connectでinput属性のエレメントにハンドラを設定しているのが問題っぽい。

<form action="#" id="hoge">
</form>
<input type="image" src="" name="btn_submit" id="btn_hoge" value="btn_submit" />

こんな感じでformの外にimage要素を配置すると、サーバには1回だけ行儀よくPOSTし、返却値も期待するJSON形式で受け取れるようになります。

formの中にいる<input type="image">な要素にdojo.connectすると、dojoで接続されたイベントハンドラと、form自体のactionとの両方が実行されてしまうようです。

2010年6月27日日曜日

Dojoのdojo.requireでSSL通信したい

※Dojo
※以下、Dojo1.4

Dojoでdojo.require("dijit.form.ComboBox");

なんかでモジュールをロードすると、

http://ajax.googleapis.com/ajax/libs/dojo/1.4.3/dijit/form/ComboBox.js

こんな感じのモジュールロードのHTTP通信が発生します。

で、HTTPSで画面表示してる場合、セキュリティ警告が出る、、。

https通信でモジュールロードしたい場合のtips。

参考)Googleグループ

こんな感じでDojoをロードしている部分でモジュールのロード元を指定します。

<script src="/libs/dojo1.4/dojo.xd.js" djConfig="parseOnLoad: true, isDebug: true">

SSL対応後。

<script type='text/javascript'>
    var djConfig = {
        isDebug: true,
        parseOnLoad: true,
        modulePaths: {
            "dojo": "https://ajax.googleapis.com/ajax/libs/dojo/1.4.3/dojo",
            "dijit": "https://ajax.googleapis.com/ajax/libs/dojo/1.4.3/dijit",
            "dojox": "https://ajax.googleapis.com/ajax/libs/dojo/1.4.3/dojox",
            }
    };
</script>
<script src="/libs/dojo1.4/dojo.xd.js"></script>

ローカルにDojo丸ごとダウンロードしてmodulePathsでローカルのjs資材を指定しても大丈夫だと思う。

2010年6月2日水曜日

GAE環境で使えないJREパッケージ

GoogleさんのPaaS環境Google App Engienで使えないJREクラス。

ホワイトリスト
http://code.google.com/intl/ja/appengine/docs/java/jrewhitelist.html

ブラックリスト
http://d.hatena.ne.jp/uehaj/20090505/1241531042

2010/06/02現在、java.awtは使えない。
ので、帳票関係厳しい。。awt非依存のライブラリが必要。

状況証拠から、以下のようなエラーが出れば、多分使えないクラスを使ってる。
rejectとかって言われてるし。

java.lang.NoClassDefFoundError: java.awt.Point is a restricted class. Please see the Google App Engine developer's guide for more details.
at com.google.appengine.tools.development.agent.runtime.Runtime.reject(Runtime.java:51)

2010年6月1日火曜日

お名前.comのVPSにyumインストール

お名前.comのVPSを使ってみています。
何せ安いですし、サポートも丁寧で今のところお勧めです。

お名前.comのVPS、デフォルトでyumが入っていません。

yumは要るよ。。
ってことでインストール。

yumのRPM探してインストールしようと思ったのですが、色々なブログに載っているftp.riken.jpとかはリンク切れでRPM見つかりませんでした。yumのサイトも見てみたんですが、よく分からなかったのでソースからインストールしようと挑戦し、、撃沈・・・。

yum入れないと何も始まらないのに、なんか面倒なことになってきたな~、と思っていたところ、VPSのコンパネからインストールできるという記事が!!
http://www.vps-root.net/02-function/yum.html

リンク先に書いてあるとおりです。
コンパネから「セキュリティ」>「アップデート」の設定を「アップデートなし」に変更。
http://guide.onamae-server.com/vps/sys.php?c1=6&c2=26&c3=63&g=3
※この操作をするとコンパネのサポート対象外になります。

これでyum入ります。
※コンパネで変更後すぐにはyumコマンド見つかりませんでした。SSHをログアウトかSVNを再起動かなんかそんなことをしているうちに見つかるようになります。

ちなみにコンパネから手動更新してみたところ「何も更新ないですよ」って言われましたが、yum updateしてみたところ200個ぐらい落ちてきたので、yumの方がいいっぽいです。

2010年5月26日水曜日

PHP Excelで自動改行&行高さ自動調整

PHP Excelで文字の自動改行をしたい。

ここに記載されているプロパティの設定でOK。
http://phpexcel.codeplex.com/Thread/View.aspx?ThreadId=18434

$objPHPExcel->getActiveSheet()->getStyle('A1')->getAlignment()->setWrapText(true);

ただ、PHPExcelをnewするときは問題ないけれど、テンプレートファイルをロードして書き込むとテンプレートファイルの行高さがずっと保持されたままの状態になるらしい。

自動改行設定した後に行高さを調整しないとキレイに出力されない。

// テンプレートファイルの読み込み
$reader = PHPExcel_IOFactory::createReader('Excel5');
$excel = $reader->load(TEMPLATE_PATH);

// シートの設定
$excel->setActiveSheetIndex(0);
$sheet = $excel->getActiveSheet();
$sheet->getDefaultStyle()->getFont()->setName('MS Pゴシック');
$sheet->getDefaultStyle()->getFont()->setSize(9);
$sheet->setTitle('sheet name');

// 書き込み
$sheet->setCellValue('A1', 'あいうえおかきくけこさしすせそ');

// 自動改行
$sheet->getStyle('A1')->getAlignment()->setWrapText(true);

// 行高さ設定(setRowHeightにパラメタ指定しないことで自動調整)
$sheet->getRowDimension(1)->setRowHeight();

PHPでExcel出力

WEBからExcel出力させたい!
既に色々なライブラリや参考WEBサイトがありますが、備忘録として。。

※参考
http://d.hatena.ne.jp/saicologic/20080606/1212715767
http://www.syuhari.jp/blog/archives/1621
http://journal.mycom.co.jp/articles/2009/03/06/phpexcel/index.html

以下、PHP Excel + Zend Frameworkの例です。

ここやここなんかを参考にPHP Excelのダウンロードと展開、パスの設定。

Excel95形式(.xls)を扱う限りはphp_zipは必要なさそうです。

PHP Excelに身を委ねてExcelファイルを生成。
生成した一時ファイルを返却。
Content-Typeは「application/vnd.ms-excel」らしい。

include 'PHPExcel.php';
class ExcelController extends Zend_Controller_Action
{
    
    public function testAction()
    {
        // 一時ファイル
        $fileName = '/tmp/output.xls';
        
        $excel = new PHPExcel();
        // シートの設定
        $excel->setActiveSheetIndex(0);
        $sheet = $excel->getActiveSheet();
        $sheet->setTitle('sheet name');
        // セルに値を入れる
        $sheet->setCellValue('A1', 'あいうえお');
        // Excel95 形式で出力
        $writer = PHPExcel_IOFactory::createWriter($excel, 'Excel5');
        $writer->save($fileName); // ホントはファイル出力せずにバイナリで返して欲しい
        
        // ダウンロード
        $this->getResponse()
             ->setHeader('Content-Type', 'application/vnd.ms-excel')
             ->setHeader('Content-Disposition', 'attachment; filename="test.xls"')
             ->appendBody(file_get_contents($fileName))
             ->sendResponse();
        exit;
    }
    
}

こんな感じで、
example.com/excel/test/
にアクセスでtest.xlsのダウンロード。

========================
2010/06/27追記

IEの場合、キャッシュ関係のヘッダがないとダウンロードできないらしい。
Pragmaヘッダが無いと、「このインターネットサイトを開くことができませんでした。要求されたサイトが使用できないか、見つけることができません。後でやり直してください。」とのエラー。

include 'PHPExcel.php';
class ExcelController extends Zend_Controller_Action
{
    
    public function testAction()
    {
        // 一時ファイル
        $fileName = '/tmp/output.xls';
        
        $excel = new PHPExcel();
        // シートの設定
        $excel->setActiveSheetIndex(0);
        $sheet = $excel->getActiveSheet();
        $sheet->setTitle('sheet name');
        // セルに値を入れる
        $sheet->setCellValue('A1', 'あいうえお');
        // Excel95 形式で出力
        $writer = PHPExcel_IOFactory::createWriter($excel, 'Excel5');
        $writer->save($fileName); // ホントはファイル出力せずにバイナリで返して欲しい
        
        // ダウンロード
        $this->getResponse()
             ->setHeader('Content-Type', 'application/vnd.ms-excel')
             ->setHeader('Content-Disposition', 'attachment; filename="test.xls"')
             ->setHeader('Cache-Control', 'no-cache') // IE
             ->setHeader('Pragma', 'no-cache')        // IE
             ->appendBody(file_get_contents($fileName))
             ->sendResponse();
        exit;
    }
    
}

========================
2010/07/10追記

atachementのfilenameはそのまま送出するとブラウザによって文字化けする。

※参考
http://oku.edu.mie-u.ac.jp/~okumura/php/filename.php
http://d.hatena.ne.jp/guangda/20100106/1262762061
http://d.hatena.ne.jp/guccyon/20080530/p1

とりあえず、お手軽安全なのはSJISに変換か。。
Win環境以外は知らないことにしよう。

// ダウンロード
$fileName = mb_convert_encoding($fileName, 'SJIS');
$this->getResponse()
     ->setHeader('Content-Type', 'application/vnd.ms-excel')
     ->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"')
     ->setHeader('Cache-Control', 'no-cache') // IE
     ->setHeader('Pragma', 'no-cache')        // IE
     ->appendBody(file_get_contents($fileName))
     ->sendResponse();