URLとXPathを渡すと値のリストを返してくれるツールを書いた

jottitのほうに公式ページをつくりました。

画像一覧からXPathでぶっこぬいてダウンローダにわたす、といった用途から簡単なマイニングまでべんりにつかう予定。

つかいかた

% ./exthtml.pl [ -a [AGENT] -e [REFERER] -c [COOKIE_JAR] ] -x [XPATH] [URL]
% perl exthtml.pl -a Mozilla -c ~/Library/Cookies/Cookies.plist -X "//img[@class='image']/@src" http://www.tumblr.com/dashboard
http://media.tumblr.com/XEVQcg9Zzaj7driiTV8baCRJ_400.gif
http://media.tumblr.com/G6je7UvklaklnqmgypCVpgBg_100.jpg
http://media.tumblr.com/G6je7UvklaklnqmgypCVpgBg_400.jpg
http://media.tumblr.com/Bcb4YCrxh3s3i9vsNiAe7H0C_100.jpg
http://media.tumblr.com/Bcb4YCrxh3s3i9vsNiAe7H0C_400.jpg
http://media.tumblr.com/cqRYWOkYRambcyqr1PmewR2r_100.jpg
http://media.tumblr.com/cqRYWOkYRambcyqr1PmewR2r_400.jpg
http://media.tumblr.com/vj8toQRftakozppfgR7H0j40_400.jpg
http://media.tumblr.com/51bAnBJJUamcm3iu9QVSYb2t_100.png
http://media.tumblr.com/51bAnBJJUamcm3iu9QVSYb2t_400.png
% perl exthtml.pl -a Mozilla -c ~/Library/Cookies/Cookies.plist -X "//table[@id='timeline']//img[@class='photo fn']/@alt" http://twitter.com/ | sort | uniq -c
   1 Tadaki Osawa
   1 Vol.2%
   2 Yusuke Yanbe
   1 hanemimi
   2 log_070702
   1 oklahomamixer
   2 totoounk皇子
   2 ☆ギンギンギラギラギンギンのもりひろ☆
   2 オリハタ
   3 ヤマタケ
   1 小池 陸
   1 岸田渉
   1 星村

code

#!/usr/bin/perl
use strict;
use warnings;

use Encode;
use Getopt::Long;

use LWP::UserAgent;
use HTTP::Cookies::Guess;

my $url = pop @ARGV;

my ($xpath, $referer, $cookie, $agent);
my $result = GetOptions(
    "x|xpath=s" => \$xpath,
    "e|referer=s" => \$referer,
    "c|cookie-jar=s" => \$cookie,
    "a|agent=s" => \$agent,
);

die "usage: ./exthtml.pl [ -a [AGENT] -e [REFERER] -c [COOKIE_JAR] ] -x [XPATH] [URL]" unless ($url && $xpath);

$xpath = decode('utf-8', $xpath);

my $ua = LWP::UserAgent->new;
$ua->cookie_jar(HTTP::Cookies::Guess->create(file => $cookie)) if ($cookie);
$ua->agent($agent) if ($agent);

my $tree = HTML::TreeBuilder::XPath::Remote->new_from_uri($url, $ua, $referer);
for my $node ($tree->findnodes($xpath)) {
    print encode('utf-8', $node->getValue."\n");
}

package HTML::TreeBuilder::XPath::Remote;
use strict;
use warnings;

use List::Util qw( first );

use Encode;
require Encode::Detect;

use HTML::TreeBuilder::XPath;
use HTML::ResolveLink;

use LWP::UserAgent;
use HTTP::Request;
use HTTP::Response::Encoding;

sub new_from_uri {
    my ($pkg, $uri, $ua, $referer) = @_;
    
    my $resolver = HTML::ResolveLink->new(
        base => $uri,
    );
    
    return HTML::TreeBuilder::XPath->new_from_content(
        $resolver->resolve(
            $pkg->get($uri, $ua, $referer)
        )
    );
}

sub get {
    my ($self, $uri, $ua, $referer) = @_;
    
    my $html;
    
    $ua ||= LWP::UserAgent->new();
    my $req = HTTP::Request->new('GET', $uri);
    $req->header(referer => $referer) if ($referer);
    
    my $res = $ua->request($req);
    
    # this detection is based on Web::Scraper.
    if ($res->is_success) {
        my @encoding = (
            $res->encoding,
            ($res->header('Content-Type') =~ /charset=([\w\-]+)/g),
            "Detect",
            "shift-jis"
        );
        my $encoding = first {
            defined $_ && Encode::find_encoding($_)
        } @encoding;
        
        $html = Encode::decode($encoding, $res->content);
        return $html;
    }
    return;
}

