2008/01/31


先日書いた「個人的ソーシャルブックマークサービスの歩き方」という記事にもある通り、私は個人的な資料をdel.icio.us、ソーシャルなものをはてなブックマークに...と使い分けています。
ただし、携帯からはdel.icio.usが使えない為、はてなブックマークを使ってお気に入りユーザのブクマから必要な物だけを自分のブックマークとしてエントリしています。その後、資料として必要な物をdel.icio.usに手作業で転送しています。ただし量が多い場合にはPlaggerを使うこともあります。
ただし、ここで一つ問題が発生していました。
はてなブックマークのフィードにはブクマコメントがitem/descriptionフィールドに格納されています。ただしPublish::Delicousを含むほぼ全てのSBM系プラグインではsummaryではなくbody(body_text)をコメント部として扱う仕様になっています。ですので
http://b.hatena.ne.jp/mattn/rss <description>おぉ。thx>miyagawa</description>
とdescriptionフィールドに格納されている文字列そのままが欲しいにも関わらず <content:encoded>
  &lt;blockquote cite="http://www.ac.cyberhome.ne.jp/~mattn/cgi-bin/blosxom.cgi/software/lang/perl/20071015162834.htm" title="Big Sky :: Publish::Wassrをでっちあげた"&gt;
    
    &lt;cite&gt;&lt;a href="http://mattn.kaoriya.net/software/lang/perl/20071015162834.htm"&gt;Big Sky :: Publish::Wassrをでっちあげた&lt;/a&gt; &lt;a href="http://b.hatena.ne.jp/entry/http://www.ac.cyberhome.ne.jp/~mattn/cgi-bin/blosxom.cgi/software/lang/perl/20071015162834.htm"&gt;&lt;img src="http://b.hatena.ne.jp/images/entry.gif" title="このエントリーを含むブックマーク" alt="このエントリーを含むブックマーク" border="0"&gt;&lt;/a&gt;&lt;/cite&gt;

  &lt;/blockquote&gt;
  &lt;p&gt;おぉ。thx>miyagawa&lt;/p&gt;
</content:encoded>
という元記事の引用文が含まれたbodyで配信されてしまいます。はじめはPublish::XXXでpost_bodyしているSBM系のプラグインを全て直そうかと(use_summaryみたいなオプションで)思いましたが面倒。いっそAggregator::SimpleのXML::Feed::RSSを操作している部分にオプション付けて強制的にcontentでなくsummaryを使わせるように修正しようかとも思いました。ただ、よく考えたらsummaryをbodyに上書きしてやるプラグインを書いた方が便利だし汎用的だと思い以下のプラグインを作りました。
Plagger/Plugin/Filter/SummaryToBody.pm
package Plagger::Plugin::Filter::SummaryToBody;
use strict;
use base qw( Plagger::Plugin );

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

sub filter {
    my($self, $context, $args) = @_;
    $args->{entry}->body($args->{entry}->summary);
}

1;

__END__

=head1 NAME

Plagger::Plugin::Filter::SummaryToBody - copy summary field to body field.

=head1 SYNOPSIS

  - module: Filter::SummaryToBody

=head1 DESCRIPTION

This plugin copy summary field to body field. This is helpful to sanitize
description field. ex) Hatena bookmark field include <blockquote> tag for
quote.

=head1 AUTHOR

Yasuhiro Matsumoto

=head1 SEE ALSO

L<Plagger>, L<Plagger::Plugin::Filter::SummaryToBody>

=cut
使い方はmodule定義だけ。以下は私がはてブからdel.icio.usの転送につかっているYAML
hatebu2delicous.yaml
global:
  assets_path: /home/user/plagger/assets/
  timezone: Asia/Tokyo
  log:
    level: info

plugins:
  - module: Subscription::Config
    config:
      feed:
        - http://b.hatena.ne.jp/[hatena user]/rss

  - module: Filter::SummaryToBody

  - module: Filter::Rule
    rule:
      module: Deduped
      path: /tmp/hatebu2delicious.db

  - module: Publish::Delicious
    config:
      username: [delicious username]
      password: [delicious password]
      interval: 2
      post_body: 1
どっちかっていうとBreakXXX系のプラグインで、しかも個人用途でしかありませんが一応公開しておきます。
後でCodeReposにも置いておきます。

追記
もしかしたら空繰再繰さんの「Plagger::Plugin::Filter::ExtractBody」を使ってXPathで「p」とする事でも同じ結果になるかもしれませんね。
こちらは後日試します。
Posted at by




これでFilterも作りやすくなるのかな...
例えば、はてなブックマークのフィードからShibuya.pmタグが付いてる物のOPMLを作るとか?(自身無さげ)
でもこれ、MIMEパターンをconfig.yamlに上手くめり込ませる方法ってないのかな...
指定する場合、「このURLに対しては変則的なxxxなMIMEで取りたい」って使いたいんだよね。

