2008/01/31


Web::Scraper 0.15とcisco_scraper.pl
問題が一つ。添削してくださったパッチだと process '//li/node()[4]', 'title' => sub {$_->string_value;};
となっているのですが、4番目とは限らないんです。
たとえば、
http://www.cisco-records.co.jp/html/item/004/010/item393180.html
は何曲か試聴サンプルがないために、この処理だと取得できないです。
おろろ...
これはtext()でTextNodeを参照するしかないですね。
ただ、text()では改行等のゴミまで拾ってしまうので、以下のようにnormalize-space()で空文字ノードを省いています。
もしかすると、node()[2]も同じように修正した方がいいかもしれませんね。
#!/usr/bin/perl

use strict;
use warnings;

use Web::Scraper;
use URI;
use YAML;
use Data::Dumper;

my $uri = shift;

my %scraper;

$scraper{'link'} = scraper {
    process 'a', 'name' => 'TEXT';
    process 'a', 'uri'  => '@href';
    result qw/name uri/;
};

$scraper{'genre'} = scraper {
    process '//a[1]', 'top'   => $scraper{link};
    process '//a[2]', 'style' => $scraper{link};
    result qw/top style/;
};

$scraper{'track'} = scraper {
    process '//li/text()[normalize-space(.)!=""]', 'title' => sub {
        my $s = $_->as_XML;
        $s =~ s/\s+$//;
        return $s;
    };
    process 'li>a', 'uri' => '@href';
    result qw/title uri/;
};

$scraper{'item'} = scraper {
    process 'td.de_title',      'title'  => 'TEXT';
    process 'td.de_artist',     'artist' => 'TEXT';
    process 'td.nm_jacket>img', 'image'  => '@src';
    process 'td.de_price',              'price'   => 'TEXT';
    process 'td.de_label>a',            'label'   => $scraper{link};
    process 'td.de_genre',              'genre'   => $scraper{genre};
    process 'td[headers="de_format"]',  'format'  => 'TEXT';
    process 'td[headers="de_release"]', 'release' => 'TEXT';
    process 'td[headers="de_country"]', 'country' => 'TEXT';
    process 'td[headers="de_sheet"]',   'sheet'   => 'TEXT';
    process 'td[headers="de_arrival"]', 'arrival' => 'TEXT';
    process 'td[headers="de_nomber"]',  'number'  => 'TEXT';
    process '//p[@class="de_star"]/node()[2]', 'star' => 'TEXT';
    process 'ul[id="de_sound"]>li', 'tracks[]' => $scraper{track};
    result
        qw/title artist image price label genre format release release country sheet arrival number star tracks/;
};

