目次


作って学ぶ、今どきのWebサービス

第2回 RSSフィードの料理はLWPとXML::RSSにおまかせ

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 作って学ぶ、今どきのWebサービス

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:作って学ぶ、今どきのWebサービス

このシリーズの続きに乞うご期待。

連載第1回は、CPANモジュールのインストールを説明しました。今回から、いよいよPerlプログラミングの世界に入っていきましょう。手始めに、PerlにおけるWebプログラミングの要ともいえるLWP(LibWWW Perl)を用いた、HTTPコンテンツをPerlでHackする手法を解説します。せっかくなのでXMLの扱い方も少々、ということで、比較的扱いやすいRSSも題材にミックスしたいと思います。

LWPの役割

プログラムの中から、あるWebサイトで公開されているコンテンツを利用したい、というケースはよくありますね。HTMLを取得してそこから必要な値を抜き出したり、あるいはWeb上に公開されているXML文書を取ってきて別の形に作り替えたり。リモートのコンテンツを自由自在に操れるようになると、「あるサイトとあるサイトを組み合わせて、もう一つ別のサイトを作り出す」なんてことが実現できるようになります。

そんなとき利用するのがLWPです。LWPはWWW上のデータを処理するためのライブラリの集まりです。その中にLWP::UserAgentなどのモジュールが含まれています*

LWP::SimpleでXML文書を取得

Web上のコンテンツを取得するには、LWP::Simpleを使うのが最も簡単です。例えば、わたしのブログのRSSフィードを取得して、それを標準出力に書き出すにはリスト1のスクリプトでOKです。

リスト1LWP::SimpleでXML文書をGET
1 #!/usr/local/bin/perl
2 use strict;
3 use LWP::Simple;
4
5 my $url = shift;
6 my $document = LWP::Simple::get($url)
7 or die "cannot get content from $url";
8
9 print $document;

以下、リスト1の内容を簡単に解説していきましょう。まず2行目ですが、「use strict」を記述すると、そのスクリプト内では必ず変数を、myやlocalで局所化しなければならなくなります。そのため、バグの少ないきれいなソースを書くための助けになります。「何はなくともまずuse strict」は、基本中の基本です。

次は3行目の「use LWP::Simple;」です。このように、スクリプト内で利用するモジュールは「use<モジュール名>」としてその利用を宣言します。

処理の本体は5~7行目です。コマンドライン引数で受け取ったURLのコンテンツを、LWP::Simpleのgetメソッドで取得します。この戻り値に、リモートコンテンツのドキュメントが丸ごと入っています。

実行すると実行例1のようになります。コマンドライン引数にRSSフィードのURLを指定して実行すると、リモートからRSSフィードを取得して書き出します。

実行例1LWP::SimpleでXML文書をGET
$ perl lwp-sample01.pl http://d.hatena.ne.jp/naoya/rss
<?xml version="1.0" encoding="utf-8" ?>
<?xml-stylesheet href="/naoya/rssxsl" type="text/xsl" media="screen"?>
<rdf:RDF
xmlns="http://purl.org/rss/1.0/"
:
:

RSSフィードって何?

多少順番が前後してしまいましたが、先ほどから何度か出てきているRSSフィードについても簡単に説明しておきます。RSSフィードは、Webサイトに書かれている内容の要約や更新情報が掲載されているXML文書です。主にRSSリーダーと呼ばれる巡回ソフトで読むことや、ほかのアプリケーションからサイトのデータを利用するときに使われることを想定している文書です。ブログツールなどで記事を書くと、自動でRSSフィードが公開されたりします。

RSSフィードはXML文書なので、サイトの記事タイトルや本文の内容などを効率的に抜き出せます。具体例を見てみましょう。リスト2は、わたしのブログのRSSフィード*です。http://d.hatena.ne.jp/naoya/rssから取得できます。

