Perl Hackers Hub

第32回新人さんのためのPerl入門(2)

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

Perlのコーディング規約

いざ学んだPerlをアウトプットしようとしたときには、Perlの自由な記述に戸惑うことがあるでしょう。ここではコーディングスタイルの一例として「Perl style guide」を、そしてコーディングをサポートするモジュールとしてPerl::CriticとPerl::Tidyを簡単に紹介します。

perlstyleでコーディングスタイルを参考にする

Perlのコードをどのように書くとよいのかの一例として、Perldoc内にPerl style guideという形で紹介されています。ターミナルからperldoc perlstyleで参照でき、インデントやスペースの置き方、ドキュメントの書き方の指南が提供されています。

perlstyle内でも書かれているように、次の2つは必ずコードの冒頭に書いてください。

use strict;
use warnings;

文法チェックと警告が有効になり、変数の宣言忘れやタイプミスなどについて警告を出してくれます。これらの警告を受け付けたくないときは、そのときだけ例外的にno strict、no warningsを使います。

Perl::CriticでPerlベストプラクティスに沿っているかを確認する

『Perlベストプラクティス』注1という書籍ではPerlのベストプラクティスを複数紹介しています。Perl::Criticは、Perlのコードがそのベストプラクティスに沿っているかをチェックしてくれます。インストールすると、perlctiricファイル名でチェックができます。ホームディレクトリ以下に作成される.perlcriticを編集することで、細かくルールを定義できます。

Perl::Tidyでコードを整形する

Perl::Tidyは、ソースコードをルールに沿って整形してくれます。インストールすると、perltidy ファイル名で実行できます。たとえばperltidy -i=4 -nsfs ファイル名とすると、インデントは4スペースで、セミコロンの前のスペースが除去されて出力されます。Perl::Criticと同様に.perltidyrcにルールを定義しておくと便利です。

PerlでWebアクセスをする

Perlではモジュールを使いこなせば、巨大なWebシステムからコマンドラインツールまで簡単に実装できます。その中でもHTTP通信は、モジュールでとても簡単に実装できます。特にプログラミング初心者にとっては、実際にWebサイトやWeb APIから情報を取得することで、できることの幅が広げられるでしょう。PerlでHTTP通信をするためのモジュールでお勧めなのは、本節で紹介するLWP::UserAgentとFurlです。

LWP::UserAgentでHTTP通信をする

LWPはPerlでWebアクセスするための各種モジュールの集まりです。LWP::UserAgentはその中の一つで、ユーザエージェントを実装するクラスです。

リスト1はLWP::UserAgentを利用してWebページをダウンロードします。

リスト1 lwp.pl
1:  use LWP::UserAgent;
2: 
3:  my $ua = LWP::UserAgent->new(
4:     agent => "PerlHackersHub_32/1.0",
5:     timeout => 10,
6: );
7: my $url = "http://gihyo.jp/dev/serial/01/perl-hackers-hub";
8: my $res = $ua->get($url);
9: print $res->content;

モジュールは1行目のようにuseで指定して、3~6行目でLWP::UserAgentオブジェクトのインスタンスを作成し$uaに代入しています。4~5行目ではユーザエージェントとタイムアウトを指定しています。GETを使うために8行目ではアロー演算子を用いてgetメソッドを呼び出し、取得先を指定します。HTTP::Responceが返ってくるので変数$resに格納し、htmlを表示させたいので$res->contentのようにcontentメソッドを使い、printで表示させます。

実行すると図1のようにHTMLを取得できます。同様にしてステータスコードやヘッダも取得できるので、次のように出力を追加してみます。

print $res->code."\n";
print $res->message.'\n'; # 間違えた!
print $res->header('Content-Type')."\n";
図1 lwp.plの実行結果
$ perl lwp.pl
<!DOCTYPE html>
...
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=UTF-8" />
<title>Perl Hackers Hub:連載 gihyo.jp … 技術評論社
(省略)

