Terminal上で動作するTwitter閲覧ツール「Twiterm」を作った - すぎゃーんメモにて、初めてAnyEvent::Twitterというモジュールを使ってみたのだけど、使い方を激しく勘違いしていたので、正しい使い方をメモっておく。
AnyEvent::Twitter - search.cpan.org
バージョン
2009年9月末時点では最新は0.26。
最も簡単なサンプル
use strict; use warnings; use AnyEvent; use AnyEvent::Twitter; # ユーザー名、パスワードを引数から取得 my ($username, $password) = @ARGV; my $twitty = AnyEvent::Twitter->new( username => $username, password => $password, ); # コールバックの登録 $twitty->reg_cb( statuses_friends => sub { my ($twitty, @statuses) = @_; # とりあえず取得で来たstatusの数だけ表示 print scalar @statuses, "\n"; }, ); # 観測開始 $twitty->receive_statuses_friends; $twitty->start; # プログラムが終了しないよう別のイベントループを回す my $cv = AnyEvent->condvar; my $w; $w = AnyEvent->timer( after => 0, interval => 5, cb => sub { print AnyEvent->time, "\n"; }, ); $cv->recv;
実行結果は以下のようになる。
$ perl twitty.pl sugyan ******** 1254348322.7404 198 1254348327.75838 1254348332.76877 1254348337.77918 1254348342.78943 1254348347.79971 1 1254348352.80151 1254348357.81191 1254348362.82232 1254348367.83263 1254348372.84291 2 1254348377.85351 1254348382.8638 1254348387.87423 1254348392.8845 1254348397.89477 2 1254348402.9019 1254348407.91221 1254348412.92259 1254348417.93377 1254348422.94404 1254348427.95658 1 1254348432.96256 1254348437.97293
タイマーは5秒毎に時間を表示し、それと並行してAnyEvent::Twitterが定期的にfriends_timelineを取得し、新たに取得できた件数を表示している。
(1回きりで200件を取ってくるものだと思っていた…orz)
statuses_friendsコールバック
ここで渡されてくる引数、1番目は動いているAnyEvent::Twitterのインスタンスそのまま。では2番目(以降)の@statusesに渡ってくるのは何か?
reg_cvでのstatuses_friendsのコールバックをちょっと変更してみる。
# コールバックの登録 $twitty->reg_cb( statuses_friends => sub { my ($twitty, @statuses) = @_; if (@statuses > 0) { my $status = shift @statuses; print "@$status\n"; print join(',', keys %{$status->[0]}), "\n"; print join(',', keys %{$status->[1]}), "\n"; } }, );
$ perl twitty.pl sugyan ******** 1254351016.40435 1254351021.41007 HASH(0x948f30) HASH(0x90c7c0) timestamp,text,screen_name source,truncated,favorited,created_at,text,user,in_reply_to_user_id,id,in_reply_to_status_id,in_reply_to_screen_name 1254351026.42057
このへんはpodにしっかり書いてあるのでドキュメントを読めば分かるのだけど、
- @statusesは各statusに対応するARRAYリファレンスを格納した配列
という構造になっている。
state
ところで上記のstatuses_friends、1回目はガツっと200件ほど取ってくるけど、2回目以降は新たに取得できたものだけが@statusesに入ってくる。更新がなければ空の配列になる。
どこでそういう制御をしているかというと、内部でstateというものを持っていて、それを使っている。
またstatuses_friendsを変更してみる。
# コールバックの登録 $twitty->reg_cb( statuses_friends => sub { my ($twitty, @statuses) = @_; print scalar @statuses, "\n"; if (@statuses > 0) { print $statuses[0]->[1]->{id}, "\n"; } use YAML; print Dump $twitty->state; }, );
$ perl twitty.pl sugyan ******** 1254351935.7121 200 4509801997 --- statuses: friends: id: 4509801997 1254351940.72949 1254351945.73973 1254351950.74998 1254351955.75037 1254351960.76067 5 4509812919 --- statuses: friends: id: 4509812919 1254351965.77954 1254351970.79002 1254351975.79041 1254351980.80073 1254351985.81098 3 4509820045 --- statuses: friends: id: 4509820045 1254351990.82985 1254351995.84023
このように、取得できた@statusesの先頭要素(つまり最新のstatus)のidが常にstateに記録されている。
内部ではAPIからfriends_timelineを取得する際にこのid以降のものを取得するよう指定しているので、更新分だけがコールバックに渡されるようになる。
またこのstateは初期化時に引数で渡すことができ、例えば処理を中断する際に、中断前にstateを保存しておくと、それを使って新しくAnyEvent::Twitterのインスタンスを作ってやれば中断前の続きからstatusを取得することが出来るようになる。
my $twitty = AnyEvent::Twitter->new( username => $username, password => $password, state => { statuses => { friends => { id => 4509820045, }, }, }, );
このように指定してやると、最初にいきなり200件とってくることもなく、指定したid以降のものだけを取ってくることになる。
その他のコールバック
reg_cbにはもう2つコールバックを登録できる。
error
エラーが起こったときの処理を登録できる。
# コールバックの登録 my $watcher; $watcher = $twitty->reg_cb( error => sub { my ($twitty, $error) = @_; print "Error: $error\n"; undef $watcher; }, );
$ perl twitty.pl sugyan hogefugapiyo 1254354336.81342 Error: error while fetching statuses for friends: 401 Unauthorized 1254354341.82181 1254354346.83121
認証エラーの場合、errorが何度も呼ばれてしまうので$watcherをundefして二度と呼ばれないようにしている。
next_request_in
TwitterのAPIには制限があり、1時間に150回までしかstatusの取得などはできない。1時間ごとに制限の値はリセットされる。
next_request_inでは、次にリセットされるタイミングまでの時間、それまでの間のAPIの使用可能回数、そしてそれらから算出した次回実行までの時間(後述)を受け取る。
# コールバックの登録 my $watcher; $watcher = $twitty->reg_cb( next_request_in => sub { my $self = shift; my ($seconds, $remaining_request, $remaining_time) = @_; print "seconds : $seconds\n"; print "remaining_time : $remaining_time\n"; print "remaining_request : $remaining_request\n"; }, );
$ perl twitty.pl sugyan ******** 1254355336.83357 seconds : 23.2765011119348 remaining_time : 3140 remaining_request : 142 1254355341.84632 1254355346.85656 1254355351.86685 1254355356.87708 1254355361.88741 seconds : 23.2624113475177 remaining_time : 3116 remaining_request : 141 1254355366.89266 1254355371.90285 1254355376.91329 1254355381.92353 1254355386.93091 seconds : 23.2406015037594 remaining_time : 3091 remaining_request : 140 1254355391.94673
だいたい、status_friendsなど取得したときのコールバックが実行された後に呼ばれることになる。取得したstatusに対する処理はそちらで書いて、実行タイミングの調整(後述)などはこのコールバック内で行うのが良いと思う。
タイムラインの取得間隔
statusesを繰り返し取得する際、その更新間隔を内部でどうやって決定しているのか。
AnyEvent::Twitterは、初期化時に引数で"bandwidth"というパラメータを渡せる。これは、APIの回数制限に対しどれだけの頻度でアクセスを行うか、を決める値になる。
例:
my $twitty = AnyEvent::Twitter->new( username => $username, password => $password, bandwidth => 0.5, );
たとえば普通にfriends_timelineの取得を繰り返し行うだけ、という場合は3600秒間に150回使用できるので最短24秒間隔でアクセスすることができる。しかし、他のクライアントからも同時にAPIを使用している、といった場合、このプログラムだけで許容量を使い切ってしまうわけにはいかない。それを調整するための値がこの"bandwidth"。これを使って、次の取得までの間隔を(remaining_time / remaining_request) / bandwidth という式で決定する。これを1.0にすれば150回/時間の制限を使い切ることになるし、1未満の数字にすれば時間間隔はそれより長くなりAPI制限に対して余裕ができるようになる。いちおう1以上の値を設定することもできる。何も指定しなかった場合はデフォルトで0.95に設定されるらしい。
ちなみに「xx秒間隔で取得したい」という場合は、podのnewメソッドの引数としては明記されていないけれど"interval"で秒数を指定すればbandwidthより優先されてその時間間隔で取得できるようになる。
my $twitty = AnyEvent::Twitter->new( username => $username, password => $password, interval => 5, );
例えばこうすると必ず5秒間隔で取得しにいくようになる。あっという間にAPI使用制限に達してしまうだろうけどw また、微妙にundocumentedなカンジなのであんまり使わない方がいいのかも?
friends_timeline以外に取得できるもの
AnyEvent::Twitterで取得できるのはfriends_timelineだけでなく、mentionsも取得できる。podには
friends
public (currently unimplemented)
user (currently unimplemented)
mentions
と書いてあり、public_timeline, user_timelineに関してはまだ未実装のため取得できない。まぁ仕組み的にそんなに他とそんなに変わらないはずなのでサクッとパッチを書いてあててやれば使えるようになる気がするけど。
friends_timelineとmentionsを並行して取得する
取得イベントとして登録しておけば、friends_timelineとmentionsを1つのインスタンスから取得できる。
use strict; use warnings; use AnyEvent; use AnyEvent::Twitter; # ユーザー名、パスワードを引数から取得 my ($username, $password) = @ARGV; my $twitty = AnyEvent::Twitter->new( username => $username, password => $password, bandwidth => 0.8, ); # コールバックの登録 $twitty->reg_cb( statuses_friends => sub { my ($twitty, @statuses) = @_; print "friends: ", scalar @statuses, "\n"; }, statuses_mentions => sub { my ($twitty, @statuses) = @_; print "mentions: ", scalar @statuses, "\n"; }, next_request_in => sub { my $self = shift; my ($seconds, $remaining_request, $remaining_time) = @_; print "seconds : $seconds\n"; print "remaining_time : $remaining_time\n"; print "remaining_request : $remaining_request\n"; }, ); # 観測開始 $twitty->receive_statuses_friends; $twitty->receive_statuses_mentions; $twitty->start; # プログラムが終了しないよう別のイベントループを回す my $cv = AnyEvent->condvar; my $w; $w = AnyEvent->timer( after => 0, interval => 5, cb => sub { print AnyEvent->time, "\n"; }, ); $cv->recv;
単純に$twitty->receive_statuses_mentionsとそれを受けるコールバックを追加しただけ。
$ perl twitty.pl sugyan ******** 1254364086.29058 1254364091.28648 mentions: 199 seconds : 19.8586956521739 remaining_time : 1827 remaining_request : 115 1254364096.28652 1254364101.28655 1254364106.28656 1254364111.28656 friends: 199 seconds : 19.7916666666667 remaining_time : 1805 remaining_request : 114 1254364116.28651 1254364121.28656 1254364126.28656 1254364131.28656 mentions: 0 seconds : 19.921875 remaining_time : 1785 remaining_request : 112 1254364136.28652 1254364141.28656 1254364146.28656 1254364151.28656 friends: 1 seconds : 19.8761261261261 remaining_time : 1765 remaining_request : 111 1254364156.28649
friends_timelineとmentionsが交互に取得されているのが分かる。
取得するタイムラインの頻度を決める重み付け
「フォロワーたくさんいるからfriends_timelineは頻繁に見たいけど@は滅多に飛んでこないからmentionsはそんなに何度も見にいかなくていい」という場合もあるかもしれない。
そんなときは、receive_status_... に引数として重み(weight)をそれぞれ渡してやれば良い。
これを指定すると、それぞれの重みに応じてAPIから取得するタイムラインを調整してくれる。例えばfriends_timeline:mentinsを3:1に設定してやると、
$twitty->receive_statuses_friends(3); $twitty->receive_statuses_mentions(1); $twitty->start;
$ perl twitty.pl sugyan ******** 1254364955.25351 friends: 200 seconds : 12.7925531914894 remaining_time : 962 remaining_request : 94 1254364960.24945 1254364965.24947 1254364970.24946 friends: 0 seconds : 12.755376344086 remaining_time : 949 remaining_request : 93 1254364975.24946 1254364980.24948 1254364985.24939 mentions: 199 seconds : 12.6766304347826 remaining_time : 933 remaining_request : 92 1254364990.24945 1254364995.24946 friends: 0 seconds : 12.6373626373626 remaining_time : 920 remaining_request : 91 1254365000.2494
このように4回中3回はfriends_timelineを、1回だけmentionsを取得するようになる。API制限はどちらも合わせて150回/時間までなので、取得する時間間隔に関しては前述の仕組みの通りになる。
投稿(status_update)
ここまでのstatusesの取得とは別に、指定した文字列でstatusをupdateすることもできる。これもイベント駆動なインターフェースなので他の処理をブロックすることなく、投稿に成功したときにコールバックサブルーチンが呼ばれることになる。
前述までのサンプルではAnyEvent::Twitterの処理と別にAnyEventのタイマーを回していたが、これを以下のように書き換えてみる。
# プログラムが終了しないよう別のイベントループを回す my $cv = AnyEvent->condvar; my $w; $w = AnyEvent->io( fh => \*STDIN, poll => 'r', cb => sub { chomp(my $input = <STDIN>); use Encode qw/decode_utf8 encode_utf8/; $twitty->update_status(decode_utf8($input), sub { my ($twitty, $status, $js, $error) = @_; print "update!\n"; print "status : ", encode_utf8($status), "\n"; print "error : $error\n" if defined $error; }); }, ); $cv->recv;
どうやらdecode_utf8してからじゃないと日本語が化けるっぽい。
こうすると、タイマーは回らずに標準入力からの入力待ちになり(タイムラインの取得は前述までと同様に回っている)、何か文字を入力するとその文字列をTwitterに投稿する。
上記の例の場合イベントループは終了しないので入力するたびに投稿されることになる。
閲覧/投稿ができる簡単Twitterクライアント
ここまでの機能を理解していれば、自動でタイムライン(friends_timeline, mentions両方)を定期的に取得しては出力し、文字を入力すると投稿する、という簡単なターミナル向けクライアントを作成できる。
ここだけgist使うw