my $item = $scraper{'item'}->scrape( URI->new($uri) );
warn Dump $item;
あと、ブックマークコメント
コールバック渡しだと相対URLの展開がされないのは僕だけ?
との事ですが...少し調べてみた所Web::Scraper側でパッチが必要かもしれません。
以下svn/trunk(rev2351)からの差分です。
Index: lib/Web/Scraper.pm
===================================================================
--- lib/Web/Scraper.pm  (revision 2351)
+++ lib/Web/Scraper.pm  (working copy)
@@ -152,12 +152,12 @@
         local $_ = $node;
         return $val->($node);
     } elsif (blessed($val) && $val->isa('Web::Scraper')) {
-        return $val->scrape($node);
+        return $val->scrape($node, $uri);
     } elsif ($val =~ s!^@!!) {
         my $value =  $node->attr($val);
         if ($uri && is_link_element($node, $val)) {
             require URI;
-            $value = URI->new_abs($value, $uri);
+            $value = URI->new_abs($value, $uri)->as_string;
         }
         return $value;
     } elsif (lc($val) eq 'content' || lc($val) eq 'text') {
Posted at by




Web::Scraperの0.15がリリースされたので、色々試してみてます。
ほんのりと変わった所を調べてみます。

UserAgentが置き換えられるようになった

uaのスコープを変えて頂けたので、先日の「WWW::MechanizeとWeb::Scraperでtwitterのfriendsを全部取ってみる」でやった以下の様な無理やりなUA置き換えでなく undef &Web::Scraper::__ua;
*Web::Scraper::__ua = sub {
    $mech;
};
以下のようにキレイな置き換えが出来るようになります。 $Web::Scraper::UserAgent = $mech;
スコープもourになってますから、別のパッケージ作るときにも便利かもしれませんね。


ショートカットとしてRAWが使えるようになった

これはインラインのjavascriptをそのままスクレイピングするのに使えますね。
#!/usr/bin/perl

use strict;
use warnings;
use Web::Scraper;
use URI;
use YAML;
use Data::Dumper;

my $my_script = scraper {
    process '//script[2]', 'code' => 'raw';
    result 'code';
};
my $item = $my_script->scrape( URI->new("http://mattn.kaoriya.net") );
warn Dump $item;
この例では、はてなスターのトークン設定部が取得出来ます。 --- |-
<!--
Hatena.Star.Token = '43aa5d7954c8fc062faae4eaaa864913599c277b';
-->
※htmlというショートカットでも使えます。


相対URLから絶対URLを自前で生成しなくてもよくなった

お腹が空いてきたのでWeb::Scraperでモスバーガーのメニューをスクレイピング」でやったように、今までは process '.', url => sub {URI->new_abs($_->attr('href'), $uri)->as_string;};
というコードが必要でしたが、「@」による属性参照でリンクエレメントであるようならば、自動的に絶対URLを生成してくれます。
上のようなコードも process '.', url => '@href';
と楽になりますね。
以上が私が0.13と0.15のdiffをざーーーーと見た感じの変更点です。


おまけ

今日はこれに付け加え、一つtipsを...
Web::ScraperでCISCO RECORDSをスクレーピングより
たとえば <li><span>1</span>Track 1</li>
というHTMLから"Track1"だけを抽出しようにも process 'li', 'title' => 'TEXT';
だと 1Track1 なんて結果になるのでそれを回避するために process 'li', 'title' => sub {
    my $elem = shift;
    $elem->find_by_tag_name('span')->delete;
    return $elem->as_text;
};
なんてことをしてるのですが、もっといい方法があるはず。
treeを壊さずやるとすれば、TextNodeを参照するのがいいかと思います。
例えば、XPathのnode()を使い、番号指定で取得します。だた現状のWeb::ScraperではTextNodeはショートカットで参照出来ませんので、以下のようにstring_valueを返すように手を加えると上手く行きます。
※相対URLの修正も含んでいます。
--- cisco_scraper.pl    Tue Sep 18 13:30:20 2007
+++ cisco_scraper2.pl   Tue Sep 18 14:12:49 2007
@@ -14,9 +14,7 @@
 
 $scraper{'link'} = scraper {
     process 'a', 'name' => 'TEXT';
-    process 'a', 'uri'  => sub {
-        return URI->new_abs( $_->attr('href'), $uri )->as_string;
-    };
+    process 'a', 'uri'  => '@href';
     result qw/name uri/;
 };
 
@@ -27,23 +25,15 @@
 };
 
 $scraper{'track'} = scraper {
-    process 'li', 'title' => sub {
-        my $elem = shift;
-        $elem->find_by_tag_name('span')->delete;
-        return $elem->as_text;
-    };
-    process 'li>a', 'uri' => sub {
-        return URI->new_abs( $_->attr('href'), $uri )->as_string;
-    };
+   process '//li/node()[4]', 'title' => sub {$_->string_value;};
+    process 'li>a', 'uri' => '@href';
     result qw/title uri/;
 };
 
 $scraper{'item'} = scraper {
     process 'td.de_title',      'title'  => 'TEXT';
     process 'td.de_artist',     'artist' => 'TEXT';
-    process 'td.nm_jacket>img', 'image'  => sub {
-        return URI->new_abs( $_->attr('src'), $uri )->as_string;
-    };
+    process 'td.nm_jacket>img', 'image'  => '@src';
     process 'td.de_price',              'price'   => 'TEXT';
     process 'td.de_label>a',            'label'   => $scraper{link};
     process 'td.de_genre',              'genre'   => $scraper{genre};
@@ -53,12 +43,7 @@
     process 'td[headers="de_sheet"]',   'sheet'   => 'TEXT';
     process 'td[headers="de_arrival"]', 'arrival' => 'TEXT';
     process 'td[headers="de_nomber"]',  'number'  => 'TEXT';
-    process 'p.de_star',                'star'    => sub {
-        my $elem = shift;
-        $elem->find_by_tag_name('span')->delete;
-        return $elem->as_text;
-
-    };
+    process '//p[@class="de_star"]/node()[2]', 'star' => sub {$_->string_value;};
     process 'ul[id="de_sound"]>li', 'tracks[]' => $scraper{track};
     result
         qw/title artist image price label genre format release release country sheet arrival number star tracks/;
このTextNode参照の需要があるならショートカットでも良いと思うんですが、いまんところ無さそうですね。

追記
otsune氏より「diff -uじゃないと...」という指摘で修正。
Posted at by




Exuberant ctags

ctags 5.7 improves Perl support

  • Added support for 'package' keyword
  • Added support for multi-line subroutine, package, and constant definitions
  • Added support for optional subroutine declarations
  • Added support for formats
  • Ignore comments mixed into definitions and declarations
  • Fixed detecting labels with whitespace after label name
  • Fixed misidentification of fully qualified function calls as labels
これ凄いす。もう朝からお腹いっぱいです。
さっそくWin32版試してみました。
改行されたメソッドにもジャンプ出来るし、余計なコメントにもヒットしないし、使いやすいです。
vimで開発する方のpluginフォルダには必ずと言って良いほど入っているtaglist.vimを使うとパッケージ名称も一覧されます。
パッケージやサブルーチンがキレイに一覧されます。
さらに今回「package」に対応したので、ちょっと時間は掛かりますが C:\Perl\site\lib>ctags -R -h ".pm"
こんな事して...
set tags=./tags,tags,../tags,c:/perl/site/lib/tags
こんな事しておくと...


ctags_beforejump
こんな状態でビジュアル選択しておいて、"C-]"を押す事で
ctags_afterjump
こんな感じにタグジャンプします。ウマーーー
複数含むモジュールだと、taglist.vimのTagListには複数のpackageが表示されます。

すばらしす...
Posted at by