« 2005年06月 | メイン | 2005年08月 »
前回「シンプルなWebアプリなCGIを書いてみる」をかいてから、時間かかったけど、とりあえず自分にフレンドリーなCGIフレームワークをつくってみた。2回ほど仕事で作るCGIに使っただけで、機能も貧弱だけどアウトプット主義で公開。ネタは前回と同じ郵便番号を入力するCGI。
入力する情報が増えたのと確認画面がでるようになったのが進歩。いままでCGIが複雑化するので嫌いだった確認画面がフレームワークのおかげで簡単になったのでいれた。
CGIのソースコードはこうなった。
index.cgi
#!/usr/bin/perl use strict; MyCGI::SimpleWebApp::Zip002->new()->run(); package MyCGI::SimpleWebApp::Zip002; use strict; use base qw(CGI::SAF); sub setup{ my $self = shift; $self->register_run_mode('confirm','reg'); $self->header_props( -charset=>'EUC-JP', -expires=>'now', -pragma=>'no-cache', -cache_control=>'no-cache' ); $self->html_fillin_props( ignore_fields=>['mode'] ); } sub do_default{ my $self = shift; $self->template('index.tmpl.html'); $self->fillin(1); } sub do_confirm{ my $self = shift; $self->validate; if($self->alert){ return $self->forward('default'); } $self->template('index_confirm.tmpl.html'); $self->fillin(1); } sub do_reg{ my $self = shift; $self->validate; if($self->alert){ return $self->forward('default'); } $self->template('index_reg.tmpl.html'); } sub validate{ my $self=shift; $self->alert("郵便番号が正しく入力されていません") unless $self->param('zip') =~ /^?d{7}$/; $self->alert("性別が入力されていません") unless $self->param('sex') =~ /^[12]$/; $self->param('sexj',{1=>"男性",2=>"女性"}->{$self->param('sex')}) if(defined $self->param('sex')); } 1;
実際のCGIの部分は、
MyCGI::SimpleWebApp::Zip002->new()->run();
の部分だけなんだけど、アプリケーションの動作をするクラスもCGIに直接書いている。
かなり短縮して書くパターンで書いているので、あまり紹介に適さないCGIになってしまっているが、CGI::Applicationを参考にSledgeのいいところを入れた感じで、アプリケーションクラスが書けていると思う。
フレームワークのモジュールCGI::SAFを継承して、setupでrun_modeを登録、headerやfillinformのオプションを設定。
デフォルトのrun_modeはdefaultに固定なので、do_defaultを書く。do_default内ではSledgeのようにtemplateを自動で選択はしないので、templateを設定。fillinをする場合は1をセット。これで済みます。前回の配列@errはalertメソッドとなって埋め込まれています。HTML::Templateへも自動で反映されます。入力不足があった場合、
sub do_reg{ my $self = shift; $self->alert("郵便番号が正しく入力されていません") unless $self->param('zip') =~ /^?d{7}$/; if($self->alert){ #エラーなのでフォームに戻る return $self->forward('default'); } }
と書けます。ずいぶん見通しもよくなりました。
フレームワークのソースは以下。SAFはSmall or Simple Application Frameworkの略。
動作に必要なモジュールは、CGI.pmとClass::Accessor。
欲しいモジュールは、HTML::TemplateとHTML::FillInForm。
CGI::ApplicationとSledgeをかなり参考にしました。
package CGI::SAF; use strict; use CGI; use Carp; use base qw(Class::Accessor); __PACKAGE__->mk_accessors(qw(current_run_mode user_req_mode finish fillin template)); sub new{ my($class,$query)=@_; $query=CGI->new() unless $query; return undef unless(ref($query) =~ /^CGI/); my $self = { _query=>$query, _run_modes=>["default"], _alert_array=>[], current_run_mode=>"default", user_req_mode=>"", finish=>undef, fillin=>undef, template=>undef, _html_fillin_props=>{ fobject=>$query }, _html_template_props=>{ die_on_bad_params=>0, loop_context_vars=>1, associate=>$query }, _rediret_header_props=>{}, _header_props=>{} }; bless($self,$class); $self->init(); $self->setup(); return $self; } sub _run_modes{shift->{_run_modes}} sub _alert_array{shift->{_alert_array}} sub query{shift->{_query};} sub param{shift->query->param(@_);} sub register_run_mode{ push(@{shift->_run_modes},@_); } sub error{ my $self = shift; local $Carp::CarpLevel=$Carp::CarpLevel+1; Carp::croak @_; } sub header_props{ my($self,%args) = @_; foreach my $key (keys %args){ if(defined $args{$key}){ $self->{_header_props}->{$key}=$args{$key}; }else{ delete $self->{_header_props}->{$key} } } return $self->{_header_props}; } sub response{ my($self,$body,%args) = @_; $self->header_props(%args); print $self->query->header(%{$self->header_props}); print $body; $self->finish(1); } sub redirect_header_props{ my($self,%args) = @_; foreach my $key (keys %args){ if(defined $args{$key}){ $self->{_rediret_header_props}->{$key}=$args{$key}; }else{ delete $self->{_rediret_header_props}->{$key} } } return $self->{_rediret_header_props}; } sub redirect{ my($self,$str,%args) = @_; $self->redirect_header_props(%args); require URI; my $uri = URI->new_abs($str,$self->query->url()); $self->{_rediret_header_props}->{"-uri"}="$uri"; print $self->query->redirect(%{$self->redirect_header_props}); $self->finish(1); } sub html_template_props{ my($self,%args) = @_; foreach my $key (keys %args){ if(defined $args{$key}){ $self->{_html_template_props}->{$key}=$args{$key}; }else{ delete $self->{_html_template_props}->{$key} } } return $self->{_html_template_props}; } sub html_template{ my($self,$template,%args)=@_; $self->param('alert_array',$self->_alert_array); if(ref($template) eq "SCALAR"){ $self->{_html_template_props}->{scalarref}=$template; }else{ $self->{_html_template_props}->{filename}=$template; } $self->html_template_props(%args); require HTML::Template; return HTML::Template->new(%{$self->html_template_props()}); } sub html_fillin_props{ my($self,%args) = @_; foreach my $key (keys %args){ if(defined $args{$key}){ $self->{_html_fillin_props}->{$key}=$args{$key}; }else{ delete $self->{_html_fillin_props}->{$key} } } return $self->{_html_fillin_props}; } sub html_fillin{ my($self,$html,%args)=@_; $self->{_html_fillin_props}->{scalarref}=?$html; $self->html_fillin_props(%args); require HTML::FillInForm; my $fif = HTML::FillInForm->new(); return $fif->fill(%{$self->html_fillin_props()}); } sub make_output_content{ my $self = shift; my $template = $self->template; return $self->error("template not found?n") unless $template; my $tmpl=$self->html_template($template); my $html=$tmpl->output; if($self->fillin){ $html=$self->html_fillin($html); } return $html; } sub alert{ my $self = shift; $self->push_alert(@_); return $self->have_alert(); } sub push_alert{ my $self=shift; push(@{$self->_alert_array},map {{val=>$_}} @_); } sub have_alert{ my $self = shift; return ($#{$self->_alert_array} > -1) ? 1 : 0; } sub forward{ my($self,$rm)=@_; $rm = "default" unless $rm; my %hash = map{$_=>1} @{$self->_run_modes}; $self->error("Not defined such run mode '$rm'?n") unless exists $hash{$rm}; $self->current_run_mode($rm); my $run_mode = "do_" . $rm; return $self->$run_mode(); } sub run{ my $self = shift; my $body; eval{ $self->prerun(); return if $self->finish; my $rm = $self->mode_fixer(); $rm = "default" unless $rm; $self->user_req_mode($rm); #do something $self->forward($rm); return if $self->finish; #create output content unless finish $body=$self->make_output_content(); }; if($@){ $body=$self->do_error($@); } return if $self->finish; $body = "" unless defined $body; print $self->query->header(%{$self->header_props}); print $body; } sub init{} sub setup{} sub prerun{} sub mode_fixer{ shift->param('mode'); } sub do_default{ my $self = shift; return $self->response("CGI Simple Application"); } sub do_error{ my $self=shift; return $self->response(@_); } 1;
仕事に使えるポータビリティの高い状態を維持しつつ便利で楽なものをもうすこし考えてみようと思います。
HTMLの中にはてなIDを埋め込むという話題だけど、ついにPerlモジュールもでてきて方法が決まりそうだ。方法は、HTMLの中にコメントでRDFとfoafを埋め込む方法。
これがあまりスマートに見えない。
<-- <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://xmlns.com/foaf/0.1/"> <rdf:Description rdf:about="http://d.hatena.ne.jp/naoya/20050727/1122428017"> <dc:creator> <Person> <holdsAccount> <OnlineAccount> <accountServiceHomepage rdf:resource="http://www.hatena.ne.jp/"/> <accountName>naoya</accountName> </OnlineAccount> </holdsAccount> </Person> </dc:creator> </rdf:Description> </rdf:RDF> -->
を埋め込むと。
rdf:aboutのところはpermalink毎に変えていかないといけないのか?かなり面倒だぞ。
複数個のアカウントを書いていこうとか思ったら、blogのような自動生成ではないホームページではほぼ無理。ツールの進歩も必要かな。
ただ、はてな近藤さんの
よく考えるとRSSとかにも入れることができてより拡張性が高くなるかもしれませんね。
確かに。
microformatsでvCardを表現するときの、hCardは、
<div class="vcard"> <a class="url fn" href="http://tantek.com/"> Tantek Çelik </a> <div class="org">Technorati</div> </div>
こういうのと比べるとエレガントじゃないよなぁと思ったり。
仕様的な正しさ≠エレガント。まぁ、hCardは仕様的にもあっているわけだが。
オライリーのFlash Hacks購入。家にあるHacksシリーズはこれで5冊目。この本の帯には、
探求と発想こそが 最大のツールである。
当たり前のことでもあるが、Flashにおいてはまさにそれ。カット&トライとひらめきが重要。この本の内容は試してみたくなるような、アイディアの元になるようなHackがいくつもあって、かなりHack度高しです。ただ、Hack度が高い分だけこの本だけでは何ともならないとも思う。どなたかよいFlashの本教えてください。
ちなみにFlashは嫌いだ。重いしパレットをガンガン開くし、なまじ見た目が派手な分だけ目を引いてしまうし夢に出るし。嫌いだ
最近話題のHatena ID Auto-Discovery の仕様。
伊藤直也さんの最新日記によると、
<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" /> <link rel="DC.creator" href="http://www.hatena.ne.jp/user/naoya" />
というところに来ているみたい。
投げ銭だけではなくはてなの外のページがはてなIDでいう誰のページなのか分かるというのはとてもおもしろことができそうな気がする。しかもはてなに留まらず方法が一般化すれば、自分に関係あるサイトをいくつか並べて、
<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" /> <link rel="DC.creator" href="http://www.hatena.ne.jp/user/kazeburo" /> <link rel="DC.creator" href="http://mixi.jp/show_friend.pl?id=56485" /> <link rel="DC.creator" href="http://blog.drecom.jp/kazeburo/profile" />
と、いろいろ使えそうだ。全部同じ人なので仕様的に矛盾してそうだけど。
やらないけど、これを見ていて1つ思いついたことがある。
ぱどタウンで「私のホームページ」を設定することがあるのですが、このホームページに違う人のページをさも自分のページのようにリンクを張って問題になることがあります。僕には問題になる所以がなかなか理解できませんが。んで、自分のページであることを証明してもらうために、
<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" /> <link rel="DC.creator" href="http://youyou.padotown.net/home/XXXXXX/" />
のような物を簡単に埋め込めるようになったら、本当にその人のページかどうかチェックすることが簡単になるなと思った。やらないだろうけど。
Auto-Discovery自体は、宮川さんのRSS Auto Discoveryを参考にすればたぶんすぐできる。
ぱどタウンの福岡のタウンがオープンしました。タウン名は「よかよかタウン」です。「とんこつタウン」でなくて非常にがっかりです。
ぱどのIRにもありますが、ぱどが発行されていないエリアへのオープンです。
このたびの「ぱどタウン 福岡よかよかタウン」の開設は、「情報誌ぱど」未発行地域におい て、地域コミュニティを形成し、既存のぱどタウンとともに全国でのぱどタウンネットワークを構築 することを目的としております。また、住民(ユーザー)の獲得につきましては、ネット広告、各ウ ェブサイト、ぱどタウンでのお友達紹介機能、地域メディアなどを積極的に活用し、より多くの方々 にご活用いただきたいと思っております。
だそうです。
ちなみに福岡には実験機能として、招待状の機能があります。問題が起きることもあるかもしれませんが、使われていくとうれしいかなと思います。
西日本は二十一日、高気圧にすっぽり覆われ、京都市内は三五・七度、大阪市内は三四・六度とそれぞれこの夏一番の暑さを記録した。
From Yahoo!ニュース
昼間に外に出たら死ぬかと思った。
京都を紹介するときに「京都は暑くて寒い、でもいいところ」というのがあるけど、暑いとはいっても40度とか言わないし、寒いと言っても雪がめちゃくちゃ降るわけでもない。結構穏やかなところという意味でOK?
Safari 1.3でescape関数には日本語部分が%uNNNNになってしまうバグがあって、はてなブックマークなどのbookmarkletが気分よく使えない。そこでbookmarkletのコードの「escape」を「encodeURIComponent」に変えてみた。
javascript:window.open('http://b.hatena.ne.jp/add?mode=confirm&is_bm=1&title='+encodeURIComponent(document.title)+'&url='+encodeURIComponent(location.href),%20'_blank',%20'width=520,height=600,resizable=1,scrollbars=1,statusbar=1');undefined;
ブックマークのアドレス編集で上と入れ替えます。
とりあえずは文字化けなく動くことは確認しましたが、タイトルの日本語がescapeされないので問題を起こすことがあるかもしれません。
CGIの中でHTTPのheaderを出力してしまってからのDIEというのはなるべく避けているけど、やってしまったときに、CGI::Carpがどのように処理をしているのかがなかなか面白かったのでメモ。
my $bytes_written = eval{tell STDOUT}; if (defined $bytes_written && $bytes_written > 0) { print STDOUT $mess; } else { print STDOUT "Content-type: text/html?n?n"; print STDOUT $mess; }
標準出力のファイルポジションをチェックするなんてことができるのね。
CGI::Carpはプログラムを作成するときは便利ですが、本運用時は使わないようにしましょう。
エラーが出た場合のメッセージがセキュリティ上問題になる可能性もあります。
自転車をしばらく封印して会社まで歩いて通うことにしている。
大体2.5kmぐらいなんだけど30分かからないぐらいで歩けます。思ったより楽。
汗かいてKRPのロビーに入った瞬間クーラーで寒い。
ってかあんなに広いのにクーラーが効いていること自体すげぇ。
技術評論社のCGI&PerlポケットリファレンスがPerlの本では今も昔も一番便利です。家のも会社のもぼろぼろになってます。
Amazonで書籍をPerlで検索したときに16番目に表示されますが、この手の本で出版から6年もたってこの場所にあることは珍しいことだと思います。
ただ、この本で十分だということではありません。あくまで即効性の高いポケットリファレンス。もっと詳しく知りたくなったらオライリーにしませう。
HTML::Templateに限ったことではないですが、Webアプリケーション(CGI)で使用するテンプレートをどこに置いているかの話。
自分はテンプレートを大抵CGIと同じディレクトリに置いてしまいます。テンプレートのファイル名には「*.tmpl.html」を使用することにしてます。
-|- index.cgi |- index.tmpl.html |- imgs/画像
などという形で画像もここに置いてしまいます。なので
http://nomadscafe.jp/test/sample_web_app/zip001/index.cgi
に対して、
http://nomadscafe.jp/test/sample_web_app/zip001/index.tmpl.html
とするとテンプレートが得られます。もしセキュリティ的に良くないのであればApacheの設定で表示できなくするのは簡単です。
<Files *.tmpl.html> Order deny,allow Deny from all </Files>
この方法によるメリットとしては、「画像やリンクなどのパスがずれない」ということです。デザイナーから受け取ったHTMLをそのまま使用できます。HTML::Templateのタグを埋め込んだテンプレートをデザイナーに渡しておけばデザイン変更も簡単にやってもらえます。ちなみにHTMLをパーツごとに分けることもしなかったりします。Dreamweaverで一括変換してしまえば済みます。WYSIWYGで作成できなくなるのはデザイナーにとってはマイナスです。テンプレートの拡張子を「.tmpl.html」と「html」を残しているのも同じ理由です。
ただ、これはシンプルな、あるいは簡単なWebアプリケーションの話だと思います。大掛かりにフレームワークやMVCモデルを導入していくとトリガーとなるCGIの中身が抽象化しいって、テンプレートの位置関係が分かりにくくなると思います。そのときはテンプレートと画像は一つのディレクトリに納めてViewがHTML中のパスを自動的に変更してくれたりしたらいいのかな。
モヒカン族の説明にあって脊髄反射でムッと来た。自分もメガネをかけていて眼鏡くん(メガネくん)と呼ばれるようなこともあるが非常にむかつく。
一般的には「眼鏡をかけている(装着している)男性」の意味だが、
メガネくん愛好者の間では「メガネを取っても美形ではあるが、普段はメガネを外さない」メガネくんが最も好ましいという見方が一般的である。
なお、眼鏡は体の一部である。
とキーワードでは説明されるが、「眼鏡くん」という言葉自体、差別的でメガネをかけている人を卑下することだ。人格を否定されるようなものだ。体の一部だ。スラムダンクで小暮が桜木に「メガネくん」と呼ばれるシーンもあるがあのページがあるおかげでスラムダンクが嫌いになりそうなぐらいだ。完全版全巻持っているけど。眼鏡くんに萌えなぞない。体の一部だ。身体の特徴的な部分の名称をあだ名にされるとむかつくそれと同じ。
メガネないと童顔にみえるからメガネは外せない。眼鏡っ娘は(ry
HTML::Templateには2つの記法がサポートされています。
1つめはタグとして書く方法。
<TMPL_VAR NAME=hoge>
もう一つは、コメントとして書く方法
<!-- TMPL_VAR NAME=hoge -->
どちらかというと、上の方法がデフォ。コメントとして書く方法についての説明自体も
If you're a fanatic about valid HTML and would like your templates to conform to valid HTML syntax
ValidなHTMLの狂信者だったらとちょっと皮肉めいた書き方をしている。この書き方はちょっと気に入らない。
自分は基本的にコメント記法で書いてます。なぜかと言えばDreamweaverを使うから。ValidなHTMLを目指している訳ではないです。
某メール配信の予定があるのですが、その内容が届きました。そのメールには、これでテストメールをしてください。配信希望時刻は、X時などと指定されている。
「これで配信」で「配信希望時間」ならそのまま何も考えないが、「テストメール」で「配信希望時間」ときていると、意地悪く考えてしまうな。つまり『とりあえず期日が迫っているから現状でテキストを渡すけど、配信希望時間まで修正するから覚悟しておけよ』と言うことですね。
追記(19:36):「最終版としてくださいと」という版がきました。予感的中
今ぱどタウンの古い仕組みをOOなPerlでラッピングしていて、そろそろ情報取得まわりは一段落。1つ1つのCGIを書き直して行くのに、フレームワークがあった方がいいなと構築中なわけだが、1度自分がCGIでよく書くパターンをまとめてみた。
CGIとは〜〜だというのはたくさん答えがあるだろうけど、自分的には「ユーザからの入力をチェックして画面にだす」ものだというのがしっくりくる。当たり前のことを書いているようだがMVCモデルでいうコントローラーの作成がCGIだと。んで、この「チェックをして画面にだす」というところで入力不足でエラーだった場合にどのようにユーザフレンドリーに知らせるかが1つのポイントだと思う。
簡単によく書くパターンでCGIを書いてみた。郵便番号を入力するとそのまま表示する超シンプルなWebアプリとなってます。(CGIのソース、入力画面テンプレート、完了画面テンプレート。PerlのモジュールはHTML::TemplateとHTML::FillInFormが必要です)
このページで、わざと郵便番号ではない数字を入力して送信すると
このような形でエラーが報告されます。フォームの上部にエラーをまとめて表示して、入力した文字列もそのままHTML::FillInFormで復元されてます。簡素なエラー画面でブラウザの戻るボタンを押す必要もありません。戻るボタンを押したときにいままで入力していた物がなくなっているということもないです。まぁ、ほとんどはHTML::FillInFormのおかげなんですけどね。
エラーを画面に出す部分は、入力画面テンプレートの
<!-- TMPL_LOOP NAME=err --><!-- TMPL_IF NAME=__first__ --><ul class="error_list"><!-- /TMPL_IF --> <li><!-- TMPL_VAR NAME=val ESCAPE=HTML --></li> <!-- TMPL_IF NAME=__last__ --></ul><!-- /TMPL_IF --><!-- /TMPL_LOOP -->
の部分。HTML::Templateのloop_context_vars機能を使って、エラーがないときに影響がでないようにしています。あと、テンプレートにはフォーム中にhiddenでmodeの設定をいれてます。
CGIの構造は、よけいな部分を消して書くと
1 #!/usr/bin/perl 2 use strict; 3 use CGI; 4 my $q = CGI->new; 5 my @err; 6 if($q->param('mode') eq "reg"){ 7 push(@err,"郵便番号が正しく入力されていません") unless($q->param('zip') =~ /^?d{7}$/); 8 if($#err == -1){ 9 #完了画面出力 10 exit; 11 } 12 } 13 $q->param('err',[map{{val=>$_}} @err]); 14 #通常画面出力 15 my $tmpl = HTML::Template->new( 16 filename => 'index.tmpl.html', 17 loop_context_vars=>1, 18 die_on_bad_params=>0, 19 associate=>$q 20 ); 21 my $fif = HTML::FillInForm->new; 22 print $q->header( 23 -charset=>'EUC-JP', 24 -expires=>'now', 25 -pragma=>'no-cache', 26 -cache_control=>'no-cache' 27 ); 28 print $fif->fill(scalarref=>?$tmpl->output,fobject=>$q,target=>'form1');
のようになっています。5行目でエラー報告用配列をつくり、6行目で「mode」によって分岐。7行目で郵便番号のチェックをして入力エラーがあればエラーの内容をエラー報告用配列にpushする。
8行目でエラー配列をチェックして配列の要素数が「-1」つまりエラーがなければ完了画面を出力してexit。エラーがある場合やmodeがregではない場合は入力フォームの出力へ。データはすべてHTML::TemplateとHTML::FillInFormへはCGIオブジェクトのパラメーターで渡されます。こんな形でエラーの表示と入力情報の復元を実現します。
かんたんなアプリケーションについては大体こんな感じで書いてきました。プログラムの見通しという面では上から下へと手続き型に書いてあるので、まぁmodeの部分が分かれば悪くないと思ってますが、modeが増えたとき(入力確認画面など)のプログラムが非常に難解で見通しの悪いものになることも事実。
もっと大きなプログラムを構築したり、1つのCGIで行うことが増えた時や複数人での開発にはやはりフレームワークがあった方がいいのかもとようやく気づき今構築中。CGI::Applicationをもっと簡単にしたようなもので今までのイメージを崩さないで使えることを目指してます。ぱどタウンとべったりな所を整頓して具合良ければまたここに書きます。
さっき、POISのとあるサーバ1台のHDDがとんだ。予備マシンをセットアップして今は通常通り動いてます。
HDDがエラーなら良くあることなんだけど見事にとんだ。お亡くなり。
HDDをはずしてBIOSで認識しないしぴーぷーぴーぷーぺーとか言ってる。
梅雨も本格化してきたし、季節の変わり目なのかなぁ。