Index: lib/Plagger/Plugin/Subscription/Feed.pm
===================================================================
--- lib/Plagger/Plugin/Subscription/Feed.pm (revision 1959)
+++ lib/Plagger/Plugin/Subscription/Feed.pm (working copy)
@@ -17,7 +17,6 @@
 sub load {
     my ( $self, $context ) = @_;
 
-    # TODO: Auto-Discovery, XML::Liberal
     my $uri = URI->new( $self->conf->{url} )
       or $context->error("config 'url' is missing");
 
@@ -30,6 +29,20 @@
     my $content = Plagger::Util::load_uri($uri);
     my $feed = eval { Plagger::FeedParser->parse(\$content) };
 
+    if unless($feed) {
+        use HTML::TokeParser;
+        my $parser = HTML::TokeParser->new(\$content);
+        while (my $token = $parser->get_tag("link")) {
+            my $attr = $token->[1];
+            if ($attr->{rel} eq 'alternate'
+                    && ($attr->{type} eq 'application/rss+xml'
+                     or $attr->{type} eq 'application/atom+xml') {
+                $uri = $attr->{href};
+                $feed = eval { Plagger::FeedParser->parse(\$content) };
+                last;
+            }
+        }
+    }
     unless ($feed) {
         $context->log( error => "Error loading feed $uri: $@" );
         return;
Posted at by




どっちかっていうと、今日の出来事みたいな記事になります。
題名の件をやろうとまず、以下のようなYAMLを書いた。
plugins:
  - module: Subscription::Config
    config:
      feed:
        - http://b.hatena.ne.jp/t/shibuya.pm?mode=rss

  - module: Publish::OPML
    config:
      title: Shibuya.pm
      filename: shibuya-pm.opml
で実行したけれどOPMLが空っぽ。
ソースを追ってBreakEntriesToFeedsが使えそうだったので以下の行を足した。   - module: Filter::BreakEntriesToFeeds
    config:
      use_entry_title: 1
でも駄目。で、miyagawaさんにメールした。なぜか英語で...
余談 : なぜ英語か
以前、Web::Scraperについて質問メールを送った。でも反応が無かったので物は試しと英語で書いた。そしたら返事が返って来た。
→ 以後英語... orz
miyagawaさんから、「use BreakEntriesToFeeds」と返事が来たけど、「BreakEntriesToFeeds」は1 entryを1 feedに変換する為のプラグインで、subscriptionを変更するものでは無かった。
で、書きあげたのが以下のプラグイン

BreakEntriesToSubscriptions.pm
package Plagger::Plugin::Filter::BreakEntriesToSubscriptions;
use strict;
use base qw( Plagger::Plugin );

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

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

    for my $entry ($args->{feed}->entries) {
        my $feed = $args->{feed}->clone;
        $feed->clear_entries;

        $feed->add_entry($entry);
        $feed->link($entry->link);
        $feed->url($entry->link);
        eval {
            use HTML::TokeParser;
            my $agent = Plagger::UserAgent->new;
            my $res = $agent->fetch($entry->link, $self);
            my $parser = HTML::TokeParser->new(\$res->content);
            while (my $token = $parser->get_tag("link")) {
                my $attr = $token->[1];
                if ($attr->{rel} eq 'alternate'
                        && ($attr->{type} eq 'application/rss+xml'
                         or $attr->{type} eq 'application/atom+xml')) {
                    $feed->url(URI->new_abs($attr->{href}, $entry->link)-> as_string);
                    last;
                }
            }
        } if $self->conf->{use_auto_discovery};
        $feed->title($entry->title)
            if $self->conf->{use_entry_title};
        $context->subscription->add($feed);
    }

    $context->subscription->delete_feed($args->{feed});
}

1;

__END__

=head1 NAME

Plagger::Plugin::Filter::BreakEntriesToSubscriptions - some entry = 1 subscription

=head1 SYNOPSIS

  - module: Filter::BreakEntriesToSubscriptions

=head1 DESCRIPTION

This plugin breaks all the subscription entries into a single feed. This is
a fairly hackish plugin but it's helpful for make OPML from feeds.

=head1 CONFIG

=over 4

=item use_entry_title

Use subscription's title as a newly generated feed title. Defaults to 0.

=back

=head1 AUTHOR

Yasuhiro Matsumoto

=head1 THANKS

Tatsuhiko Miyagawa

=head1 SEE ALSO

L<Plagger>

=cut
ドキュメントにも書いた通り、ちょっと(かなり?)hackishなプラグインです。
このプラグインを使って

shibuya-pm2opml.yaml
plugins:
  - module: Subscription::Config
    config:
      feed:
        - http://b.hatena.ne.jp/t/shibuya.pm?mode=rss

  - module: Filter::BreakEntriesToSubscriptions
    config:
      use_entry_title: 1
      use_auto_discovery: 1

  - module: Publish::OPML
    config:
      title: Shibuya.pm
      filename: shibuya-pm.opml
こんなYAMLを用意すれば
「Shibuya.pm」のタグが付いている「はてなブックマーク」からオートディスカバリでフィードを取得したOPML
こんなOPMLファイルが出来上がります。

miyagawaさんに感謝

おしまい

※もしかしたらオートディスカバリ出来なかったURL(例えばPDFとか)はOPMLに含めないようにするオプションがいるかも...
Posted at by