リスト2筆者のブログのRSSフィード(一部省略)
<?xml version="1.0" encoding="utf-8" ?>
<rdf:RDF
xmlns="http://purl.org/rss/1.0/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xml:lang="ja">
<channel rdf:about="http://d.hatena.ne.jp/naoya/rss">
<title>naoyaのはてなダイアリー</title>
<link>http://d.hatena.ne.jp/naoya/</link>
<description>naoyaのはてなダイアリー</description>
<items>
<rdf:Seq>
<rdf:li rdf:resource="http://d.hatena.ne.jp/naoya/20050330/1112136510"/>
:
:
</rdf:Seq>
</items>
</channel>
<item rdf:about="http://d.hatena.ne.jp/naoya/20050330/1112136510">
<title>会社にお泊まり</title>
<link>http://d.hatena.ne.jp/naoya/20050330/1112136510</link>
<description>夜遅かったので会社に泊まることにしました。</description>
<dc:date>2005-03-30T07:48:30+09:00</dc:date>
</item>
:
:
</rdf>

ざっと斜め読みしても、何となく日記そのもののタイトル、各日記の記事タイトルや要約などが含まれているのが分かりますね。このXML文書をパース*すれば、それらを抜き出して利用するプログラムが書ける、というわけです。

XML::RSSでRSSフィードをパース

XML文書のパースの仕方にはいろいろな方法がありますが、ことRSSに関しては、RSSのパースに特化したモジュールであるXML::RSSを使うのが簡単かつ定番です。XML::RSSはCPANからインストール可能です。

先のLWP::Simpleで取得したRSSフィードをXML::RSSでパースして、各日記のタイトルだけを出力するスクリプトを書いてみます(リスト3)。以下、要所を解説していきましょう。

リスト3XML::RSSでRSSフィードをパース
1 #!/usr/local/bin/perl
2 use strict;
3 use Encode;
4 use LWP::Simple;
5 use XML::RSS;
6
7 my $url = shift;
8 my $document = LWP::Simple::get($url)
9 or die "cannot get content from $url";
10
11 my $rss = XML::RSS->new;
12 $rss->parse($document);
13 for (@{$rss->{items}}) {
14   print encode('euc-jp', $_->{title}), "\n";
15 }

3~5行目では、LWP::Simpleに加えてXML::RSS、それから出力の文字コードをUTF-8からEUC-JPに変換するためにEncodeモジュールをuseでロード*します。

少し飛んで11行目、XML::RSSモジュールはオブジェクト指向インタフェースのモジュールなので、まずnewメソッドでインスタンスを生成します。

その後、12~15行目で、XML::RSSオブジェクトのparseメソッドに取得したドキュメントを渡すと、オブジェクトが内部でそれをパースして、その結果を保持します。後はそのオブジェクトが保持しているデータ構造をループで回して必要な値を取得し出力すれば完了です。どうですか?簡単ですね。

スクリプトを実行すると、実行例2のように記事のタイトルが出力されます。別のRSSフィードを指定してみましょう(実行例3)。うまくいきました。RSSフィードであればどんなものでも同様の手順でパースできるので、いろいろなサイトのタイトルでも抜き出すことができるというわけです。

実行例2XML::RSSでRSSフィードをパース
$ perl rss-sample01.pl http://d.hatena.ne.jp/naoya/rss
会社にお泊まり
Netdisaster
スウィングしようぜ
スウィングガールズ
男くささに感動する邦画ドラマの名作
リアルはまぞう
実行例3別のRSSフィードを指定
$ perl rss-sample01.pl http://naoya.dyndns.org/~naoya/mt/index.rdf
WebService::Hatena::Fotolife
組み込み型全文検索エンジンSenna
Web Site Expert #2
Blog Hacks 英語版出る...かも
Class::DBI::Plugin::Connection
:
:

HTTPの条件付きGETって?

さて、モジュールの使い方に慣れてきたところで、もう少し踏み込んだプログラミングをしていきましょう。

LWP::Simpleのgetメソッドでは、与えられたURLのコンテンツをHTTPのGETで取得します。一度コンテンツを取得するだけならそれでも構わないのですが、例えばスクリプトを定期的に実行する場合などは、特定のURLに対して何度もGETを発行することになります。このとき、何度も同じコンテンツを取得しにいくよりは、

  • 相手のサイトが更新されていた場合のみGET
  • それ以外のときは以前のコンテンツをローカルに保存しておいてそちらを参照する

といったことができれば、相手のサイトにかかる負荷が減り、効率が良いですね。

