Web ServicesがWWWの仕組みに加わったのは、最近のことです。興味深いWebアプリケーションの多くは、クライアントとサーバーの間で行き来する要求 / 応答形式による、従来型のCGIプログラムとして構築されています。サービス・アーキテクチャーを利用することにより、既存のCGIプログラムの機能をWeb serviceとして使用することができます。ただし、アプリケーションを書き直したのでは、あまりに面倒であり、また費用も高くつきすぎます。1つの簡単な裏技として、Web serviceにWebページ自体の読み取りと構文解析を行わせるというやり方があります。この「ページはぎ取り」技法は、最近のプログラマーのツールボックスに入っている最も便利な道具であり、この記事のテーマともなっています。
この記事は、読者がPerlのプログラミングについてある程度の知識をもっていることを想定しています。私が以前に書いたPerlでのSOAP::Liteの使用 をご覧ください。その記事では、Perl用のSOAP::Lite モジュールを使用してWeb serviceをプログラミングする方法を説明しています。
図1: Slashdotの投票
もう少し具体的な話をするために、Slashdotの毎週の投票 (図1) に関するWeb serviceを定義しましょう。このサービスを使用すると、プログラムは、最新の投票トピック (および選択肢) を入手し、現時点の投票結果を検索し、特定の選択肢に投票することができます。このAPIは表1 にまとめてあります。
表1: Slashdot Web serviceのAPI
| URL: | http://marian.daisypark.net/~jjohn/slashdot_poll.pl | ||
|---|---|---|---|
| メソッドAPI | |||
| メソッド名 | 入力 | 出力 | 説明 |
getPollTopic
| なし | 構造体を戻します | トピック、投票ID、および投票選択肢を戻します。エラーが発生した場合には、"error" という特殊なメンバーがゼロ以外の値になります。 |
vote
| pollIDおよびchoiceID | 成功または失敗を表すブール値 | SlashDotの投票トピックと選択肢を表すストリングが指定されると、このメソッドは投票を試みます。 |
getPollResults
| pollID | 構造体 | pollIDが指定されると、2エレメントの配列に選択肢をマップする構造体が戻されます。この配列の最初のエレメントは、その選択肢に投じられた票の数であり、2番目のエレメントは、その選択肢が取得した合計票数のパーセンテージです。 |
XML-RPCと同じように、SOAPは任意の複雑なデータ構造体を戻すことができます。SOAPのほうがデータ・タイプのコレクションが豊富ですが、普通に思いつくすべてのタイプ (ストリング、整数、リスト、およびディクショナリー) が表現できます。また、XML-RPCの場合と同じように、プログラマーは、基礎になるXMLに関して何もする必要はありません。
これらの3つのメソッドは、概念的には単純なものです。getPollTopic() 関数は、Slashdotがこの投票トピックを識別するためにデータベース内で使用しているラベルを含めた、他の2つのメソッドで重要なすべての情報を戻します。人間が読むことのできる形式の投票選択肢をそれに対応するSlashdot IDにマップするディクショナリーも、このメソッドによって戻されます。投票トピックのIDと個々の選択肢のIDは、ともにvote() メソッドで必要なものです。このメソッドが実際に投票をSlashdotに登録します。最後に、getPollResults() が現行の投票の状態を報告します。
このサービスを作成する上で最も難しい部分は、HTMLの構文解析に関するもので、SOAPとは無関係です。リスト1の行4-7で、新しいSOAPディスパッチ・オブジェクトが作成されます。ここで発行されるクラスはSlash と呼ばれ、SOAP要求をディスパッチする同じスクリプト内に実装されます。
リスト1: Slashdot投票サーバー
1 #!/usr/bin/perl
2 # Wrap a CGI script into a web service
3 use strict;
4 use SOAP::Transport::HTTP;
5
6 my $dispatch = SOAP::Transport::HTTP::CGI->dispatch_to('.', 'Slash');
7 $dispatch->handle;
8
9 package Slash;
10 use HTML::TreeBuilder;
11 use LWP;
12 use constant BASE_URL => 'http://slashdot.org';
13
14 sub getPollTopic {
15 my $homepage = get_page();
16 return { error => 1 } unless $homepage;
17
18 my $tree = HTML::TreeBuilder->new;
19 $tree->parse($homepage);
20
21 my %poll = (); # to be returned
22
23 for my $td ($tree->look_down("_tag", "td") ){
24 if( my $form = $td->look_down("_tag", "form") ){
25 if($form->attr("action") eq 'http://slashdot.org/pollBooth.pl' ){
26
27 # get the pollID
28 if( my $input = $td->look_down("_tag", "input")){
29 if( $input->attr("name") eq 'qid' ){
30 $poll{pollid} = $input->attr("value");
31 }
32 }
33
34 # get the poll topic
35 my $b = $td->look_down("_tag", "b");
36 $poll{topic} = $b->as_text;
37
38 # the poll options
39 for my $i ( $td->look_down("_tag", "input")){
40 next unless $i->attr("name") eq "aid";
41 my $label = ($i->right)[0];
42 my $aid = $i->attr("value");
43 $poll{options}->{$label} = $aid
44 }
45 }
46 }
47 }
48 return \%poll;
49 }
50
51 sub vote {
52 my ($class, $pollID, $choice) = @_;
53 if( get_page("/pollBooth.pl?aid=$choice&qid=$pollID") ){
54 return SOAP::Data->type( Boolean => 'true' );
55 }else{
56 return SOAP::Data->type( Boolean => 'false' );
57 }
58 }
59
60 sub getPollResults {
61 my ($class, $pollID) = @_;
62
63 my %choices;
64 if( my $content = get_page("/pollBooth.pl?qid=$pollID&aid=-1")){
65 my $tree = HTML::TreeBuilder->new;
66 $tree->parse( $content );
67 my $last;
68 for my $td ($tree->look_down("_tag", "td") ){
69 if( my $i = $td->look_down("_tag", "img") ){
70 if( $i->attr("src") =~ /leftbar.gif$/ ){
71 chop $last;
72
73 # point to a list ( actual votes and percentage )
74 $choices{ $last } = [ split( m!\s+/\s+!, $td->as_text, 2) ];
75 }
76 }
77 $last = $td->as_text;
78 }
79
80 }else{
81 return {error => 1};
82 }
83 return \%choices;
84 }
85
86 # Helper function to get pages in a more robust way
87 sub get_page {
88 my ($path) = @_;
89 my $ua = LWP::UserAgent->new;
90 my $uri = URI->new_abs($path, BASE_URL);
91 my $rq = HTTP::Request->new( GET => $uri );
92 my $rs = $ua->request($rq);
93
94 return ($rs->is_error ? undef : $rs->content );
95 }
|
行9でSlash クラスが開始されます。ところで、Perlクラスの作成に慣れていない人は、オンライン・ドキュメンテーション・ツールperldoc を使用して、Tom Christiansenのオブジェクト指向チュートリアルを検索してください (キーワードは "perltool" です)。Slashdotページを取り出して構文解析するには、LWP およびHTML::TreeBuilder モジュールをパッケージ宣言の後に組み込む必要があります。(これらのモジュールについては、すぐ後で説明します。)行12は、変更不能な定数を作成するためのPerlのメカニズムを示しています。定数を作成するには、ファイルを有効範囲とする字句変数を宣言したり、パッケージ・グローバルを使用したりするのではなく、この方法を使用するようにしてください。実行時に定数が誤って変更されることを望むようなプログラマーはいないはずです。
このクラスはほとんどの標準オブジェクト・クラスとは異なり、そのすべてのメソッドがクラス・メソッドになっています。オブジェクトごとの情報がないため、コンストラクター・メソッド (多くの場合、new() と呼ばれます) はありません。Perlは、呼び出されたメソッドに常にクラス名またはオブジェクト参照子を渡すため、すべてのメソッドのパラメーター・リストの先頭に引き数が1つ追加されます。
最初のメソッドgetPollTopic() は、LWP ライブラリーを使用してSlashdotのフロントページのコンテンツを取得します。このライブラリーには、HTTPを介してデータを入手するために使用される、多くのヘルパー・クラスが含まれています。LWP を使い慣れていない人は、O'ReillyのWeb Client Programming with Perl を参照してください。本書は絶版になっていますが、O'ReillyのOpen Bookサイトから入手することができます (参考文献を参照してください)。作業を単純化するために、すべてのLWP コードは、行87から始まるget_page() サブルーチンに集中しています。このサブルーチンは、URLの最後の部分だけを受け取ります。このクラスはSlashdotからのページだけを取得するため、この関数の外側で完全なURLを構成しようとすると、多くのコードを反復させる必要があります。私たちはURIメソッドnew_abs を使用して、get_page() に入れて渡された追加パス情報に基づいて絶対URLを作成します。
getPollTopic に戻ると、フロントページがHTML::TreeBuilder によってツリーに似た構造に構文解析されます。これはHTMLを分解するための最速の方法ではありませんが、堅固で使いやすいライブラリーであり、学習するだけの価値はあります。新しいツリー・ビルダーが行18で作成され、行19でSlashdotホーム・ページのHTMLコンテンツが送り込まれます。
HTML::TreeBuilder は、HTMLをタグでグループ化していますので、プログラマーがlook_down メソッドを使用して、現行の有効範囲の中にある、HTMLタグに対応するオブジェクトのセットについて反復することができます。行23で、有効範囲はSlashdotのフロントページのすべてのHTMLコンテンツになっています。投票トピックに関する情報は<TD> タグに入っています。look_down は、このようなタグを検索するたびに、開始<TD> タグから終了<TD> タグまでの間のすべてのHTMLが有効範囲に含まれる、新規HTML::TreeBuilder オブジェクトを戻します。行23で始まるループは、action属性がpollBooth.pl プログラムを指しているFORM要素を含む<TD> タグを検出しようとしています。(HTMLでコンテンツだけが記述され、レイアウトは含まれていなければ良いとは思いませんか?)HTMLの構文解析の有効範囲が、行23での文書全体から<TD> タグの内容の検索だけに変更されたことに注意してください。
正しいテーブル・データのエレメントが突き止められると、投票トピックIDは容易に抽出することができます。HTMLのこのセクションには、qid というname 属性の付いた、隠れた入力エレメントを含むフォームがあります。このエレメントのvalue 属性は、この投票トピックを識別するためにSlashdotが使用するキーです。HTML::TreeBuilder メソッドattr を使用することにより、行30でこのname 属性の値が抽出され、%poll というハッシュに保管されます。
投票トピックの検索も簡単に行えます。このセクションで太字になっているテキストは投票トピックだけだからです。$td の現在の有効範囲についてlook_down メソッドを使用すると、唯一の<B> エレメントが突き止められます。as_text メソッドは、現在の有効範囲のすべてのコンテンツを、マークのないプレーンASCIIテキストに変換します。行36で行われているように、これは$b 有効範囲からトピックを抽出するための完全な方法です。
リスト2: Slashdot投票オプションのHTMLの例
<BR><INPUT TYPE="radio" NAME="aid" VALUE="3">Heat Ray Vision
|
getPollTopic の最後のタスクは、最も手が込んでいます。すべての投票選択肢は、各選択肢について投票する必要のあるHTMLとともに抽出する必要があります。HTMLによる選択は、ラジオ・ボタンによって行われます (例についてはリスト2 を参照)。この選択肢に投票するための値は、name属性がaidになっている<input>タグ内にあります。人間が読むことのできる選択肢は、その<input>タグのすぐ右にあります。正しい<input> タグの検出は、単にそれらのリスト全体を反復して希望するname 属性を備えたタグを見付けることによって行われます。この<input> タグの右にあるテキストを入手するためには、right メソッドを使用します。このメソッドは、HTMLタグを表すHTML::TreeBuilder オブジェクトのリスト、または単なるマークアップされていないテキストのストリングのリストを戻します。人間が読むことのできるテキストは、常に、<input> タグの右側にある最初のエレメントになります。行43では、人間が読むことのできる選択肢ラベルを、その選択肢への投票を登録するために必要な値にマップするハッシュが作成されます。行43はハッシュのハッシュを作成しています。複雑なPerlデータ構造になじんでいない人は、Perlに同梱されているPerl Data Structures Cookbook を参照してください。これはperldoc perldsc コマンドを使用して見ることができます。
現在の投票トピックに関するこうしたすべての情報がハッシュに集められると、これらの情報を呼び出し元に戻すことができます。行48では、この%poll ハッシュへの参照を戻しています。すべてのSOAPメソッドが単一の値を戻すことになりますので、これがハッシュをスカラーに詰め込むための最も自然な方法です。
APIメソッドの残りの部分は非常に簡単です。投票するためには、Slashdotは投票トピックIDと選択肢番号を必要とします。これらは、SlashdotのpollBooth.pl プログラムに対するGET 照会として発信することができます。APIメソッドvote は、ユーザーから受け取ったこれらの両方の値を、Slashdotに発信されるURLにパッケージします。vote は2つの引き数だけしか受け入れませんが、$class という3番目の引き数があることに注意してください。これはメソッドですので、Perlがパラメーター・リストにクラス参照を追加しています。
ページ要求の成否は、ブール値で呼び出し元に戻されます。SOAP::Lite は、直接的なPerlマッピングを行わないSOAPデータ型を簡単に作成できる機能を備えています。SOAP::Data->type 呼び出しを使用すると、(リスト1 の行54および56のように) 任意のSOAPデータ型を作成することができます。
リスト3: 投票結果に関するHTMLの例
<TR>
<TD width="100" align=right>Heat Ray Vision </TD>
<TD width="450"><NOBR>
<IMG src=http://images.slashdot.org/leftbar.gif width="4" height="20" alt="" />
<IMG src=http://images.slashdot.org/mainbar.gif height="20"
width="4" alt="0%">
<IMG src=http://images.slashdot.org/rightbar.gif width="4" height="20" alt="" />
256 / <FONT color=006666>0%</FONT></NOBR>
</TD>
</TR>
|
最後のメソッドgetPollResults は、投票結果を報告するためにSlashdotが使用する方法の影響で複雑になっています。リスト3 は、1つの投票選択肢に関する結果を表示するHTMLを表しています。この選択により受け取る、人間が読むことのできる選択肢ラベル、実際の投票数、およびこの選択肢の得票パーセンテージのすべてを記録する必要があります。残念なことに、人間が読むことのできるストリングは、ランダム・テキストではなく投票選択肢であることを表すなんらかの明確な標識の前に送られてきます。したがって、リスト1 の行68-77のコードでは、すべての<TD> タグを探し、最後に見付かったものを$last 変数に記憶しています (行77)。すべての選択肢の後には、グラフが続いていますので、<TD> にleftbar.gif というイメージがあれば、その前の<TD> に人間が読むことのできるテキストが含まれていることは明らかです。
小細工はこれだけではありません。実際の投票数およびパーセンテージは、1つのストリングとして抽出されます (行74)。このストリングが、これらの値を分けるスラッシュで分割され、2つのエレメントを含むリストが作成されます。このリストが無名配列に戻され、%choices ハッシュに取り込めるようになります。他の場合と同じように、このハッシュは参照として呼び出し元に戻されます。
このAPIを利用するWebクライアントは、簡単に作成することができます。リスト4 は、上記のSOAPサービスを使用する単純なCGIスクリプトです。多くのCGIプログラムと同じように、このスクリプトは複数の状態を取り扱います。ユーザーが最初にこのプログラムを実行したときには、図2 と非常によく似た画面が表示されます。この画面には、トピックが表示され、また投票選択肢がドロップダウン・メニューとして表示されます。
図2: 初期投票画面
リスト4 の行17-20は、CGIパラメーターaction の値に応じてpaint() またはvote() (APIメソッドvote とは混同しないでください) を実行するスイッチ・ステートメントです。もちろん、このプログラムを最初に呼び出したときにはパラメーターは設定されていませんので、paint() サブルーチンが呼び出されます。これにより、図1 のような画面が表示されます。
paint() で最初に注意すべきことは、このサブルーチンが初期化CGIオブジェクト、および (おそらくは) HTMLのフラグメントを受け取るということです。これらは、それぞれ、ローカル変数$cgi および$mesg に保管されます。投票情報を入手するために、行24でgetPollTopic へのSOAPクライアント呼び出しが開始されています。タイピング・エラーを避けるために、SOAP URI値とプロキシー値の両方が定数に保管されています。行27はリモート・メソッドを呼び出します。$resp をgetPollTopic の戻り値と考えると便利です。このオブジェクトは、どこかにその戻り値を保管していますが、SOAP呼び出し中に発生した伝送エラーに関する情報も保管しています。$resp のfault() メソッドを呼び出すと、これらのエラーの有無を調べることができます。障害が発生している場合には、faultcode() メソッドおよびfaultstring() メソッドを使用して、障害に関する追加情報を判別することができます。エラーがない場合には、行35に示すように、result() メソッドを使用してgetPollTopic の戻り値を抽出することができます。
このAPIメソッドがハッシュへの参照を戻すということを思い出してください。投票選択肢をリストするドロップダウン・メニューを作成するために、新しいハッシュが作成されます。この新規ハッシュはその投票値を人間が読むことのできるラベルにSlashdot値をマップします。これは直観に反するように思えますが、行52で使用されているCGIのpopup_menu() で使用される構造です。
行41は、投票フォームを作成するために必要なすべてのHTMLエレメントの印刷を開始します。このサブルーチンにメッセージが渡されると、このメッセージは投票フォームの後に表示されます。
図3: 投票結果
Web訪問者が選択肢を選択してサブミット・ボタンを押すと、CGIスクリプトがvote() サブルーチンを呼び出します。このサブルーチンはvote とgetPollResults の2つのAPIメソッド呼び出しを行いますので、やや高くつきます。つまり、2つのHTTP呼び出しと、それらの呼び出しによって引き起こされるすべてのネットワーク待ち時間が発生します。多くのエラー検査がある以外は、このコードは比較的簡潔です。行100では投票結果のハッシュを入手し、視覚に訴える表を作成します。この表は、paint() プログラムに渡されて表示されます。
リスト4: CGI投票クライアント
1 #!/usr/bin/perl
2 # a web client for the slashdot poll client
3 use strict;
4 use SOAP::Lite;
5 use CGI qw/:all *table/;
6 use CGI::Carp qw/fatalsToBrowser/;
7
8 use constant SOAP_URL =>
9 'http://marian.daisypark.net/Slash';
10 use constant SOAP_PROXY =>
11 'http://marian.daisypark.net/~jjohn/slashdot_poll.pl';
12
13
14 my $cgi = CGI->new;
15 my $action = $cgi->param("action");
16
17 for($action){
18 /^vote/ && do{ vote( $cgi ); last; };
19 paint($cgi);
20 }
21
22 sub paint {
23 my ($cgi, $mesg) = @_;
24 my $client = SOAP::Lite->uri(SOAP_URL);
25 $client->proxy(SOAP_PROXY);
26
27 my $resp = $client->getPollTopic();
28
29 if( $resp->fault ){
30 die
31 "ERROR: SOAP Failure: ",
32 $resp->faultcode, ":",
33 $resp->faultstring;
34 }
35 my $poll = $resp->result();
36 my %menu_options;
37 while( my($k, $v) = each %{ $poll->{options} } ){
38 $menu_options{$v} = $k;
39 }
40
41 print
42 header,
43 start_html( -title => "Slash Poll Proxy",
44 -bgcolor => "#FFFFFF",
45 ),
46 h1("Slash Poll Proxy"),
47 p("The current poll topic is: ", b($poll->{topic})),
48 p("Cast your vote by selecting one of the following:"),
49 start_form,
50 qq(<input type="hidden" name="action" value="vote">),
51 qq(<input type="hidden" name="pollID" value="$poll->{pollid}">),
52 popup_menu(
53 -name => 'choice',
54 -labels => \%menu_options,
55 -values => [ keys %menu_options ],
56 ),
57 submit,
58 end_form,
59 hr,
60 $mesg,
61 end_html;
62 }
63
64 sub vote {
65 my ($cgi) = @_;
66 my $choice = $cgi->param("choice");
67 my $pollid = $cgi->param("pollID");
68
69 if( !$choice || !$pollid ){
70 return paint($cgi, font({color=>"#FF0000"},
71 "Error! Vote again"));
72 }
73
74 my $client = SOAP::Lite->uri(SOAP_URL);
75 $client->proxy(SOAP_PROXY);
76
77 # vote
78 my $resp = $client->vote($pollid, $choice);
79
80 if( $resp->fault ){
81 die
82 "ERROR: SOAP Failure: ",
83 $resp->faultcode, ":",
84 $resp->faultstring;
85 }
86
87 unless( $resp->result ){
88 return paint($cgi, font({color=>"#FF0000"},
89 "Vote failed! Vote again"));
90 }
91
92 # Get the results
93 my $resp = $client->getPollResults($pollid);
94
95 if( $resp->fault ){
96 return paint($cgi, font({color=>"#FF0000"},
97 "Can't get results"));
98 }
99
100 my $results = $resp->result();
101 my $ret = start_table;
102 for my $r ( keys %{$results} ){
103 $ret .= Tr(td(
104 [$r, b($results->{$r}->[0])]
105 )
106 );
107 }
108 $ret .= end_table;
109 return paint( $cgi, $ret);
110 }
|
SOAPはRPCメカニズムのようにして使用することができますが、本来の長所と将来性は、それが持つオブジェクト指向の性質から来るものです。すべてのオブジェクトは、固有データを保管して特定のインスタンス化をすることができます。つまり、Webオブジェクトがプログラム状態を記憶することができるのです。SOAPはプラットフォームに関して中立ですので、クライアントのPerlスクリプトで、Python SOAPサーバー内のクラス・データを変更するオブジェクトを作成することができます。SOAPがどの程度までOOPの概念を実装できるのかは今後を見なければなりません。。SOAPオブジェクトからサブクラスを派生させることはできるのでしょうか。継承についてはどうでしょうか。適度な長さの継承ツリーであっても、いくつかのWebサーバーを横断する必要がありますので、プログラマーは、SOAP呼び出しが戻るまでかなり長時間待たなければならないことがあります。いずれにしても、SOAPの未来は興味津々です。
- SOAP::Liteモジュールの紹介については、developerWorksのPerlでのSOAP::Liteの使用 を参照してください。
-
Quick Guide to SOAP::Lite は、このPerlライブラリーの使用方法を学習するための格好の手段です。
-
SOAP仕様をご自分で読むことが何よりです。
-
perl.com で、作者であるPaul Kulchenkoによる
SOAP::Liteの手引きを読んでください。 -
O'Reillyのフリー・ブックをお勧めします。特に、Web Client Programming with Perl はぜひ読んでください。
-
ZOPE はオープン・ソースのアプリケーション・サーバーで、Perlのインターフェースも用意されています。
Joe Johnston (jjohn@cs.umb.edu) は、昼間はO'Reilly and Associatesでプログラマーとして勤務しています。愛猫がキーボードの上にいないときには、Perl Journal、use.perl.org、www.perl.com、およびO'Reilly Networkのために記事を書いています。また、Michael Lordと協力して、ユーモラスなUFO伝説のサイトAliens, Aliens, Aliens も作成しました。