ブログが続かないわけ

この日記のはてなブックマーク数
Webエンジニアが思うこと by junichiro on Facebook

Web::Scraper でいい感じのデータ構造になってくれなくて困っているのはどこのどいつだ〜い? アタイだよ!

このエントリーを含むはてなブックマーク hateb
例えばこんなHTMLからニュースの一覧を取得することを考えよう。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="ja" xml:lang="ja" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<title>サッカーニュース</title>
<body>
<ul class="news">
<li>
<a href="http://sports.livedoor.com/article/vender-15.html">C・ロナウドが休日返上宣言!</a>
</li>
<li>
<a href="http://sportsnavi.yahoo.co.jp/soccer/index.html">イタリア代表のドナドーニ監督「アイルランドを甘く見てはいない」</a>
</li>
<li>
<a href="http://sportsnavi.yahoo.co.jp/soccer/index.html">バルセロナが前回王者セビージャを下す=スペイン国王杯</a>
</li>
<li>
<a href="http://sportsnavi.yahoo.co.jp/soccer/index.html">ユベントス奮闘、5&#8722;3でエンポリを下す=イタリア杯</a>
</li>
</ul>
</body>
</html>
ニュースのタイトルの一覧を取得するとしたら、やっぱりタイトルは複数あるので配列で受け取りたいところだ。そこで、'titles[]'みたいな形になると想像できる。で、こんなコードになる。
use strict;
use Web::Scraper;
use URI;

my $uri = URI->new("http://localhost/~tobe/news_sample.html");
my $scraper = scraper {
process "ul.news>li>a" ,'titles[]' => 'TEXT';
};
my $result = $scraper->scrape($uri);
結果(以下、YAML形式でDumpする)
---
titles:
- C・ロナウドが休日返上宣言!
- イタリア代表のドナドーニ監督「アイルランドを甘く見てはいない」
- バルセロナが前回王者セビージャを下す=スペイン国王杯
- ユベントス奮闘、5−3でエンポリを下す=イタリア杯
上々の出来だ。今度は、それぞれのリンク先を取得したいと思う。
use strict;
use Web::Scraper;
use URI;

my $uri = URI->new("http://localhost/~tobe/news_sample.html");
my $scraper = scraper {
process "ul.news>li>a" ,'links[]' => '@href';
};
my $result = $scraper->scrape($uri);
結果
---
href:
- !!perl/scalar:URI::http http://sports.livedoor.com/article/vender-15.html
- !!perl/scalar:URI::http http://sportsnavi.yahoo.co.jp/soccer/index.html
- !!perl/scalar:URI::http http://sportsnavi.yahoo.co.jp/soccer/index.html
- !!perl/scalar:URI::http http://sportsnavi.yahoo.co.jp/soccer/index.html
全く問題ない。ところが、この考えの延長でいくと、タイトルとリンクの組み合わせを取得したいときに無理が生じる。
例えば、こんな感じで書いてみたらどうだろうか。
use strict;
use Web::Scraper;
use URI;

my $uri = URI->new("http://localhost/~tobe/news_sample.html");
my $scraper = scraper {
process "ul.news>li>a" ,'titles[]' => 'TEXT', 'links[]' => '@href';
};
my $result = $scraper->scrape($uri);
結果
---
links:
- !!perl/scalar:URI::http http://sports.livedoor.com/article/vender-15.html
- !!perl/scalar:URI::http http://sportsnavi.yahoo.co.jp/soccer/index.html
- !!perl/scalar:URI::http http://sportsnavi.yahoo.co.jp/soccer/index.html
- !!perl/scalar:URI::http http://sportsnavi.yahoo.co.jp/soccer/index.html
titles:
- C・ロナウドが休日返上宣言!
- イタリア代表のドナドーニ監督「アイルランドを甘く見てはいない」
- バルセロナが前回王者セビージャを下す=スペイン国王杯
- ユベントス奮闘、5−3でエンポリを下す=イタリア杯
確かに、取得できているんだけど、これではちょっとうまくない。1次元の別々の配列で、添字が等しければ...なんてのはデータ構造としてあんまりよろしくない。
ここはやっぱり、こんな形のデータ構造として取得したいところだ。
---
result:
- link: !!perl/scalar:URI::http http://sports.livedoor.com/article/vender-15.html
title: C・ロナウドが休日返上宣言!
- link: !!perl/scalar:URI::http http://sportsnavi.yahoo.co.jp/soccer/index.html
title: イタリア代表のドナドーニ監督「アイルランドを甘く見てはいない」
- link: !!perl/scalar:URI::http http://sportsnavi.yahoo.co.jp/soccer/index.html
title: バルセロナが前回王者セビージャを下す=スペイン国王杯
- link: !!perl/scalar:URI::http http://sportsnavi.yahoo.co.jp/soccer/index.html
title: ユベントス奮闘、5−3でエンポリを下す=イタリア杯
そこで重要なのがコードリファレンスを使ったやり方だ。これをマスターしなければ、Web::Scraperは使いこなせない。
use strict;
use Web::Scraper;
use URI;