この仕組みはHTTPプロトコルでサポートされていて、「If-Modified-Sinceヘッダによる条件付きGET」なんて呼ばれたりします。

  • サーバ側は、コンテンツがGETされたときにコンテンツと一緒にLast-Modifiedヘッダを送信する
  • クライアント側はHTTPヘッダのIf-Modified-Sinceで、前回取得したときにサーバから送信されたLast-Modifiedヘッダを送信する
  • サーバ側は、If-Modified-Sinceとコンテンツの更新時間を比較して、更新されていなかったら304Not Modifiedとヘッダのみ、更新されていたら200 OKとコンテンツすべてを返す

これによって、サーバ側は同じクライアントには一度だけしか文書を送信しなくて良いので負荷が減るといった仕組みです。要するに同じサイトに何度もコンテンツを取得しにいく場合はIf-Modified-Sinceヘッダを使うのがマナーですよ、ということです。

LWPで条件付きGET

さて、この条件付きGETを使ってコンテンツをGETしてくる方法を考えます。自分でHTTPヘッダに時刻を挿入してローカルにキャッシュして……とか考えるとちょっと面倒臭いですね。しかし、そこはモジュールさまさま。LWPにはこの条件付きGETを行うメソッドもしっかり用意されています。

LWP::Simpleのmirrorメソッドがそれです(リスト4)。第1引数がURL、第2引数が取得したコンテンツの保存先です。先のスクリプトをさらに改造して、条件付きGETをするように変更してみましょう(リスト5)。

リスト4mirrorメソッド
mirror('http://www.example.com/', '/path/to/cache');
リスト5LWPで条件付きGET
1 #!/usr/local/bin/perl
2 use strict;
3 use Digest::MD5 qw(md5_hex);
4 use Encode;
5 use LWP::Simple;
6 use XML::RSS;
7
8 our $CacheDir = '/tmp/rsscache'
;
9 if (! -e $CacheDir) {
10   mkdir $CacheDir or die "cannot create $CacheDir: $!";
11 }
12
13 my $url = shift;
14 my $cache = sprintf("%s/%s.xml", $CacheDir, Digest::MD5::md5_hex($url));
15
16 LWP::Simple::mirror($url, $cache)
17 or die "cannot get content from $url";
18
19 my $rss = XML::RSS->new;
20 $rss->parsefile($cache);
21 for (@{$rss->{items}}) {
22   print encode('euc-jp', $_->{title}), "\n";
23 }

まずは3行目。後ほどURLをMD5形式にするため、MD5を使用します。MD5を利用できるようにするモジュールDigest::MD5をuseでロードしておきます。

8~11行目では、ローカルキャッシュの保存先ディレクトリがなかったら作成しています。14行目では、キャッシュファイルのファイル名を作っています。ファイル名は何でも良いので、ここでは取得する対象のURLからユニークな文字列をMD5で作り、それをファイル名にしています。Digest::MD5モジュールのmd5_hex関数に文字列を渡せば、そのMD5が返ってきます。

16、17行目。いままではgetだったのをmirrorメソッドに変更しました。20行目では、スカラーデータ*をparseに渡していたところを、ローカルに保存したキャッシュに変更しています。このスクリプトを実行すると、先ほどと出力結果はまったく一緒ですが、Webサイトとの通信は最小限にとどめてローカルキャッシュを活用して動作するようになります。仕組みを理解するのがちょっと面倒臭いですが、実装はモジュールのおかげで簡単ですね。

より細かなHTTPクライアントはLWP::UserAgentで

LWP::Simpleはその名前に「Simple」とあるように、シンプルなHTTPクライアント用のモジュールです。もっと細かくさまざまなパラメータを制御したい場合や、オブジェクト指向プログラミングに役立てたいといった場合には、LWP::UserAgentを使います。

例えば、User-Agentやタイムアウトの時間を任意の値に設定し、Proxyサーバを通してHTTPGETを行うには、リスト6のようなコードになります。以下、簡単に解説していきましょう。

