SweetPotato::Plagger このページをアンテナに追加 RSSフィード

2006-11-14

[] 第九回XML開発者の日 聴講予定 13:23  第九回XML開発者の日 聴講予定 - SweetPotato::Plagger を含むブックマーク はてなブックマーク -  第九回XML開発者の日 聴講予定 - SweetPotato::Plagger  第九回XML開発者の日 聴講予定 - SweetPotato::Plagger のブックマークコメント

Plagger: the duct tape of the Web宮川達彦(Six Apart, Ltd.)
第九回XML開発者の日

CustomFeed::Configはどう省略すればいいのか 09:07 CustomFeed::Configはどう省略すればいいのか - SweetPotato::Plagger を含むブックマーク はてなブックマーク - CustomFeed::Configはどう省略すればいいのか - SweetPotato::Plagger CustomFeed::Configはどう省略すればいいのか - SweetPotato::Plagger のブックマークコメント

Filter::EntryFullText→EFTなら,CustomFeed::Config→C?これは略しすぎか。じゃあCustomFeedの部分も使ってCFCとか?CuFeCって書くと銅・鉄・炭素で化学っぽい。

[] CustomFeed::MelonBooks 02:53  CustomFeed::MelonBooks - SweetPotato::Plagger を含むブックマーク はてなブックマーク -  CustomFeed::MelonBooks - SweetPotato::Plagger  CustomFeed::MelonBooks - SweetPotato::Plagger のブックマークコメント

このプラグインはWeb::Scraperを用いて書き直されました。最新版はCodeReposに置いてあります。

メロンブックス通信販売の入荷日別アイテムリストを取得し,アイテム毎にエントリを作成する。同人誌,同人ソフト,同人グッズのリストに対応。

Plagger/Plugin/CustomFeed/MelonBooks.pm

リファクタリングを行った。

package Plagger::Plugin::CustomFeed::MelonBooks;
use strict;
use warnings;
use base qw( Plagger::Plugin );

use HTML::TreeBuilder::XPath;
use Plagger::Entry;
use Plagger::Feed;
use Plagger::Util;
use URI;

sub register {
    my ($self, $context) = @_;
    $context->register_hook(
        $self,
        'subscription.load' => \&load,
    );
}

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

    my $day = $self->conf->{day} || Plagger::Date->now()->strftime('%Y/%m/%d');

    my $feed = Plagger::Feed->new;
    $feed->aggregator( sub { $self->aggregate($context, $day); } );
    $context->subscription->add($feed);
}

sub aggregate {
    my($self, $context, $day) = @_;

    my $feed = Plagger::Feed->new;
       $feed->title("MelonBooks $day");
       $feed->link('http://shop.melonbooks.co.jp/tsuhan/system/index.php?RATED=18');

    $day =~ s!/!%2F!g;

    my $list = [];
    for my $genre (
        '%C6%B1%BF%CD%BB%EF',             # Dojinshi
        '%C6%B1%BF%CD%A5%BD%A5%D5%A5%C8', # Dojin Soft
        '%C6%B1%BF%CD%A5%B0%A5%C3%A5%BA', # Dojin Goods
    ) {
        push @$list, $self->parse_list($day, $genre);
    }

    for my $item (@$list) {
        my $entry = Plagger::Entry->new;
           $entry->title($item->{title});
           $entry->link($item->{link});
           $entry->author($item->{circle});
           $entry->tags($item->{genres});
           $entry->body(join '', map { qq!<img src="$_">! } @{$item->{images}});
        $feed->add_entry($entry);
    }

    $context->update->add($feed);
}

sub parse_list {
    my ($self, $arrival, $genre) = @_;

    my $list = [];
    for (my $dispstart = 0;; $dispstart += 10) {
         my $content = Plagger::Util::load_uri(URI->new(
             "http://shop.melonbooks.co.jp/tsuhan/system/list.php"
           . "?RATED=18&DISPACTION=disppage&ARRIVAL=$arrival&DISPSTART=$dispstart&GENRE=$genre"
           . "&DISPEMPTY=&CHGRATE=&DISPSTARTCHG=0&DISPORDER=maker&DISPPAGE=10&DISPSTYLE=desc"
         ));

        last unless $content =~ /<OPTION VALUE="(\d+)" SELECTED >/;
        last unless $dispstart == $1;

        my $tree = HTML::TreeBuilder::XPath->new;
           $tree->parse($content);
        my $nodelist = eval { $tree->findnodes('//body//table[@bordercolor="white"]') };
        if ($@) {
            $self->log( error => $@ );
            next;
        }

        for my $node (@$nodelist) {
            my $item = $genre eq '%C6%B1%BF%CD%BB%EF' # Dojinshi
                     ? $self->parse_dojinshi($node->as_HTML('<>&'))
                     : $self->parse_non_dojinshi($node->as_HTML('<>&'));
            push @$list, $item if $item;
        }
    }

    wantarray ? @$list : $list;
}