WedataにあるLDR Full Feedのsiteinfoを使ってフィードを全文入りにupgradeするPlagger::Plugin::Filter::EntryFullText::LDRFullFeed

lib/Plagger/Plugin/Filter/EntryFullText/LDRFullFeed.pm

http://b.hatena.ne.jp/otsune/20080530#bookmark-8776502

SITEINFOはwedata版に書き換えたほうがよさそう

とのことなので書き換えた。HTML::Featureを使ってもっと便利にしたかったのだけど、UAがうまく置き換えられない問題とかが残ってるのであいかわらずEntryFullTextを継承。なのでPPF::EFT::SiteInfoのEFTのyamlがうまく一緒に使えない問題が残ってます。あとPPF::EFT::SiteInfoも野良siteinfoがあればまだ使えるのでこっちは名前かえました。Webservice::WedataはkoyachiさんのところからとってきてくださいWebService::WedataもCPANからインストールできるようになりました、とてもよかったですね。

config.yaml

plugins:
  - module: Subscription::Config
    config:
      feed:
        - url: http://kichiku.oq.la/rss
        - url: http://kawamurayukie.cocolog-nifty.com/blog/

  - module: Filter::EntryFullText::LDRFullFeed
    config:
      force_upgrade: 1
#      impersonate: 1

pixiv.yaml

# upgrade http://pixiv.net/
author: fuba
custom_feed_handle: http://www\.pixiv\.net/
custom_feed_follow_link: member_illust\.php\?
handle: http://www\.pixiv\.net/member_illust\.php\?
extract_xpath:
  body: //div[@id="content2"]
  author: //div[@id="profile"]/div/text()

config.yaml

ブラウザのcookieを使う。なにかがexpiredだとmypage.phpにリダイレクトされるっぽいので一度www無しのedit.phpに捨てアクセス。custom_feed_handleはwww入りになってるのでEFTは動かないで、2つ目以降のURLだけ有効になる。

global:
  user_agent:
    cookies: /Users/ec/Library/Cookies/Cookies.plist 
  timezone: Asia/Tokyo

plugins:
  - module: Subscription::Config
    config:
      feed:
        - url: http://pixiv.net/edit.php
        - url: http://www.pixiv.net/ranking_r18.php
  - module: Filter::EntryFullText

HTML::Feature::Engine::LDRFullFeed - WedataにあるLDR Full FeedのSITEINFOを使ってWebページの本文を抽出するPerlモジュール

LDR Full FeedのSITEINFOがWedataに移動して便利になったので、そろそろHTML::Featureのエンジンが必要だと思って書いてみた。HTML::FeatureについてはHTML::Feature - 重要部分を抽出するモジュール - - download_takeshi’s diaryを、エンジンの拡張についてはHTML::Featureはエンジンをいろいろ拡張できるよ! - download_takeshi’s diaryを参照ください。

HTML::Feature::Engine::LDRFullFeed

koyachiさんのWebService::Wedataを使ってWedataからSITEINFOを取得、そのSITEINFOをpriority順にソートして、最初にURLがマッチしたSITEINFOのXPathでHTML::Elementをとってくるという仕組み。LDRFullFeedの今の実装みてないのでかなり適当な気もするけど、動いてるので多分大丈夫。priority→typeの仕様変更に対応。

package HTML::Feature::Engine::LDRFullFeed;
use strict;
use warnings;
use base qw(HTML::Feature::Engine);
use HTML::Feature::Result;
use HTML::TreeBuilder;
use HTML::TreeBuilder::XPath;
use WebService::Wedata;

sub run {
    my ($self, $c, $html_ref, $opt) = @_;
        
    die unless $opt->{url};
    return $self->_extract($c, $html_ref, $opt->{url});
} 

