2008/01/31


私は、Plaggerを使用して、はてなブックマークを別のソーシャルブックマークに同期していますが、その際ブックマークタグがどのように扱われるかについて以下説明します。
はてなブックマークのフィードは
RSS形式
http://b.hatena.ne.jp/[hatena account]/rss もしくは
AtomFeed形式
http://b.hatena.ne.jp/[hatena account]/atomfeed というURLで提供され、データには以下の様な物が含まれています。 ※以下の例はRSSの場合
<item rdf:about="http://coderepos.org/share/changeset/552">
    <title>Changeset 552 - CodeRepos::Share - Trac</title>
    <link>http://coderepos.org/share/changeset/552</link>
    <description>笑わせて頂きました</description>
    <content:encoded>
      &lt;blockquote cite="http://coderepos.org/share/changeset/552" title="Changeset 552 - CodeRepos::Share - Trac"&gt;
        
        &lt;cite&gt;&lt;a href="http://coderepos.org/share/changeset/552"&gt;Changeset 552 - CodeRepos::Share - Trac&lt;/a&gt; &lt;a href="http://b.hatena.ne.jp/entry/http://coderepos.org/share/changeset/552"&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;笑わせて頂きました&lt;/p&gt;
    </content:encoded>
    <dc:date>2007-10-19T21:19:44+09:00</dc:date>
    <dc:creator>mattn</dc:creator>
    <dc:subject>coderepos</dc:subject>
    <dc:subject>erogeek</dc:subject>
    <taxo:topics>
      <rdf:Bag>
      <rdf:li resource="http://b.hatena.ne.jp/t/coderepos" />
      <rdf:li resource="http://b.hatena.ne.jp/t/erogeek" />
      </rdf:Bag>
    </taxo:topics>
