というわけで、突然はじまりました勝手に添削のコーナー。
今回は、WEB+DB PRESS Vol.32の「Yahoo! Web サービス活用ガイド」から。
私もWEB+DB Pressへの連載をはじめたので、同誌のますますの(反映|繁栄)を祈ってやまないのだけど、それだけに、同誌にこういうサンプルコードがあるのは気になる。一応きちんと動くので、blogとかのentryであればこれでもよいのだけど、この手の雑誌はかなり長い間保管され、読者に何度も参照されることを考えれば、「その後」のことを考えて推敲しておく方がいいだろう。Damianも言っていたように、「ソースコードは未来の自分へのラブレター」なのだ。ましてや、雑誌に乗る場合は、読者に向けたラブレターでもあるのだから。
Vol. 32 pp. 94
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: |
#!/usr/local/bin/perl -w
use strict;
use CGI;
use LWP::Simple;
use XML::Simple;
use constant WEBAPI_BASEURL => 'http://api.search.yahoo.co.jp/WebSearchService/V1/webSearch?';
use constant MYYDN_APPID => 'YahooDemo';
use constant MAX_RESULTS => 10;
my $q = new CGI;
print $q->header(-charset => 'utf8'), $q->start_html();
print qq(<form><input type="text" name="query"><input type="submit" value="search"></form>);
if ($q->param("query")){
(my $key = $q->param("query")) =~ s/(\W)/"%" . unpack("H2", $1)/ge;
#リクエストURIの生成
my $req_url = WEBAPI_BASEURL;
$req_url .= "appid=YahooDemo";
$req_url .= "&query=".$key;
$req_url .= "&results=".MAX_RESULTS;
#APIへリクエストを送信
my $yahoo_response = get($req_url);
#取得したXMLをパースする
my $xmlsimple = XML::Simple->new();
my $yahoo_xml = $xmlsimple->XMLin($yahoo_response, ForceArray=>['Result']);
print $yahoo_xml->{'totalResultsAvailable'} . "hits";
print "<ol>";
foreach my $result (@{$yahoo_xml->{'Result'}}){
print qq(<li><a href="$result->{'ClickUrl'}">$result->{'Title'}</a></li>);
}
print "</ol>";
}
print $q->end_html(), "\n";
|
一目見て、Perlが嫌いになりそうなソースである。
私がrefactorしたらこうなった。
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: |
#!/usr/local/bin/perl -T
use strict;
use warnings;
use Readonly;
use URI;
use CGI qw/:standard/;
use LWP::Simple qw/get/;
use XML::Simple;
use Encode qw/encode_utf8/; # to drop utf8 flag from XML::Simple
Readonly my $WEBAPI_BASEURL
=> 'http://api.search.yahoo.co.jp/WebSearchService/V1/webSearch';
Readonly my $MYYDN_APPID => 'YahooDemo';
Readonly my $MAX_RESULTS => 10;
print
header(-charset => 'UTF-8'),
start_html(-lang=> 'ja', -title=>$ENV{SCRIPT_NAME}, -encoding=>'UTF-8'),
start_form(), textfield("query"), submit(-value=>"search"), end_form(),
search_result(),
end_html();
sub search_result{
return unless param("query");
my $uri = URI->new($WEBAPI_BASEURL);
$uri->query_form( appid => $MYYDN_APPID,
query => param("query"),
results => $MAX_RESULTS );
my $response = get($uri) or return;
my $xml = XML::Simple->new->XMLin($response, ForceArray=>['Result']);
return
$xml->{'totalResultsAvailable'}, "hits",
ol(map {
encode_utf8 li(a({href=>$_->{'ClickUrl'}}, $_->{'Title'}))
} @{ $xml->{'Result'} } );
}
|
それでは、解説を進めよう。手元にPerl Best PracticesとPerlプログラミング救命病棟があればなおいい。この二冊は、Perlスクリプトを書いてお金をもらっているひとは必ず手元においておくべきだろう。
まずは定数の扱いから。オリジナルではuse constantを使っていたが、これは駄目である。use constantの「定数」は、実は定数を返すsubなので、sigil ($@%&*)が定数から消えてしまう。何よりqq()でインターポレート出来ないのが痛い。実際19行目では結局定数ではなく文字列リテラルをそのまま使っており、まるで意味なしになっている。悪い例としてこれほど適切なのも珍しい。
Damian先生のお薦めは、Readonlyモジュールを代わりに使う事だ。残念ながら5.8現在ではcoreに入っていないが、5.10では入る公算が大だ。今から使って損はないだろう。それもいやなら、いっそ定数は使わないで済ませた方がましである。私は定数はあまり使わないが、それで困った事はほとんどない。どうしてもというときは明示的に
sub DEBUG { 1 } # DEBUG を定数として使う
としている。
逆にqq()の濫用の例もある。32行目では、$result->{'ClickUrl'}をqq()の中に入れているが、tagと相まって見づらいったらありゃしない(この点からも、Perl 6の->からの.への移行は歓迎)。こういう場合は
printf qq(<li><a href="%s">%s</a></li>), $result->{'ClickUrl'}, $result->{'Title'};
とすれば、ずっと見やすくなるではないか。perlにはqq()だけではなくsprintf()もprintf()もあるということを覚えておこう。もちろん文字列連結演算子としての.もあるし(Perl 6では~)、Here Textもある。これらをどう使い分けるかが腕の見せ所だが、第一優先は「一番読みやすくなる書き方」だと思う。
次にロジック。オリジナルではHTMLの出力の途中でAPIにアクセスし、XMLをパースしてその結果を出力しているが、単に見た目が汚いだけではなく、それだけWeb ServerとCGI Scriptの通信時間が長くなってパフォーマンス的にも悪い。加工処理と出力処理は分けるのが賢明だ。
次に、"en passant"。オリジナル16行目だ。
こういうことはJAPHスクリプトの中だけでやっていただきたい。「私はいかにもPerlを知っています」というようでかっこいいように感じるかもしれないが、それは自意識過剰というものだ。書くなら
# my ($var = $oldvar) =~ s/$regexp/$replacement/ は駄目! my $var = $oldvar; $var =~ s/$regexp/$replacement/;
とすべきである。しかもこの場合、後述のように、この部分は実はURIモジュールを使う事により不要になってしまうのである。
LWPが入っていれば、一緒にURIモジュールが付いてくるのだが、これを使わないというのは不思議である。オリジナルの18-21行目でやっていることは、弾版では26-28行目に登場するが、どちらがわかりやすいだろうか?
わかりにくそうな処理があったら、まずそれをやるモジュールを探してみる。あればそれを使う。なければモジュールにしてCPANに上げる。これがBest Practiceである。
そして、CGIモジュールの活用。$q->param()だけが芸じゃない。折角HTMLの展開にも使っているのであれば、もっともっと活用しよう。
いつ$q->param()を使って、いつuse CGI qw/:standard/を使うかというのは、実はnon-trivialな質問なのだけど、このようにCGIスクリプトの中だけで文字列生成が完結する場合には、use CGI qw/:standard/などを使って余計な$q->を省き、テンプレートなどを使って処理が別になっている場合は、$qを使って処理を渡すというのがきれいかつわかりやすい切り分けだと思う。今回は前者なので、そのようにしてみたらこんなにコードがすっきりした。
ちなみに、このサンプルコードのオリジナルの作者は、Yahooの中の人々である。みなさんは「Yahooでもこれくらい」と安心しただろうか、それとも「もしかしてプロダクションコードもこんな感じ!?」と不安になっただろうか。
#近藤さん、ちゃんと仕事しましょう
Dan the Contributing Writer Thereof

ありがとうございます。追補しました。
これからも読者の声を迅速に繁栄し、ますます反映することを誓います:)
Dan the Typo Generator