sub _tag_cleaning {
    my ($self, $html) = @_;
    return unless $html;
    # preprocessing
    $html =~ s{<!-.*?->}{}xmsg;
    $html =~ s{<script[^>]*>.*?<\/script>}{}xmgs;
    $html =~ s{&nbsp;}{ }xmg;
    $html =~ s{&quot;}{\'}xmg;
    $html =~ s{\r\n}{\n}xmg;
    $html =~ s{^\s*(.+)$}{$1}xmg;
    $html =~ s{^\t*(.+)$}{$1}xmg;
    # control code ( 0x00 - 0x1f, and 0x7f on ascii)
    for ( 0 .. 31 ) {
        my $control_code = '\x' . sprintf( "%x", $_ );
        $html =~ s{$control_code}{}xmg;
    }
    $html =~ s{\x7f}{}xmg;
    return $html;
}

sub _extract {
    my ($self, $c, $html_ref, $url) = @_;
    my $result = HTML::Feature::Result->new;

    my $root = HTML::TreeBuilder::XPath->new_from_content($$html_ref);
   
    my @contents;
    my $siteinfo = $self->_detect_siteinfo($c, $url);
    if ($siteinfo) {
        @contents = $self->_xpath2elems($siteinfo->{data}->{xpath}, $root);
        return unless (@contents);
        if (my @title = $self->_xpath2elems('//title', $root)) {
            $result->title($title[0]->as_text);
        }
    }
    else {
        return;
    }
    
    my $element = HTML::TreeBuilder->new_from_content(
        $self->_tag_cleaning(
            join '', map {$_->as_HTML('<>"&')} @contents
        )
    );
    $root->delete;
    $result->element($element);
    return $result;
}

sub _xpath2elems {
    my ($self, $xpath, $context) = @_;

    my $nodes;
    if (eval {
        $nodes = $context->findnodes($xpath);        
    }) {
        return $nodes->get_nodelist;
    }
    return;
}

sub _detect_siteinfo {
    my ($self, $c, $url) = @_;
   
    my $wedata = WebService::Wedata->new;
    $wedata->{ua} = $c->user_agent;
    
    my $i = 0;
    my %priority = qw/
        SBM 1000
        INDIVIDUAL 100
        IND 100
        SUBGENERAL 10
        SUB 10
        GENERAL 1
        GEN 1
    /;
    
    my $db = $wedata->get_database('LDRFullFeed');
    for my $item (
        sort {
            $a->{data}->{priority} <=> $b->{data}->{priority}
        }
        map {
            $_->{data}->{priority} ||= ($_->{data}->{type})
                ? $priority{$_->{data}->{type}}
                : 0; $_;
        } @{$db->get_items}
    ) {
        if (($item->{data}->{url}) && ($url =~ /$item->{data}->{url}/)) {
            return $item;
        }
    }
    return;
}
1;

HTML::Featureをちょっといじる

HTML::Featureはデフォではuser_agentが変更できないのと、Engineが自分の処理してるページのURLを知る事ができないって仕様になってて、特に後者はSITEINFOを使う上で致命的になる。ので$self->engine->runにオプションも渡せるようにして、HTML::Feature::Engine::LDRFullFeed側でそこからURLを貰ってくるという実装にしてます。でもこれだとHTML::Feature::parseにURLを2個渡さなくちゃいけなくてださいので、なんとかしてほしいなー>id:download_takeshi

package HTML::Feature_;
use base qw/HTML::Feature/;
sub user_agent {_user_agent(@_)};
sub _user_agent {
    my $self = shift;
    return $self->{user_agent} ||= SUPER->_user_agent;
}

sub _run {
    my ($self, $html_ref, $opts) = @_;
    $opts ||= {};

    local $self->{element_flag} = exists $opts->{element_flag} ? $opts->{element_flag} : $self->{element_flag};
    $self->engine->run($self, $html_ref, $opts);
}

extract.pl

今の仕様だとテスト書くのがめんどくさかったので、とりあえず適当にコマンドラインで使えるサンプルをオレオレtsubuanをベースに書いた。たぶんCGIでもうごきます。CGIで動かしたときの仕様はtsubuan互換、コマンドラインでの実行例は以下のようなかんじ。SITEINFOが存在しなかったときにはデフォルトEngineのTagStructureが動くようにしてます。

% perl extract.pl http://d.hatena.ne.jp/fuba/20070401/1175418910
LDRFullFeed
<html><head></head><body><p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%c3%e6%b3%d8%c0%b8">中学生</a>からは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%a4%cf%a4%c6%a4%ca%a5%a2%a5%f3%a5%c6%a5%ca">はてなアンテナ</a>! <a class="keyword" href="http://d.hatena.ne.jp/keyword/%a4%cf%a4%c6%a4%ca%a5%a2%a5%f3%a5%c6%a5%ca%cd%df%a4%b7%a4%a4%a1%aa">はてなアンテナ欲しい!</a><p class="sectionfooter"><a href="/fuba/20070401/1175418910">Permalink</a> | <a href="/fuba/20070401/1175418910#c">コメント(0)</a> | <a href="/fuba/20070401/1175418910#tb">トラックバック(0)</a> | 18:15</body></html>

firefox.jpの転送先をしらべる

ランダムな転送という実装は、つまり十分な回数のアクセスを奨励している。とりあえず1001回の連続アクセスを試みる。

$ perl -MYAML::Syck -e 'my %urls;for my $i (0..1000) {if (`curl -l http://firefox.jp/`=~/src\=\"([^\"]+)\"/) {sleep 1; $urls{$1}++}} warn Dump(\%urls)'

stderr

http://archive.ncsa.uiuc.edu/mosaic.html: 60
http://browser.netscape.com/: 58
http://icab.de/: 65
http://java.sun.com/products/archive/hotjava/index.html: 50
http://jp.opera.com/: 53
http://kmeleon.sourceforge.net/: 53
http://links.sourceforge.net/: 41
http://v3.vapor.com/voyager/: 42
http://www.apple.com/jp/safari/: 64
http://www.avantbrowser.com/: 37
http://www.fenrir.co.jp/sleipnir/downloads/: 55
http://www.flock.com/: 61
http://www.geocities.co.jp/SiliconValley-Bay/6049/: 60
http://www.jp.access-company.com/products/nf_mobile/browser/index.html: 45
http://www.lunascape.jp/: 40
http://www.maxthon.com/: 55
http://www.microsoft.com/japan/windows/products/winfamily/ie/default.mspx: 55
http://www.omnigroup.com/applications/omniweb/: 67
http://www.w3.org/Amaya/: 40

stderr2

増えてるらしいのでもう一回やってみた。acid testはブラウザじゃねーよ

ftp://ftp.funet.fi/pub/networking/services/www/erwisE/: 16
http://3b.net/browser/: 22
http://acid2.acidtests.org/: 13
http://acid3.acidtests.org/: 14
http://archive.ncsa.uiuc.edu/mosaic.html: 24
http://browser.netscape.com/: 20
http://curl.haxx.se/: 14
http://galeon.sourceforge.net/: 20
http://grail.sourceforge.net/: 24
http://home.arachne.cz/: 7
http://ia-world.jp/: 25
http://icab.de/: 15
http://java.sun.com/products/archive/hotjava/index.html: 39
http://jp.opera.com/: 24
http://kids.knowledgewing.com/: 15
http://kmeleon.sourceforge.net/: 19
http://links.sourceforge.net/: 20
http://links.twibright.com/: 19
http://lobobrowser.org/java-browser.jsp: 15
http://lynx.isc.org/lynx2.8.6/index.html: 19
http://shiira.jp/: 17
http://v3.vapor.com/voyager/: 14
http://w3m.sourceforge.net/: 18
http://www.apple.com/jp/safari/: 14
http://www.avantbrowser.com/: 17
http://www.cyberdog.org/: 15
http://www.dillo.org/: 17
http://www.din.or.jp/~blmzf/: 19
http://www.elinks.cz/: 24
http://www.fenrir.co.jp/sleipnir/downloads/: 22
http://www.flock.com/: 20
http://www.geocities.co.jp/SiliconValley-Bay/6049/: 19
http://www.geocities.jp/after9q/butterfly.html: 17
http://www.gnome.org/projects/epiphany/: 17
http://www.ibrowse-dev.net/: 20
http://www.ioage.com/en/: 15
http://www.jikos.cz/~mikulas/links/: 9
http://www.jp.access-company.com/products/nf_mobile/browser/index.html: 17
http://www.konqueror.org/: 12
http://www.lunascape.jp/: 19
http://www.maxthon.com/: 12
http://www.micromind.com/slipknot.htm: 14
http://www.microsoft.com/japan/windows/products/winfamily/ie/default.mspx: 27
http://www.mozilla-japan.org/products/firefox/: 11
http://www.msfirefox.com/microsoft-firefox/index.html: 26
http://www.ne.jp/asahi/art/forum/kidsie/: 17
http://www.nintendo.co.jp/ds/browser/: 14
http://www.nintendo.co.jp/wii/features/wii_channel.html: 20
http://www.omnigroup.com/applications/omniweb/: 23
http://www.spacetime.com/: 19
http://www.torchmobile.com/products/: 27
http://www.w3.org/Amaya/: 18
http://www.wyzo.com/: 17
http://www.xcf.berkeley.edu/~wei/viola/violaHome.html: 11
http://www.xsmiles.org/: 19

結論

fubあった。

LDR Full Feedのsiteinfoを使ってフィードを全文入りにupgradeするPlagger::Plugin::Filter::EntryFullText::SiteInfo(2008/2/27仕様変更)

LDRでgを押せば全文出てくるべんりなLDR Full Feedだけど、個人的には自作の自動抽出のをつかってるのでいいなーと思いながら指をくわえてその進化を見つめています。ただ本文XPathのsiteinfoはものすごく使えそうなので、とりあえず試しにPlaggerでただのりさせてもらうことにしました。siteinfoはキャッシュしてないのでちょっとまずいかもしれないけど、たぶんこのプラグインを使う人はほとんどいないので大丈夫でしょう。siteinfoの取得もPlagger::UAを使うようにしました。これでキャッシュできてる! と思う…。

lib/Plagger/Plugin/Filter/EntryFullText/SiteInfo.pm

元のEntryFullTextは継承して全機能がそのまま動くようになってるはずです。今つかってるconfig.yamlのmodule: Filter::EntryFullTextの行を入れ替えてください。いろいろ修正するついでによく確認してみたらEFT継承部分がちゃんと動いてなかった! 申し訳ない…。ということでこのへん真面目に実装しようとしてassets周りを読んでみたところかなりめんどくさそうなので、てっとりばやくEFTを流用するためにclass_id偽装機能をつけました。impersonateを1にするとまるでこのクラスがPPF::EntryFullTextであるかのようにふるまい、assets/plugins/Filter-EntryFullText以下にあるファイルも読むようになります。とりあえず問題はでてないんですが、しらないところでまずいことになってそうな気がするのでデフォルトでは動かないようにしておきます。EFTを完全に入れ替えて既存のassetsを変更することなく使いたい場合だけ1にしてください。
もちろんassets/plugins/Filter-EntryFullText-SiteInfoってディレクトリをつくれば大丈夫なので、EFTのfeed-upgraderを使いたい場合は下のような2つのやり方があることになります。とりあえず個人的には偽装してやってみるつもり。

  • impersonate:1にしてassets/plugins/Filter-EntryFullTextに置いたまま(こわい)
  • impersonate:0にしてassets/plugins/Filter-EntryFullText-SiteInfoを作ってassets/plugins/Filter-EntryFullTextの中身をコピーする(安全)

config.yaml

plugins:
  - module: Subscription::Config
    config:
      feed:
        - url: http://kawamurayukie.cocolog-nifty.com/blog/

  - module: Aggregator::Simple

  - module: Filter::EntryFullText::SiteInfo
    config:
      impersonate: 0 # これを1にするとEFTのassetsも読むけど…
      force_upgrade: 1

  - module: Publish::Gmail

ふぁぼったーで自分と同じのをfavってるユーザを数える

不評だったのでぐりもんにしました。入れるとふぁぼりページの左上にリンクがでるので、それクリックするとvisualize twitterersみたいに表示します。ダブルクリックでもどります。
http://userscripts.org/scripts/show/20789
greasemonkeyにするのもめんどくさいのでAutoPagerizeしてからFireBugで実行してください。

var you="fuba"; //ここかきかえる
var entries=$x('//*[@class="info meta entry-meta"]');
var coo={};
entries.forEach(function(d){
    var imgs=d.getElementsByTagName('IMG');
    var users=[];
    for(var i=0;i<imgs.length;i++){
        users.push(imgs[i].title)
    }
    if(users.filter(function(user){
        return(user==you)
    }).length>0){
        users.forEach(function(user){
            if(user!=you)coo[user]=(coo[user])?coo[user]+1:1
        })
    }
});
console.log(coo);