サンデープログラミング

shibuya.pmテクニカルトーク#6のちょっと前から,AtomAPIを操作するプログラムを作ろうと思ってました.目標は「はてなブックマーク」エントリ取得と登録です.エントリ取得するコンソールアプリを作り始めたのですが,日本語環境をインストールしていないcolinuxでの出力チェックがめんどくさくなってきたので,webアプリケーションに書き直すことにしました.shibuya.pmが終わってからこっち,Sledgeperlでのwebフレームワーク人気をさらっていますが,ここは基本に忠実に(?)Catalystで行きます.っていうか,Catalystしかインストールしてません.そのうえ,モジュール(たしかData::FormValidator)がcpanでうまくインストールできなかったけど,force installでむりやり突っ込んでます(笑.

それはさておき,はてなブックマークAtomAPIとは - はてなキーワードはてなフォトライフAtomAPIとは - はてなキーワードとを見て,WSSE認証が必要だということがわかったので,LWP::Authen::Wsseをインストールしました.本当はXML::Atomをインストールした方がいいのでしょうが,インストールに失敗しました.つД`)・゜・。・゜゜・*:.。..。.:*・゜ これを使って作ったモジュールは以下のようになります.

package Atompp::M::Hatena;

use Encode qw(_utf8_off);
use base 'Catalyst::Base';
use LWP::UserAgent;
use LWP::Authen::Wsse;
use HTTP::Request::Common;
use XML::XPath;
use XML::XPath::XMLParser;
use strict;

sub query
{
	my ($self, $c) = @_;
	my ($loc, $usr, $pass, $atom);
	my ($ua, $res, $xp, $url, $ret);

	$loc  = $c->config->{loc};
	$usr  = $c->config->{usr};
	$pass = $c->config->{pass};
	$atom = $c->config->{atom};

	$ua   = LWP::UserAgent->new;
	$ret  = '';

	$ua->credentials($loc, '', $usr, $pass);

	$res = $ua->request(GET $atom);
	return $res->message if ($res->is_error);
	$xp = XML::XPath->new(xml => $res->content);

	for my $entry ($xp->findnodes('/feed/link[@rel="service.feed"]')){
		$url = $entry->getAttribute('href');
	}

	$res = $ua->request(GET $url);
	return $res->message if ($res->is_error);

	$xp = XML::XPath->new(xml => $res->content);

	for my $entry ($xp->findnodes('/feed/entry')){
		my (@node, $title, $link, $sum);
		@node  = $entry->findnodes('./title');
		$title = XML::XPath::Node::Text::string_value($node[0]);
		$ret  .= $title . "\n";
		@node  = $entry->findnodes('./link[@rel="related"]');
		$link  = $node[0]->getAttribute('href');
		$ret  .= $link . "\n";
		@node  = $entry->findnodes('./summary');
		$sum   = XML::XPath::Node::Text::string_value($node[0]);
		$ret  .= '# ' . $sum . "\n\n";
	}
	_utf8_off($ret);
	return $ret;
}
1;

細かく見ていくと,

$res = $ua->request(GET $atom);
return $res->message if ($res->is_error);
$xp = XML::XPath->new(xml => $res->content);
for my $entry ($xp->findnodes('/feed/link[@rel="service.feed"]')){
	$url = $entry->getAttribute('href');
}

の部分で,ルートAtomエンドポイントを取得しています.Hatenaから取得したXMLは以下のような形になるので,

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://purl.org/atom/ns#">
  <link type="application/x.atom+xml" rel="service.post"
        href="http://b.hatena.ne.jp/atom/post" title="sampleのブックマーク" />
  <link type="application/x.atom+xml" rel="service.feed"
        href="http://b.hatena.ne.jp/atom/feed" title="sampleのブックマーク" />
</feed>

'/feed/link[@rel="service.feed"]'とXPathで指定することでEditURIを取得できます.EditURIは,特定のブックマークを参照,タイトル/コメントの編集,ブックマークの削除を行うために使います.EditURIに対してGETすると,こんなxmlが返って来ます.ここから必要なデータを抽出することになります.

<feed version="0.3" xml:lang="ja">
<title>kdaibaのブックマーク</title>
<link rel="alternate" type="text/html" href="http://b.hatena.ne.jp/kdaiba/"/>
<link rel="service.post" type="application/x.atom+xml" href="http://b.hatena.ne.jp/atom/post" title="kdaibaのブックマーク"/>
<link rel="next" type="application/atom+xml" href="http://b.hatena.ne.jp/kdaiba/atomfeed?of=20"/>
<modified>2005-11-04T18:23:01+09:00</modified>
	<author>
<name>kdaiba</name>
</author>
<id>tag:hatena.ne.jp,2005:bookmark-kdaiba</id>
<generator url="http://b.hatena.ne.jp/" version="0.1">Hatena::Bookmark</generator>
<openSearch:totalResults>667</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>20</openSearch:itemsPerPage>
...
<entry>
<title>Skype News (スカイプ ニュース):FestoonがGoogle Talk対応</title>
<link rel="related" type="text/html" href="http://hkspage.livedoor.biz/archives/50114975.html"/>
<link rel="alternate" type="text/html" href="http://b.hatena.ne.jp/kdaiba/20051104#912140"/>
<link rel="service.edit" type="application/x.atom+xml" href="http://b.hatena.ne.jp/atom/edit/912140" title="Skype News (スカイプ ニュース):FestoonがGoogle Talk対応"/>
<issued>2005-11-04T18:12:18+09:00</issued>
<author>
<name>kdaiba</name>
</author>
<id>tag:hatena.ne.jp,2005:bookmark-kdaiba-912140</id>
<summary type="text/plain">このくっつき方は予想しなかった</summary>
<dc:subject>skype</dc:subject>
<dc:subject>google talk</dc:subject>
</entry>
...
</feed>

例えば,

for my $entry ($xp->findnodes('/feed/entry')){

というところで,各ブックマークのエントリ群を取得して,その一つ一つを$entryとして調べます.で,後は各エントリの中から

@node  = $entry->findnodes('./title');
@node  = $entry->findnodes('./link[@rel="related"]');
@node  = $entry->findnodes('./summary');

タイトル,URL,コメントを抽出するわけです.最後に

_utf8_off($ret);

として,UTF8フラグをOFFにしています.さて,このモジュールを動かすにはAtompp.pmというCatalystアプリケーション自体を表現するモジュールが必要です.ヘルパースクリプトが基本となる構造をつくってくれるので,Atompp.pmでスクリプトを書いたのはconfigとdefault:Privateです.それぞれの中身は以下のようになります.

Atompp->config(
	name => 'Atompp',
	loc  => 'b.hatena.ne.jp:80',
	usr  => 'kdaiba',
	pass => 'PASS',
	atom => 'http://b.hatena.ne.jp/atom',
);
sub default : Private {
	my ($self, $c) = @_;
	my ($text);
	$text = Atompp::M::Hatena->query($c);
	$c->res->headers->header('Content-type' => 'text/plain; charset=UTF-8');
	$c->res->headers->header('Charset' => 'UTF-8');
	$c->res->output($text);
}

スクリプトを作るにあたって苦労したのがconfigです.まずuseridがhatenaのサービスにログインするときに使っているメールアドレスだと思っていたので,そこでひっかかり,次にWSSE認証するところでひっかかりました.はてなフォトライフAtomAPIとは - はてなキーワードでは,

$ua->credentials('f.hatena.ne.jp', '', 'username', 'password');

のひとつめの引数のようにFQDNを指定するのかと思っていたんですが,やってみると'b.hatena.ne.jp:80'のようにポート番号も指定する必要があるようです.それから,タイトル欄の文字列がXMLとして正当と判断されないためXPathが使えない場合がありました.

そんなこんなで苦労した後に見つけたのが,Catalyst-Plugin-AtomPP-0.04 - Dispatch AtomPP methods with Catalyst. - metacpan.orgというモジュール.作ってるのは日本の方で,こんなブログを見つけました.http://www.unknownplace.org/blosxom/coding/atom_implementation_with_catalyst.htmlさらに気がついたのがこれ.アルファギークな方々のブックマークを公開するサービス.2005-11-05 - Accept Thingsベースになっているのは XML::Atomで任意のはてなユーザの はてなブックマークをコンソールにリストアップするテスト - Accept Thingsのようです.今回作ったものと似たようなアルゴリズムを持っていると思いますが,こちらはXML::Atomを使っています.

それにしても,自分のエントリしか取得できないと思っていたんだけど,どうやって他の人のエントリが取得できるんだろう.その仕様がどこにあるのかがわからないっす.つД`)・゜・。・゜゜・*:.。..。.:*・゜