my $uri = URI->new("http://localhost/~tobe/news_sample.html");
my $items = scraper {
process "a" ,'title' => 'TEXT', 'link' => '@href';
result 'item';
};
my $scraper = scraper {
process "ul.news>li", 'result[]' => $items;
};
my $result = $scraper->scrape($uri);
ここでのポイント
1. result[]という配列でul.news>liの中身をおおまかに受け取ること。
2. それに対して、aタグのTEXT要素やhref属性の値を取得していること。(コードリファレンスで)
3. コードリファレンスの中ののtitleやlinkが複数形(配列)になっていないこと。
ul.news>li をresult[]という配列で受け取る形になっていて、その中身にtitleやlinkがあるのだから、そのresultの要素ひとつひとつに対しては、titleやlinkはひとつずつしかないからだ。

というわけで、以前に僕が書いたWeb::Scraper 使い方(超入門)にはちょっと嘘が書いてある。

いまだに、テーブルコーディングされているサイトも多いし、div でCSSコーディングされているサイトであっても、ブロックが入れ子構造になっているサイトは多いと思う。
そういうサイトからデータを抽出するときは、まず、どのブロックからデータを取得するのかを大まかに指定してしまって、その中で細かい情報を取得するという段階を踏むと、スクレーピングしやすい。このSynopsis でコールバックを使っているのは、まさにそういう方法を見せるためだと思う。

まず、どのブロックからデータを取得するのか。ここでは、<table class="ebItemlist">の内側からデータを取るよ、ということを$ebay で指定して、実際にそのブロックの中のどの部分をどのように抽出するのかという細かいところを$ebay_auction で指定しているのだろう。

ここの部分は間違いだ。取得したいデータを2次元以上のデータ構造として取得したい場合に、コードリファレンスを使えばいいのだ。
コメントで指摘してくれたaoiさん。
ありがとうございました。

2008/01/17 10:05 追記
miyagawaさんからコメントで補足を頂きました。
$item とう一時変数を用意しなくても下記のようにすっきりと書けます。
use strict;
use Web::Scraper;
use URI;

my $uri = URI->new("http://localhost/~tobe/news_sample.html");

my $scraper = scraper {
process "ul.news>li>a",
'result[]' => { 'title' => 'TEXT', 'link' => '@href' };
};

my $result = $scraper->scrape($uri);

これで、入れ子入れ子の繰り返しで、複雑なデータ構造もバチッと取れますね。
この記事のトラックバックURL
http://en.yummy.stripper.jp/trackback/800109
トラックバック
コメント
process "ul.news>li>a", 'result[]' => { 'title' => 'TEXT', 'link' => '@href' }
でもいけますよ
| miyagawa | 2008/01/17 3:07 AM |
おお!
$item なんて一時変数を用意するのはなんだか直感的でなくて、ちょっとやだなぁと思っていたので、とてもうれしいです。

この書き方の方が、得られるデータ構造が想像しやすいですし、なによりもタイプ量も少ないし、素敵です。

ありがとうございます。
| junichiro | 2008/01/17 9:58 AM |
ネストする例は、一時変数を使わなくても、

process "ul>li", "results[]" => scraper {
process "a", title => 'TEXT', link => '@href';
};

という風に直接かいても大丈夫です。この辺はスライドにのってますので。

http://www.slideshare.net/miyagawa/web-scraper-shibuyapm-tech-talk-8

| miyagawa | 2008/01/17 12:13 PM |
ありがとうございます。

そのスライドは、現地でも見ましたし、まとめ記事も書いたんですけど、すっかり忘れてしまっておりました。
| junichiro | 2008/01/17 1:45 PM |









関連情報