Perl Hackers Hub

第22回Coroを使ったやさしいクローラの作り方(2)

前回の(1)こちらから。

参考にすべきCPANモジュール

以下では古いものから新しいものまで、クローラを作る際に参考になるCPANモジュールを紹介します。

LWP::RobotUA、WWW::RobotRules

PerlにおけるHTTPクライアントと言えば、何はともあれLWP::UserAgentです。LWPのパッケージの中にはLWP::RobotUAというrobots.txtを解釈するbot用のUserAgentを作るクラスが含まれています。RobotUAはLWP::UserAgentを継承したクラスで、ユーザエージェント文字列とFromヘッダに使用するためのメールアドレスの指定が必須になっています。また、robots.txtでDisallowの指定があるURLにはアクセスできないようになっていたり、リクエストごとにデフォルトで1分のウェイトが入っていたりと、行儀の良いモジュールです。

WWW::RobotRulesはrobots.txtのパーサで、LWP::RobotUA内で使われています。LWPとは独立しているので単体で利用することもできます。自身のユーザエージェント名から、特定のURLへのアクセスが許可されているかどうかを判別するメソッドを備えています。

URI::Fetch

URI::Fetch は、gzip 圧縮や、HTTPヘッダのLast-ModifiedとETagによるキャッシュをサポートした、良い感じのダウンロード機能を提供してくれるモジュールです。

LWPx::ParanoidAgent、LWPx::ParanoidHandler

LWP::ParanoidAgent は、localhost や192.168.x.x などのプライベートIPアドレスに対するリクエストが失敗するユーザエージェントを作ってくれるモジュールです。ユーザからのリクエストを受けて任意のURLにリクエストを発行するようなサービスを運用する場合、そのWebアプリケーションを踏み台にして、本来ならば外部からアクセスできないサーバにアクセスして内部情報を漏洩させたり、攻撃のためのヒントを得たりするといったことが考えられます。このモジュールはそういったリクエストを防いでくれます。

LWPx::ParanoidHandlerは、tokuhiromさんによるLWPx::ParanoidAgent の再実装です。DNSDomain Name Systemのルックアップ処理をNet::DNS::Paranoidに切り離し、フック処理によってエラーレスポンスを返すようにしています。何が起こるかわからない多重継承を行うことなく、既存のLWP::UserAgentにプライベートネットワークへのブロック機構を加えることができます。

スクレイピングのためのモジュール

WWW::Mechanizeは、人間がブラウザ上で行う操作を自動化することを主眼としたモジュールです。Cookieのサポートやフォームの送信、リンクのクリックなど、よりブラウザに近い振る舞いでWebページを取得できます。

Web::Scraperは、XPathやCSSセレクタでWebページからのスクレイピングを行うことができるモジュールです。Web::Scraper は、URL やHTTP::Response、HTML文字列など多様な入力に対応しているので、WWW::Mechanizeと組み合わせて使うことも容易にできます。たまに使おうとするとWeb::ScraperのDSLDomain Specific Languageドメイン特化言語)を忘れてしまうのが欠点です。

Web::Queryは、jQuery風のメソッドでスクレイピング行えるモジュールです。CSS3セレクタを使ってHTMLから特定のタグや属性を抜き出せます。

CPANモジュールとLWPとの関係

CPANにはこういった特定の目的のためのHTTPクライアントが多数あり、多くのライブラリがHTTPクライアントとしてLWPを使っています。

LWPを利用するHTTPクライアントには、主に2種類の傾向が見られます。

  • LWP::UserAgentを継承して作られているもの
  • デフォルトでLWPを使用するが、パッケージ変数やアクセサ経由で(ua、user_agentといった名前が使われます)HTTPクライアントを差し替えられるようになっているもの

紹介したものの中では、LWP::RobotUA、LWPx::ParanoidAgent、WWW::MechanizeがLWP::UserAgentを継承して作られています。LWP::UserAgentを継承して作られたモジュールは完全にLWP::UserAgentのメソッドを備えているので、そのままLWP::UserAgentのDrop-in replacement[4]として利用できます。たとえばURI::FetchやWeb::Scraperで使用するユーザエージェントを、LWP::RobotUAやLWPx::ParanoidAgentに差し替えることができます。

