キーワード登録されていない単語の言及数を知る方法

を、考えてみる。というのも現時点では「もえがく★5」がキーワード登録されていないのだけど、言及数は知りたいなあ・・・と思ったものでして。
まず思いつくのは、d.hatena.ne.jp内で「もえがく★5」をキーにしてgoogle検索をかけて、その結果を使う方法。たとえば1/14の言及数を知りたいときは、「site:d.hatena.ne.jp intitle:もえがく inurl:20080114」という風に検索すればよさそう。

今回は問題ないんだけど、1度に表示できるページが100件までのようなので、「狼と香辛料」のように言及数の多い作品は完全にURLを拾ってくることができない・・・ページを完全にGetするためのスクリプトが必要かも。
ところで。Google Web Search APIというものがあるらしく、それを使えばWebスクレイピングしなくてもきれいなデータ構造で検索結果を取得できるんだけど、どうやら2006年12月をもってサービス終了になったらしい。残念。AJAX版のAPI(?)は使えるらしいが・・・

As of December 5, 2006, we are no longer issuing new API keys for the SOAP Search API. Developers with existing SOAP Search API keys will not be affected.

Google Developers

本題に戻って。上のようなURLから、1/14〜1/20までの検索結果をとってきて、Web::Scraperで加工することに。

use strict;
use warnings;
use Web::Scraper;

my %uniqueid;
foreach my $file (@ARGV){
  my %id = ();
  my $scraper = scraper {
    process 'h2.r a', 'links[]' => '@href';
    result 'links';
  };
  my $result = $scraper->scrape(openhtml($file));

  foreach my $url (@$result){
    next unless($url =~ m!http://d.hatena.ne.jp/([A-Za-z0-9-_]+)!);
    $id{$1} = 1;
  }
  print "$file - ids : " . scalar(keys %id) . "\n";
  @uniqueid{keys %id} = 1;
}
print "-" x 20 . "\n";
print "total ids : " . scalar(keys %uniqueid) . "\n";

sub openhtml{
  my $file = shift;
  open my $fh, $file or die "$file: $!";
  join '', <$fh>;
}
$ perl m.pl *.html
20080114.html - ids : 16
20080115.html - ids : 16
20080116.html - ids : 8
20080117.html - ids : 8
20080118.html - ids : 8
20080119.html - ids : 6
20080120.html - ids : 8
--------------------
total ids : 30

こんな感じ。30件ってすくなっ。

Web::Scraperとか

my $scraper = scraper {
  process 'h2.r a', 'links[]' => '@href';
  result 'links';
};
my $result = $scraper->scrape(openhtml($file));

ここのコードなんですが、最初何やってるのか全然わからなかった・・・。ので、メモしておく。
まずscraperの引数のブロックは一体なんなのか。これはどうやらサブルーチンのリファレンスで

scraper { };

scraper (sub { } );

と等価らしい。つまり、サブルーチンを引数にしている・・・ということになる。
つぎに、ブロックの中身の"process"やら"result"やらで始まる行について。これもよく分からなかったのだけど、サブルーチン呼び出しらしい。

process 'h2.r a', 'links[]' => '@href';

process('h2.r a', 'links[]', '@href');

と等価・・・だと思う。どうやら最近の風潮として括弧を略するのが流行らしいんだけど、分かりにくい・・・。慣れればいいんだろうけど。

Web::Scraper::scraper

引数はサブルーチンのリファレンス。scrape()を呼び出したときに、引数に指定したサブルーチンを実行してくれるようです。
process関数の書式は以下のサイトがわかりやすいかも。

上の例だと

process 'h2.r a', 'links[]' => '@href';

は、<h2 class="r">以下の<a>タグに関して、href属性の値をlinksという配列に格納する・・・という指定になります。links[]じゃなくてlinksにすると、配列ではなくスカラーになってしまい、最初にマッチした要素しか保存してくれないっぽい。

Web::Scraper::scrape

第一引数は

  • URIオブジェクト
  • HTML::Elementオブジェクト
  • HTMLのテキストのリファレンス
  • HTMLのテキスト

のどれかを指定し、第二引数にWebサイトのURLを指定します。URLは指定しなくてもOK*1

scraperコマンド
Usage: scraper [URI-or-filename]
  • s ソースを見る
  • y YAML::Dump
  • c コード生成
  • process 通常の引数+WARN

それにしても

Web::Scraperのコードを見てると分からない使い方がたくさん出てくるけど勉強になるなあ。モダンな(?)Perlのコードに触れるのはいいお手本になる。・・・が、真似できるかどうかといえばどうだろう・・・。

参考までに

の結果をすべて取得する方法を考える。Google下部のナビゲーションバーのリンクを全てとってくる方法が一番簡単で

scraper> process 'div#navbar a', 'next[]' => '@href'
scraper> y
---
next:
  - /search?q=site:d.hatena.ne.jp+intitle:%E7%8B%BC%E3%81%A8%E9%A6%99%E8%BE%9B%E6%96%99+inurl:20080114&num=100&hl=ja&lr=&start=100&sa=N
  - /search?q=site:d.hatena.ne.jp+intitle:%E7%8B%BC%E3%81%A8%E9%A6%99%E8%BE%9B%E6%96%99+inurl:20080114&num=100&hl=ja&lr=&start=200&sa=N
  - /search?q=site:d.hatena.ne.jp+intitle:%E7%8B%BC%E3%81%A8%E9%A6%99%E8%BE%9B%E6%96%99+inurl:20080114&num=100&hl=ja&lr=&start=300&sa=N
  - /search?q=site:d.hatena.ne.jp+intitle:%E7%8B%BC%E3%81%A8%E9%A6%99%E8%BE%9B%E6%96%99+inurl:20080114&num=100&hl=ja&lr=&start=400&sa=N
  - /search?q=site:d.hatena.ne.jp+intitle:%E7%8B%BC%E3%81%A8%E9%A6%99%E8%BE%9B%E6%96%99+inurl:20080114&num=100&hl=ja&lr=&start=100&sa=N

今回はこれでもOK。ただ、やっぱりこれにも限界があって、100 * 10 = 1000 件以上の検索結果が出た場合はフォローしきれない。最終的には「489 件中 1 - 10 件目」の「489」を取ってきて、自分でURLを構成するのが一番確実っぽい。

scraper> process '/html/body/table[2]//b', 'summary[]' => 'TEXT'
scraper> y
summary:
  - d.hatena.ne.jp
  - intitle:狼と香辛料 inurl:20080114
  - 476
  - 1
  - 100
  - 0.58

ただ、これだと出てきた結果を解釈しないといけないんだよなあ。最初に出てきた数字だけの要素を検索結果とみなしてもよさそうだけど・・・。
・・・いや、「xxx件中 xx - xx 件目 (xxx 秒)」という書式が決まっているのだから、後ろから4個目をとればいいんだ。xpathでその辺の指定は無理っぽいので、配列で取ってきてから必要なところだけ抜き出すことはできそう。

*1:多分絶対パスを再構成するのに使うんだと思うけど・・・