</item>
ブックマークした元リンクのtitle/linkに加え、ブックマークコメントが格納されたdescription、およびblockquote/citeタグを使用して引用元形式に表現されたcontent:encoded、さらにはブックマークタグを表現するdc:subjectが記述されています。
Plaggerの場合、descriptionよりもcontant:encodedを優先しており、コメントとしては冗長な引用部分が転送されてしまいます。これについては先日書いた「Plaggerで、はてなブックマークをdel.icio.usにミラーする時に、descriptionフィールドを衛生的に修正するフィルタプラグイン書いた」にある様にdescriptionをcontent:encodedに上書きしてやる事で対応出来ます。
先日この記事を書いた際、otsuneさんから「この目的であれば、 b.hatena.ne.jp/[hatena user]/atomを Filter::AtomLinkRelated すればOk」というブックマークコメント頂きました。
昨日、頂いたアドバイスの通りAtomFeedで試して見た所、複数設定した筈のブックマークタグが一つだけしか適応されないという現象が発生しました。
IRC(#plagger-ja)でotsuneさん、国内滞在説のあるmiyagawaさんに相談しながら原因を当たった所、昨日の夜にXML::Feedでのdc:subjectの扱い方に問題があるのではないかという事が分かりました。

ここで見て頂きたいのはdc:subjectというノード。dc:subjectは私の記憶ではAtom0.3では厳密に個数は規定されておらず、複数記述する事も出来てしまっています。結果、規定されていないことで色んな実装が表れてしまっています。
はてなの様に複数のdc:subjectを使って表現する物もあれば、del.icio.usの様に一つのdc:subject内に空白(スペース)等でセパレートしてタグを記述している物もあります。
以下、私が簡単に調べた各サービスのフィード出力状況と、そのフィード内のdc:subjectの扱われ方です。
サービス フィード形式 dc:subjectの扱い
はてなブックマーク RSS1.0
Atom0.3
タグ毎にdc:subject
del.icio.us RSS1.0 単一のdc:subjectを空白でセパレート
Livedoor Clip RSS2.0 タグ毎にdc:subject
Buzzurl RSS1.0 タグ毎にdc:subject
Goo Bookmark RSS1.0 出力されない
FC2 Bookmark RSS2.0
※1
出力されない
Pookmark Airlines RSS1.0 タグ毎にdc:subject
※2
Nifty Clip RSS1.0 タグ毎にdc:subject
※3
Blue Dot RSS2.0 単一のdc:subjectをカンマでセパレート
Digg RSS2.0 出力されない
※1 このフィードはちょっと頂けない
※2 入力UIは単一行だがダブルクオート記述出来る
※3 入力UIはjavascriptで追加形式(POSTは1個もしくは配列)
各サービス毎にdc:subjectの扱われ方はまちまちです。
これらの仕様をXML::Feedがどのように扱っているかが原因ではないかと思いました。
現状、XML-Feed-0.12のソースでは

lib/XML/Feed/Atom.pm(146): sub category {
    my $entry = shift;
    my $ns = XML::Atom::Namespace->new(dc => 'http://purl.org/dc/elements/1.1/');
    if (@_) {
        $entry->{entry}->add_category({ term => $_[0] });
    } else {
        my $category = $entry->{entry}->category;
        $category ? ($category->label || $category->term) : $entry->{entry}->get($ns, 'subject');
    }
}
となっていますが、上記"get"ではXML::Atomの"get"が呼ばれ、ARRAYの先頭しか返りません。
XML::Atomには"get"ではなく"getlist"も用意されており、こちらの方はARRAYを返してくれる仕様になっています。
(XML::Feed::RSSの方は元々categoryでARRAYを返す場合もある為、baseであるEntryは既にARRAYを返されても問題ない準備が出来ています)

dc:subjectが単一とは規定されていない事、XML::Atomで"getlist"が用意されている事を、XML::FeedのAUTHORであるBenjamin Trott氏にメールし、パッチも付けて送付しました。
どんな返事が返って来るか分かりませんが、これが正しい修正だとすればXML::Feedのアップグレードで直って来るかもしれません。

しばらくは、はてなブックマークからの同期はrssフィードを使いdescriptionからcontent:encodeを上書きするようなトリックを使うか、AtomFeedを使ってしかも上記の様な修正を入れて対応するかになります。
もしかしたら、Plagger側にcontent:encoded->summaryではなく、description->summaryとなるようなオプション入れても良いかも知れませんね。
それかtsupoさんのbookeyを使うってのもアリですね。
Posted at by




先日書いた「個人的ソーシャルブックマークサービスの歩き方」という記事にもある通り、私は個人的な資料を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




Journal of miyagawa (1653)
TEXTや@srcといったショートカット結果に対して任意のフィルタをかませる事が出来るようになったようです。
これまでのように process "span.entry-content", comment => 'TEXT';
と指定していた部分を process "span.entry-content", comment => [ 'TEXT', 'MyFilter' ];
と記述出来るようになったのです。
MyFilterは「Web::Scraper::Filter::MyFilter」というパッケージで定義され、filterプロシージャが呼び出されます。

さっそく、twitterの発言では70%近くが英語のmiyagawaさんの発言をスクレイピングし、エキサイト翻訳で日本語にフィルタするサンプルを作って見ました。
package Web::Scraper::Filter::EnglishToJapanese;
use base qw( Web::Scraper::Filter );
use warnings;
use LWP::UserAgent;
use HTTP::Request::Common qw(POST);

sub filter {
    my($self, $value) = @_;
    my $req = POST( 'http://www.excite.co.jp/world/english/',
        [before => $value, wb_lp => 'ENJA'] );
    my $data = $Web::Scraper::UserAgent->request($req)->content;
    $data =~ s!\x0D|\x0A!!g;
    $data =~ s/^.*?<textarea[^>]*name="after"[^>]*>(.*?)<\/textarea>.*?$/$1/;
    return $data;
}

1;

use URI;
use Web::Scraper;

my $twitter = scraper {
    process 'td.content',
        'comments[]' => scraper {
            process "span.entry-content", comment => [ 'TEXT', 'EnglishToJapanese' ];
        };
    result 'comments';
};
my $comments = $twitter->scrape( URI->new("http://twitter.com/miyagawa/") );
use YAML;
warn Dump $comments;
で、結果
---
- comment: ' ウェブログを作られた http://tinyurl.com/2xldch '
- comment: ' ウェブを出荷します:、:フィルタサポートがある削り器0.21_01。 バージョン番号が言うようにこれがdevリリースであるのに注意してください。'
- comment: ' 見ます。'
- comment: ' ダッシュボード懺悔室、Yellowcard、少年は少女が好きです: 多くの誘惑が今月のSF warfieldで http://www.ticketmaster.com.. を見せます。 ... '
- comment: ' 100のコメント. diggの上のトップページングのためのtakesako、おめでとう、ワオ490、diggs、 http://tinyurl.com/255ht7 '
- comment: ' スクリーンからStreoパート2までNFGを聞くのがあります。 輝かしいアルバム'
- comment: ' @hanekomu、うん、それはしゃぶられます。 そこでは、日本で同じです。 請求先の住所が米国にある状態で、運よく私はcreditcardsを持っています。'
- comment: ' Dashboard Confessionalの新しいアルバム http://www.amazon.com/gp/pr.. を購入した、アマゾン'
- comment: ' より古いリンクの取り逃がすことは一時的であるように思えました。私がfriendfeedされることへのスイッチかJaikuに好きでないので、 http://tinyurl.com/2gq4uj は少し救いました。'
- comment: ' したがって、さえずりの丁付けはいつまでも、行きましたか? そうだとすれば、私は、確実にさえずりを使用するのを止めるつもりです。'
- comment: ' http://subtech.g.hatena.ne... のウェブログを作りました。 Yappo++typester++Plagger++'
- comment: ' @Yappo++'
- comment: ' IT Crowd s02e05を見ます。'
- comment: ' ep1を見ます。'
- comment: ' 12ドルでmonoprice.comからの外でコンポーネントケーブルと結合器を私のPSPビデオに購入しました。 すさまじい値'
- comment: ' 作成されて、playstationのための削り器は、給送 http://tinyurl.com/yqbtjb plagger++ウェブを格納して、発行しています:、:削り器++'
- comment: ' http://feeds.feedburner.com.. に加入しました。'
- comment: ' 私の2週間のabsenseでは、私はWaMu、Master、およびCapitalOneから10の+クレジットカード申し出を受けました。 ため息をついてください。'
- comment: ' ビールを飲みます。'
エキサイト翻訳の部分をスクレイピングするんが筋ちゃうんかいな!というツッコミは無しでお願いします。
Posted at by