LWP::UserAgentを使用しているモジュールがLWP::UserAgentに期待している機能は主に、

  • requestメソッドにHTTP::Requestオブジェクトを渡すとHTTP::Responseオブジェクトが返ってくる
  • get、head、postメソッドを備える

といったものです。

ですので上記の要件を満たしていれば、おおよそLWP::UserAgentの使用個所を置き換えることができます。FurlやHTTP::Tinyなどの、依存モジュールが少なく軽量高速でLWPの代用となるモジュールが存在しているため、LWP::UserAgentの替わりにこれらのモジュールを使いたいという需要もあるでしょう。簡単なラッパを書いてやれば、LWP::UserAgentの替わりにFurlを使うように置き換えることができます。

しかしHTTP::RequestやHTTP::Responseを生成する処理がヘビーなので、実際のところLWPとの互換性を高めようとすると、Furlを使うメリットの多くが失われてしまいます。LWPの備えているメソッドやフックポイントをすべて実装すると、それはLWPの再発明になり、LWP同様に複雑なものになってしまうでしょう。そういったわけで、LWPは引き続きデファクトスタンダードとなっています。特に速度にこだわっていなければ、引き続きLWPを使うのがよいでしょう。

透過的なキャッシュをしよう

「Webページからコンテンツを取得して、リンクを抽出してダウンロード」といったちょっとしたコードを書く場合でも、何度か試行錯誤をしながら作ることになるでしょう。そんなときはすべてのリクエストに対して自動的にキャッシュしてくれるような、透過的なキャッシュ機能があるモジュールを使うと便利です。ただし、HTTPクライアントでのキャッシュと言ったときに、モジュールによってはキャッシュの意味合いが違うことがあるので、何を意図したキャッシュなのか確認したほうがよいでしょう。

たとえばLWP::UserAgent::WithCacheにおける「キャッシュ」は、HTTPヘッダのExpiresやLast-ModifiedやEtagを利用してリクエストを発行するかどうかを決めるものです。Expiresが現在時刻より先、つまりサーバ側のHTTPレスポンスで「この時刻まではコンテンツを取りに来なくていいよ」と明示的に指定されていない限り、リクエストが発行されます。リクエストが発行される場合にはIf-Modified-SinceとIf-None-Matchヘッダがセットされ、変更がなければ(サーバ側が対応していれば)軽量な304 Not Modifiedレスポンスが返ります。

これはHTTPのキャッシュ機能としては正しいのですが、相手サーバのレスポンスが遅ければ毎回待ち時間が発生することになりますし、開発中のアプリケーションや高速化のためのキャッシュ機能としては あまりうれしくありません。また、任意のURLに対してリクエストを発行できるような性質のWebアプリケーションの場合にも、相手先のサーバに連続アクセスできることになってしまいます。

一定時間は強制的にキャッシュする

開発中や高速化のためのキャッシュはHTTPレスポンスのExpiresヘッダにかかわらず強制的にキャッシュして、一定期間はリクエストを発行しないようにする必要があります。自前でHTTP::Responseオブジェクトをmemcachedなどにキャッシュするか、あるいはいくつかのモジュールには「強制的にキャッシュする」機能が備わっているので、そういったものを利用するのがよいでしょう。URI::FetchのNoNetworkオプションやLWP::UserAgent::Cachedが、オフラインでのアクセスを目的としたキャッシュになっています。ネットワークアクセスが発生しないので何度リトライしても相手のサーバに迷惑をかけることがなくなりますし、開発も高速になります。

CPANモジュールの中には、似たような名前でも意味合いが違ったり、実装方法のアプローチが違うといったものが多くあります。ソースやドキュメントを参照して利用目的に合ったものかどうかを確認するとよいでしょう。

<続きの(3)こちら。>

おすすめ記事

記事・ニュース一覧