Perlでは文字列の連結に.を使うので、改行を表す\nをダブルクオーテーションで囲んだものと連結すると、出力時に1行ずつ改行されて表示されます。

\nをシングルクォーテーションで囲ってしまうと改行されずに\nをそのまま表示してしまうので、シングルクォーテーションとダブルクオーテーションの使い分けに注意してください。

また、print文ごとに\nを付けずに特殊変数$\\nを代入する方法もあります。

$\ = "\n";
print $res->code;

print文の文末には特殊変数$\も出力されているので、\nの代入によってprint文の文末に\nが自動で出力されます。$\の仕様はperldoc -v '$\'から、特殊変数の一覧はperldoc perlvarから参照できます。

より軽量なHTTPクライアントFurl

FurlはLWPに次いで有名なHTTP通信モジュールで、処理の速さが長所です。

Furlを使う場合もリスト2のようにLWP::UserAgentと似た感覚でHTTP通信ができます。LWPに慣れたらFurlを使うとよいでしょう。

リスト2 furl.pl
use Furl;

my $furl = Furl->new(
    agent => 'PerlHackersHub_32/1.0',
    timeout => 10,
);
my $url = 'http://gihyo.jp/dev/serial/01/perl-hackers-hub';
my $res = $furl->get($url);
print $res->content;

Benchmarkで簡易ベンチマーク

BenchmarkモジュールでCPU時間をもとにパフォーマンス比較が行えるので、LWP::UserAgentとFurlで比較してみましょう。

リスト3は、ローカルのHTTPサーバへLWP::UserAgentとFurlで1,000回ずつGETリクエストしたときの比較を、cmpthese関数で行っています。cmptheseでは図2のように結果を比較表にして、遅い順に出力します。Rateは1秒間の実行回数で表され、右に比較対象との比率を表しています。このようにベンチマーク上ではFurlのパフォーマンスが良いことがわかります。

リスト3 benchmark.pl
use Benchmark qw(cmpthese);
use LWP::UserAgent;
use Furl;

必ずローカル、または管理下のサーバを対象にすること
my $url = 'http://localhost:8000/index.html';

my $lwp = LWP::UserAgent->new(timeout => 10);
my $furl = Furl->new(timeout=>10);

cmpthese(
    1000, {
    lwp => sub { $lwp->get($url) },
    furl => sub { $furl->get($url) },
});
図2 benchmark.plの実行結果
$ perl bench_lwp_furl.pl
       Rate lwp furl
lwp 901/s -- -60%
furl 2273/s 152% --

なお、外部のサーバへこうした短時間の連続アクセスを送るのは迷惑行為となるので、自分の管理下にあるサーバを対象に実行してください。これはほかのWebアクセスするプログラムも同様です。

URL操作とパーセントエンコーディング

HTTP通信をする先のURLを1つの文字列変数で宣言して扱ってもよいのですが、マルチバイト文字や特殊文字のエスケープで問題が発生することがあるので、クエリ操作やURLエンコードの方法を知っておくと便利です。

URIモジュールを使うと、http://ja.wikipedia.org/w/api.php?action=query&prop=revisions&titles=AT%26T&rvprop=content&format=xmlのようなクエリ付きURLを手軽に作成できます。query_formでクエリを組み立てていくと、クエリパラメータが多く複雑なものでもわかりやすいコードになります。

リスト4はURIモジュールを使って、Wikipedia APIへの検索クエリを組み立てています。パラメータの値が自動でURLエンコードされるので、titlesの値であるAT&T&部分がパーセントエンコーディングされて&titles=AT%26Tのようになります。こうしないと&Tというパラメータが発生し、うまく取得できない場合が生じます。また、ホスト部分やパスだけを取得したり、変更したりもできます。

リスト4 uri.pl
use URI;