sub parse_dojinshi {
    my ($self, $content) = @_;

    $content =~ m{
           <td[ ]bgcolor="whitesmoke"[ ]valign="middle"><.*?>(.*?)</.*?>
        .*?<td[ ]bgcolor="whitesmoke"[ ]valign="middle"><.*?><.*?>(.*?)</.*?>
        .*?<td[ ]bgcolor="whitesmoke"[ ]valign="middle"><.*?>(.*?)</.*?>
        .*?<td[ ]bgcolor="whitesmoke"[ ]valign="middle"><.*?><.*?>(.*?)</.*?>
    }x ? $self->to_hashref($1, $2, $3, [$4]) : undef;
}

sub parse_non_dojinshi {
    my ($self, $content) = @_;

    $content =~ m{
           <td[ ]bgcolor="whitesmoke"[ ]valign="middle"><.*?>(.*?)</.*?>
        .*?<td[ ]bgcolor="whitesmoke"[ ]valign="middle"><.*?><.*?>(.*?)</.*?>
        .*?<td[ ]bgcolor="whitesmoke"[ ]valign="middle"><.*?>(.*?)</.*?>
        .*?<td[ ]bgcolor="whitesmoke"[ ]valign="middle"><.*?><.*?>(.*?)</.*?>
        .*?<td[ ]bgcolor="whitesmoke"[ ]valign="middle"><.*?><.*?>(.*?)</.*?>
    }x ? $self->to_hashref($1, $2, $3, [$4, $5]) : undef;
}

sub to_hashref {
    my ($self, $title, $circle, $id, $genres) = @_;

    my @images = map { "http://shop.melonbooks.co.jp/img/$id$_.gif" } ('', 'a', 'b');
    return {
        title  => $title,
        link   => "http://shop.melonbooks.co.jp/tsuhan/system/detail.php?RATED=18&ITEM_ID_FULL=$id",
        circle => $circle,
        genres => $genres,
        images => \@images,
    };
}

1;

config.melonbooks.yaml

plugins:
  - module: CustomFeed::MelonBooks
    config:
      day: 2006/11/14

dayは %Y/%m/%d 形式で指定する。指定しなかった場合は今日入荷したアイテムのリストを取得する。

スクリーンショット(with Publish::Gmail)

f:id:SweetPotato:20061021160745p:image

[] Filter::ForcePermalink 02:49  Filter::ForcePermalink - SweetPotato::Plagger を含むブックマーク はてなブックマーク -  Filter::ForcePermalink - SweetPotato::Plagger  Filter::ForcePermalink - SweetPotato::Plagger のブックマークコメント

Permalinkを持たない各Entryに対して,FeedLink#EntryDigestのような形式のPermalinkを設定する。既にPermalinkを持っているEntryに対しては何もしない。

これにより,例えば,CustomFeed::Configによって切り出されたが本来はURLを持たないようなエントリに対して擬似的なURLを与えることができる。これはRule::Dedupedを使用する際に役に立つ。Rule::Dedupedは重複チェックのキーがエントリのURL(日付が存在する場合はURLと日付の連接)になっており,URLが指定されていなければ重複チェックが正しく行われないからだ。

EntryDigestの算出にはPlagger::Entry->digestを使用しているが,これはEntryのタイトルと内容に依存しているため,いずれかが変われば設定されるPermalinkも変わる。Rule::Dedupedを利用する分には問題はない。

Plagger/Plugin/Filter/ForcePermalink.pm

package Plagger::Plugin::Filter::ForcePermalink;
use strict;
use base qw( Plagger::Plugin );

use URI;

sub register {
    my ($self, $context) = @_;
    $context->register_hook(
        $self,
        'update.entry.fixup' => \&filter,
    );
}

sub filter {
    my ($self, $context, $args) = @_;

    if($args->{entry}->permalink) {
        Plagger->context->log(debug => "Entry " . $args->{entry}->title . " already has permalink. Skipped");
        return;
    }

    my $permalink = URI->new($args->{feed}->link);
    $permalink->fragment($args->{entry}->digest);
    $args->{entry}->permalink($permalink);
}

1;

TODO: 断片部からの記事の移動 02:37 TODO: 断片部からの記事の移動 - SweetPotato::Plagger を含むブックマーク はてなブックマーク - TODO: 断片部からの記事の移動 - SweetPotato::Plagger TODO: 断片部からの記事の移動 - SweetPotato::Plagger のブックマークコメント

swp::Plagger関連記事 - 断片部