リスト6LWP::UserAgentで作ったHTTPクライアント
1 #!/usr/local/bin/perl
2 use strict;
3 use LWP::UserAgent;
4 use HTTP::Request;
5
6 my $request = HTTP::Request->new(GET => 'http://d.hatena.ne.jp/naoya/rss');
7
8 my $ua = LWP::UserAgent->new;
9 $ua->timeout(10);
10 $ua->proxy(['http', 'ftp'], 'http://proxy.example.com:3128/');
11 $ua->agent('UNIX USER Sample Client/0.01');
12
13 my $response = $ua->request($request);
14 if ($response->is_success) {
15   print $response->content;
16 } else {
17   die sprintf("error(%d): %s", $response->code, $response->message);
18 }

まず6行目です。LWP::UserAgentでは、リクエストを送るための方法が幾つか用意されていますが、ここではHTTP::Requestを使った方法で実装しています。まずHTTP::Requestでリクエストのインスタンスを作り、それをLWP::UserAgentのインスタンスに渡す、という手順です。HTTP::Requestでは、ほかにもリクエストヘッダをカスタマイズしたりといったことも可能です。

8~10行目では、LWP::UserAgentのインスタンスを生成して、そこにタイムアウト値などのパラメータを設定しています。ほかにもCookieの値やリダイレクトを処理するかどうかなど幾つかの値が設定可能です。

最後に13~18行目です。6行目で作ったHTTP::Requestインスタンスを指定してLWP::UserAgentでリクエストを行うと、返却値としてレスポンスのオブジェクト(HTTP::Responseのインスタンス)が返ってきます。このオブジェクトに、リクエストが成功したかどうかを尋ねて、成功なら結果を出力、失敗ならエラー内容を、HTTPステータスコードとメッセージとともに出力しています。

動作を細かく制御したい場合は、以上のように、

リクエスト
HTTP::Request
HTTPクライアント
LWP::UserAgent
レスポンス
HTTP::Response

という3つのクラスを使って、プログラミングしていくことになります。

そのほかのHTTPクライアントモジュール

CPANに登録されているHTTPクライアントモジュールにはさまざまなものがありますが、LWP::SimpleやLWP::UserAgent以外にも次に挙げるモジュールが定番です(いずれもLWP::UserAgentのラッパーモジュール*です)。

  • LWP::RobotUA:well-behavedなWebロボット*として振る舞うクライアントを書くためのモジュール(robot.txtなどを解釈して過剰なアクセスを行わないようなクライアントを簡単に書ける)
  • WWW::Mechanize:リンクをクリックして、1つのサイトの中を遷移していった結果を取得するような場合に利用するモジュール。フォーム認証付きのページの奥にあるコンテンツや、セッションで管理されているページのコンテンツを取得するのに役立つ

次回は

ここまでで、XML::RSSによるRSSの料理方法を解説しました。ではRSS以外のXML文書を料理する場合にはどうしたら良いのでしょう?次回はそのあたりについて解説します。

このページで出てきた専門用語

LWP::UserAgentなどのモジュールが含まれています
LWPはPerlの標準モジュールなので、CPANからインストールする必要はありません。
筆者のブログのRSSフィード
なお、はてなダイアリーのRSSにはXSLTが適用されているので、ブラウザで見ても通常のページのように見えますが、内容はXML文書で記述されています。
パース
文法に沿ってデータを分析・分解すること。
Encodeモジュールをuseでロード
Perlのバージョンが5.6系以下の場合はEncodeモジュールではなくJcodeモジュールを使います。
スカラーデータ
構造を持たないプレーンなデータのこと。ここでは文字列データを指しています。
LWP::UserAgentのラッパーモジュール
LWP::UserAgentモジュールを内部的に使用して、必要な機能のみ提供するモジュールのこと。ラップは「くるむ」の意味。
well-behavedなWebロボット
行儀の良いWebロボットという意味。Webを巡回するために作られたソフトウェアでは、robots.txtを確認して、Web管理者の意図しないファイルにアクセスしないようにしなければいけません。Webロボットの中にはrobots.txtを無視するものもありますが、それは行儀が悪いとされています。

ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development, Open source, Linux
ArticleID=247526
ArticleTitle=作って学ぶ、今どきのWebサービス: 第2回 RSSフィードの料理はLWPとXML::RSSにおまかせ
publish-date=03302007