#Wikipedia API でAT&T について取得するクエリ
my $uri = URI->new('http://ja.wikipedia.org/w/api.php');
$uri->query_form(
    action => 'query',
    prop => 'revisions',
    titles => 'AT&T',
    rvprop => 'content',
    format => 'xml',
);

print $uri->host; # ja.wikipedia.org
print $uri->path; # /w/api.php
print $uri->as_string;
# http://ja.wikipedia.org/w/api.php?action=query&prop..

なお、速度を重視してURLエンコードだけ必要なプログラムを作りたい場合は、高機能なぶん重いURIモジュールを使うよりも、URLエンコードに特化したモジュールであるURI::Escape::XSやURL::Encode::XSを使うほうが良いこともあるので参考にしてください。

欲しい情報のみ抽出する

LWPやFurlといったモジュールを利用してPerlでHTTPクライアントが簡単に実装できました。Webページを丸ごと欲しい場合やAPIとの通信が目的であればこのままで問題はないのですが、ページの一部分だけを抜き出したい場合にはノイズが多過ぎます。そこで役立つのがスクレイピングに特化したモジュールで、本節ではWeb::ScraperとWeb::Queryを紹介します。

Web::Scraperでスクレイピングする

Web::Scraperではリスト5のような記法で抽出したい要素を記述していきます。

リスト5 web_scraper.pl
1: use Web::Scraper;
2: use URI;
3: my $scraper = scraper {
4: process '#primary > div > div.readingContent01
5:            > ul > li', 'items[]' => scraper {
6:         process 'a', uri => '@href',
7:     }
8: };
9: my $url = 'http://gihyo.jp/dev/serial/01/perl-hackers-hub';
10: my $res = $scraper->scrape( URI->new($url) );
11: foreach my $item ( @{$res->{items}} ) {
12:     print $item->{uri};
13: }

リスト5の3行目からのscraper{}内のように、抽出したいタグをXPathかCSS Pathで定義していきます。ここではWebページ内のulタグの中のliタグで列挙されている要素からテキストとURLを抽出します。

10行目の$scraper->scrapeで取得先を引数にして実行します。ここに入れるURLは必ずURIモジュールのインスタンスでなければならないことに注意してください。scrapeメソッドに文字列を渡すとそれをHTML文字列とみなして解析をするためです。

C-styleのforは遅い

Perlでループを使うときは、次のようなインデックス付きのループではなく、リスト5の10行目のようなforeachを使うほうがよいです。

# インデックス付きのループ
for ( my $i=0; i<10; i++ ){ }

# こちらを使う
foreach ( 0..10 ) { }

インデックス付きのループの場合、インデックスのインクリメントや終了条件のチェックのためにパフォーマンスが悪くなります。Benchmarkモジュールを使って両者の簡単なベンチマークを実行してみるとよいでしょう。

Web::QueryでjQuery風にスクレイピングをする

Web::Queryモジュールでは、jQueryのセレクタのように要素を指定してスクレイピングができます。

リスト5をWeb::Queryで書くとリスト6のようになります。findでタグを指定し、eachでリストの中身を一つ一つ処理していきます。

