最近、スクレイピングすることが多かったので、面倒くさくなって作りました。まだ、いろいろ途中ではありますが。
初Mouse、初git、初githubなんで、なんか変なことしてたらすみません。
http://github.com/ktat/LinkSeeker/
スクレイピングするときは、以前書いてますが、下記のような処理をしています。
これを、各クラスにばらけさせました。
- LinkSeeker::Getter (URLのページ取得)
- LinkSeeker::HtmlStore (取得したページの保存)
- LinkSeeker::Scraper (ページのスクレイピング) -- 実装は継承したsubclassで
- LinkSeeker::DataStore (スクレイプしたデータの保存)
- LinkSeeker::DataFilter (スクレイピングしたデータを何かする) -- 実装は継承したsubclassで
上記クラスは何もしておらず、そのサブクラスで実際の処理を実装し、configにて、何を使うか指定します。
取得するサイトを表現するのは下記のクラス(Siteじゃなくて、Pageのがいい気が...)。
- LinkSeeker::Sites ... スクレイピング対象のサイト群
- LinkSeeker::Sites::Site ... スクレイピング対象の一つのサイト
- LinkSeeker::Sites::Site::URL ... サイト内のURL(とリクエストの組み合わせ)
テスト代わりにとりあえず動かせるサンプルを置いてます。
sample/ 以下に、data ディレクトリを作った後、下記を実行してください。
perl -I../lib -Ilib -MNews -e 'News->new(file => ["news.yml"])->run'
data/src 以下に、取得したhtmlファイル
data/scraped 以下に、scrapeしたデータが入ります。
この例では、日経新聞のサイトからデータ取ってくるのですが、カテゴリのページを取得してスクレイピングして、その中にある個別のニュース記事を取得してスクレイピングしています。
こういったネストした処理も楽にできるようにしています。ユーザーは設定ファイルと、スクレイピング処理だけ書けばいいって感じです。
設定ファイルはこんな感じ
--- # 既に保存されているものがあればそれを優先する設定。 prior_stored : 1 # 下のようにも設定できる。 # html と data の両方を優先する。htmlだけにすれば、getはしないけど、scrapingはやり直す # prior_stored : ['html', 'data'] getter : # LinkSeeker::Getter::LWP をhtmlを取得するのに使う class: LWP html_store : # LinkSeeker::HtmlStore::File を取得したhtmlを保損するのに使う class: File path : data/src data_store : # LinkSeeker::DataStore::Dumper を データを保存するのに使う class: Dumper path : data/scraped # 上の設定で、class 以外の設定については、各クラスのオブジェクトを作る際に渡される sites: # サイト名。メソッド名に使われます(設定ファイル内でユニーク) nikkei_main_list: url : # url: http:.. のようにも指定できるけど、変数を使いたい場合は下記のようにする base: http://www.nikkei.co.jp/news/$category/ # variables 以下で、上の変数の設定をする variables: # 変数 $category は、News クラスの news_category メソッドで取得できる category: nikkei_news_category # News::Nikkei をスクレイピングに使う(メソッド名はサイト名 nikkei_main_list) scraper : Nikkei # 取ったデータを元にさらにスクレイピングするための設定 nest : # サイト名。メソッド名に使われます(設定ファイル内でユニーク) nikkei_news_detail: url: # fromで、その前のスクレイピング結果(hash ref)のキーを指定すると、 # そこには、URLが入っているとみなし、そこから、URLを取ってくる from: news_detail_url # 取得したページに対するユニークな名前を決定するための仕組み unique_name : # 正規表現でマッチした部分をユニークな名前として使う # 指定しないっとURLをエスケープした文字列が使われます regexp: /([^/]+)\.html$
※サイト名は設定ファイル内でユニークって書いてますが、そのうち変更すると思います。
nest以下の設定は、最初のsites の設定と同じようにかけるようにしているつもり。
sites 以下に複数のサイト設定を書けますし、nest以下にも複数のサイト設定を書けます。
getter/html_store/data_store とかは、サイト毎の設定の下にもかけます。
根っこに書くと、書いてない場合のデフォルトとして使われます。
scraperもサイト毎にかけますが、こちらは、デフォルトの設定というわけではなく、継承します。上の例でいうと、nikkei_news_detail は、scraperの設定書いていませんが、その親の nikkei_news_main の設定を継承します。もし、nikkei_news_detailで、scraperを設定して、さらにnestがある場合、nest先では、nikkei_news_detail の scraperが使われます。
Newsクラスには、設定ファイルでvariablesで指定したメソッドが必要です。
package News; use Any::Moose; use lib qw(../lib); extends 'LinkSeeker'; sub nikkei_news_category { [ qw/main keizai sangyo kaigai seiji shakai/ ]; } 1;
これにより、URL内で使われている変数を置き換えます。
複数返しているので、複数のURLが作られて、そのすべてをスクレイピングします。
注意としては、複数の値を配列リファレンスで返す場合は、同じ数を返すようにしないといけません。(て、まだ、試してないけど)
とりあえず、この設定だと、下記のURLが対象になります。
http://www.nikkei.co.jp/news/main/ http://www.nikkei.co.jp/news/keizai/ http://www.nikkei.co.jp/news/sangyo/ http://www.nikkei.co.jp/news/kaigai/ http://www.nikkei.co.jp/news/seiji/ http://www.nikkei.co.jp/news/shakai/
スクレイピングのプログラムは、News::Nikkeiに書きます。
package News::Nikkei; use Any::Moose; use Web::Scraper; my $base_url = 'http://www.nikkei.co.jp'; sub nikkei_main_list { my ($self, $src) = @_; my $scraper = scraper { process 'ul.arrow-w-m-list li a', 'news_detail_url[]' => '@href'; process 'h3.topNews-ttl2 a', 'top_news_detail_url[]' => '@href'; }; my $result = $scraper->scrape(\$src); my $top_news = delete $result->{top_news_detail_url}; unshift @{$result->{news_detail_url}}, @$top_news; for my $url (@{$result->{news_detail_url}}) { if ($url !~ /^$base_url/) { $url = $url =~m{^/} ? $base_url . $url : $base_url .'/'. $url; } } return $result; } sub nikkei_news_detail { my ($self, $src) = @_; my $scraper = scraper { process 'h3.topNews-ttl3', 'title' => 'TEXT'; process 'div.article-cap', 'content' => 'TEXT'; }; return $scraper->scrape(\$src); } 1;
他には、url のところに、post_data を与えてやれば、POSTでリクエストを送るとか、header を渡すことで、リクエストヘッダーを送れたりもします。が、ドキュメントがまだぜんぜん書けてないです。
以下、予定ですが、現状の業務で使う分にはあんまり必要なかったりするので、結構先になるかも。
DataFilterについて何も書いてない…ですが、ちょっと変更しようかなぁ、とか思ったり思わなかったりしているので、今回は言及しないということで(次回があるのか謎だけど)。