リスト6 web_query.pl
use Web::Query;
my $url = 'http://gihyo.jp/dev/serial/01/perl-hackers-hub';
my $wq = Web::Query->new($url);
my $res = $wq->find('#primary > div >
                   div.readingContent01 > ul > li')
    ->each(sub {
                  print $_->find('a')->attr('href');
                  print $_->find('a')->text();
              });

filterメソッドでフィルタリングができます。たとえばページの全リンクタグからgihyo.jpへ内部リンクしているものだけを取り出したい場合には、findとeachの間にfilterを入れるとよいでしょう。

->find('a')
->filter(
  sub{my ($j,$elem) = @_;
      $elem->attr('href') =~ /\Ahttp:\/\/gihyo\.jp\//; })
->each(sub{...});

ユーザエージェントを設定する

Web::ScraperやWeb::QueryではHTML文字列をそのまま渡しても解析、抽出してくれるので、LWP::UserAgentやFurlでgetしたレスポンスと組み合わせることができます。

my $furl = Furl->new( agent => 'PerlHackersHub_32/1.0' );
my $res = $furl->get( $url );
$scraper->scrape( $res->content ); # res->contet はHTML

Web::Queryの内部で宣言されているLWPはユーザエージェントがデフォルトlibwww-perlなので、カスタムユーザエージェントを設定したい場合はこの方法を使うか、または内部変数に直接アクセスします。Web::ScraperとWeb::Queryはどちらも内部で、ourでグローバル宣言された$UserAgentにLWP::UserAgentのインスタンスが代入されています。グローバル宣言された変数はモジュールをインポートしたプログラム側からでも呼び出すことができるので、$モジュール名::変数名で次のように代入します。

$Web::Scraper::UserAgent
   = LWP::UserAgent->new( agent=>'PerlHackersH
ub_32/1.0');
$Web::Query::UserAgent
   = LWP::UserAgent->new( agent=>'PerlHackersH
ub_32/1.0');

Web::ScraperとWeb::Queryの高速化

libxml2の利用できる環境[2]であれば、use Web::Scraper::LibXML;use Web::Query::LibXML;で読み込ませるだけで、HTMLの解析をlibxml2を使って行うため処理の高速化が見込めます。Perlモジュールではこういったロードするだけで処理速度の向上を見込めるモジュールがいくつも存在します。

変数やオブジェクトをダンプする

今回のようなスクレイピングした結果を配列やハッシュを使ったデータ構造に格納するとき、目的の数値や文字列がちゃんと取得できているかを、変数の中身全体を見ながらプログラムを動作させたくなります。そんなときはData::Dumperに代表されるダンプ用モジュールを使うと、変数やオブジェクトの値を追いながらのデバッグが楽に行えます。

Data::Dumperでデータ構造を丸ごと出力

変数やオブジェクトの中身を丸ごと確認するには、Data::Dumperがお勧めです。Perlのコアモジュールとして標準で入っているので追加でインストールする必要がなく手ごろです。

リスト7のように変数の前にDumperを置いて使用します。すると図3のように日本語がエスケープされて出力されます。YAML::Dumpのようなほかのダンプ用モジュールを使えば日本語を表示できますが、モジュールの追加が困難な環境だったり、どうしてもData::Dumperで表示させたいときは、次のコードをuse Data::Dumper;のあとあたりに書くとよいでしょう。

$Data::Dumper::Useperl = 1;
{
    package Data::Dumper;
    sub qquote{ return shift; }
}
リスト7 data_dumper.pl
use utf8;
use Data::Dumper;
use URI;
my $data = {
    name => 'gihyo taro',
    url => URI->new('http://gihyo.jp/'),
    message => ' こんにちは!'
};
print Dumper $data;
図3 data_dumper.plの実行結果
$VAR1 = {
 'message' => "\x{3053}\x{3093}\x{306b}\x{3061}\x{306f}\x{ff01}",
 'url' => bless(do{\(my $o = 'http://gihyo.jp/')}, 'URI::http' ),
 'name' => 'gihyo taro'
};

Data::DumperはC言語による拡張(XS)を行っており容易にオーバーライドできないので、Useperlフラグを立ててXSを避けて、日本語をエスケープしているqquoteをオーバーライドしています。

見やすくカラフルなData::Printer

Data::Printerというモジュールも便利です。コアモジュールではないため、CPANからインストールする必要があります。use Data::Printer;またはエイリアスであるuse DDP;でモジュールを読み込むことができます。出力したい変数に対してp $dataと書くだけで、値だけでなくオブジェクトのメソッドもまとめて出力されます。

\ {
    message " こんにちは!",
    name    "gihyo taro",
    url     URI::http {
        Parents URI::_server
        public methods (2) : canonical, default_port
        private methods (0)
        internals: "http://gihyo.jp/"
    }
}

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

おすすめ記事

記事・ニュース一覧