IRCクライアントでMyはてなやRSS FeedをウォッチするためのIRCdを書いた

はてなのお気に入りアンテナをTwitter Clientで見れるゲートウェイ「Hatetter」作りました と同じようなことを,Twitterクライアントではなく,IRCクライアントでやりたかったのです.

というわけで,Pigというのを書いてgithubにあげてあります.

http://github.com/hakobe/pig

MyはてなやRSS Feedの新着項目を発言してくれるIRCdです.そのうち,Perlでごにょごにょした結果をIRCdになげるライトな感じのフレームワークになる予定です.

追記: Mooseの依存がひどいらしいのでとりあえずAny::Moose化したので,すこしは入れやすくなってる?

Myはてなをウォッチ

とりあえず以下のようにすると,IRCクライアントでMyはてながウォッチできます.

$ git clone git://github.com/hakobe/pig.git
$ cd pig
$ vim config/myhatena.yaml # あなたのhatena_idや更新間隔をせっていしてね!
$ perl -Ilib script/pig.pl config/myhatena.yaml

ローカルの16667ポートでIRCdが起動するので,お使いのIRCクライアントで接続してください.

IRCクライアントから特定のチャンネルにjoinすると,Myはてなの新着情報がながれて来ます.チャンネルに流れてくる新着情報の種類はチャンネルの名前で決まります.

  • #antenna : あなたのMyはてなアンテナの新着
  • #{hatena_id} : id:{hatena_id} さんの新着

これで,気になるあの子をウォッチしまくりです.


RSSをウォッチ

あと,副作用的にRSSもIRCクライアントからチェックできるようになったのですが,意外と便利です.

$ cd pig
$ vim config/feed_reader.yaml
$ perl -Ilib script/pig.pl config/feed_reader.yaml

feed_reader.yamlには,Feedを流したいチャンネルとFeedのuriの組を書いておきます.チャンネルにjoinすると指定したFeedがながれてきます.

port: 16668
log_level: debug
service:
  name: FeedReader
  interval: 300 # 5分毎にチェック
  bot_name: feed # feedをしゃべるbotの名前
  channels:
    '#fse':
      feeds:
        - uri: http://www.douzemille.net/labs/urlstack/rss.cgi
        - uri: http://www.himitsu.net/himitsunofeed
    '#gmail': # Gmailの新着
      feeds:
        - uri: https://id:pass@mail.google.com/mail/feed/atom/
    '#gmail-hatena': # HatenaラベルのついたGmailの新着
      feeds:
        - uri: https://id:pass@mail.google.com/mail/feed/atom/hatena

IRCはずっと見ているので,常にチェックしたいFeedや,LDRとかに登録できない認証付きのFeedを登録しておくと便利です.

上の設定例のようにGmailのAtom FeedをIRCで流しておくのが便利です.特にはてなから来るメールをIRCでウォッチしておくと,「id:hakobe932さんのダイアリーにコメントが付きました」みたいなタイトルに反応してIRCクライアントが教えてくれるので,idコールとかがすぐにわかって便利ですね.*1


まとめ

というわけで,IRCクライアントでMyはてなやRSSをウォッチするIRCdでした.

とりあえずなんでもIRCクライアントでみれるようにしておくと,いろんなツールを切り替える必要がなくて楽ちんです.それに,IRC周辺にはいろいろとツールがそろっているので,呼ばれたときに通知してもらえたり,ログ取りが簡単だったり,mobircなどを使ってモバイルでもいつでもみれたりして何かと柔軟に利用できます.IRCとても便利.

pig自体はまだいろいろ低機能で完成度が低い感じですが,IRCdに乗せたいことが増えるたびに高機能になっていく予定でございます.もし何かしら,IRCdに乗せたい機能があればぜひ教えてください.

おまけ: 実装について

フレームワークにあたる部分は,POE::Compoenent::Server::IRCをイベントをhookしやすいようにちょっとだけラップした感じになっています.POE::Compoenent::Server::IRCはIRCのプロトコルを細かく調べなくても簡単にIRCdが書けて便利です.pigでは加えて,定期的にRSSをチェックしにいくイベントをPOE上で起こして,それがhookできるようになっています.

ここはまだ,ぜんぜん低機能で,hookできるイベントがかなり制限されてたり*2,定期的にチェックルーチンをまわすくらいしかできません.今後に期待です.使うときに追加される予定です.

MyはてなやRSSを読む部分は,Serviceという種類のクラスになっていて,一つのpigごとに一つ渡されます.Serviceはいまのところ,フレームワークから渡されたイベントを処理できればなんでも良くなっています.Serviceを書けば,簡単にIRCdに機能を載せれるようになるのが目標です.

全体的な設計はまだいまいちな感じなので,綺麗に抽象化したいところです.いまはメインクラスにPOE::Compoenent::Server::IRCのイベント処理がいってたりするので,それをうまく分割したりとか.(POEのイベントを扱うところが複雑になりがちなんだよなぁ.) Serviceもちゃんとインターフェースきめたいです.

*1:ただし,Gmail Atom Feedの日付がおかしくなって同じ通知が何度もされたりすることがあるので注意です

*2:今回のはjoin/partくらいしかhookしなくてよかった

Kansai.pmでコルーチンについて発表してきた

Kansai.pm#11にて「Perlで学ぶコルーチン」という発表をしてきました.

だいぶ前のRuby勉強会でRuby 1.9のFiberをみてPerlでもいろいろやってみていたので,その時しらべたことを中心にぐだぐだとしゃべりました.

コルーンは継続や並行処理などいろいろな概念がからんでいて調査がたいへんでした.PerlでのCoroの実装がどうなっているのかもう少し詳細に調査/発表できたらよかったです.

スライドにも書いてますが,Ruby 1.9のFiberとまったく同じインターフェースをもったFiber.pmをつくってみました.githubで 公開しています.

http://github.com/hakobe/perl-fiber/tree

以下のように簡単にFiber(=コルーチン)をつくれます.Coro::Stateとちがってコルーチン中断したとき(= Fiber->yield時)に値が返せるので便利です.

use Fiber;
my $factorial = Fiber->new(sub{
    my $n   = 1;
    my $val = 1;
    while (1) {
        $val = $val * $n++;
        Fiber->yield($val);
    }
});
$factorial->resume; #1
$factorial->resume; #2
$factorial->resume; #6

どうぞご利用ください.

今回は聞いていて勉強になる発表がいろいろあって,非常に参考になりました.なかでも吉田さんの発表からはアーキティクチャとアルゴリズムの知識の重要さがすごく伝わってきました.アーキティクチャとアルゴリズムの知識をどういんして処理速度を300倍にするとかすごすぎる.古き良き知識もしっかりみにつけたいですね.

ともあれ,運営のみなさま,ホストのid:naoyaさん,ほかみなみなさま,本日はどうもありがとうございました.次回もたのしみですね.

Remedieでハルヒちゃん見…れなかった

Remedieでハルヒちゃんを見ようと思って,YouTubeのKadokawa Anime Channelを登録して表示しようとすると

Not an ARRAY reference at /Users/yohei/lang/perl/5.8.8/lib/perl5/XML/Feed/Format/RSS.pm line 256.

と言われて動画が見れず悲しかったので,原因を追いかけてみた.

どうやら,XML::FeedのXMLパーサをXML::RSS::LibXMLに設定しながらentryのcategoryメソッドを呼ぶとだめみたいだなー.(categoryメソッド内でARRAYが期待されてるところでXML::RSS::LibXML::MagicElementオブジェクトが現れる)

以下のコードで再現した.

Remedie側では,lib/Plagger/Plugin/Aggregator/Simple.pm のcategoryのエラーをハンドリングすればとりあえず,そこでとまることはなくなった.

わーい,やっとハルヒちゃん見れるよー.


orz

CoroはCoroutineのCoro - PerlでFiber

Ruby勉強会@関西でFiberについて勉強してきた - はこべブログ ♨でPerlでもGeneratorをうまく書けるモジュールがないかなーとこぼしていたところ.

perlではCoroでほぼ同じ事ができるが、どちらにしろCoroを使う利点は1個1個をresumeすることじゃなくて 非同期に実装することだと思うのでgenerator云々はちょっと用途が違う気もするです

http://b.hatena.ne.jp/lestrrat/20090202#bookmark-11922880

というブコメをいただきました.id:lestrratさんありがとうございます.

Coroについて調べてみたところ,Coro::Introによると

The natural application for these is to include a scheduler, resulting in cooperative threads, which is the main use case for Coro today.

とあって,やはり,スケジューリングなどもおこなう,コルーチンよりずいぶん高機能なものになっているみたいです.ただし,

Coro started as a simple module that implemented a specific form of first class continuations called Coroutines.
...
This is nowadays known as a Coro::State.

とあるように,もともと持っていた純粋なコルーチンの機能はCoro::Stateというモジュールで実現できるようです.

Coro::Stateにはコルーチンをきりかえるtransferというメソッドが用意されているのですが,このメソッドはRuby1.9のFiberのyieldのように値を返すことができません.コルーチン切り替え時の値は別に記録しておく必要があります.このあたりを注意して,以下のようにGeneratorを実装してみました.

56803’s gists · GitHub
yieldで値を返せないぶん一つ余計にクロージャが必要になっているので,ちょっとくどいコードになってます.しかし,ここはPerlというかCPANというか,このコードをうまくラッピングしてくれるモジュールであるCoro::Generatorがありました.

Coro::Generatorを利用すると,同じコードが以下のように書けます.

coro_generator.pl · GitHub
ずいぶんコードの意図がわかりやすくなりました.

というわけで,RubyのFiberと同じようなことがしたければ,PerlではCoroを使えってことですね.Coro自体はコルーチンの実現以外に並列処理を簡単に書ける仕組みがいろいろあって便利そうなので,うまく使えるとかっこよいですね.

Parse::RecDescentでJSONをパース

JSON::Hatchet の構文解析子 - Tociyuki::DiaryのJSONパーサがすっきりと書けていたのものだから,ちょっとうちもJSONパースしたくなってので書いてみました.といってもLL構文のパーサを1から書くのも芸がない感じだったので.Parse::RecDescentというCPANモジュールを使ってJSONのパーシングをしてみました.

Parse::RecDescentはその名のとおり,汎用の再帰下降型パーサです.LL(1)文法にのっとっていれば,パースが可能です.*1

id:tociyukiさんがの記事にあるBNFを流用させてもらい,JSONのパーサを書くと以下のようになりました.

このように,BNFっぽいものを記述するだけでパースを行うことができてお手軽です.パターンとして正規表現を書くこともできて,スキャナーに相当することもやってくれてます.

構文規則に合わせてパージングにともなう処理を記述できます.各要素の左にあるブレースの中がそれです.処理部分を省略すると,マッチした構文要素がそのまま返ります.ここでは,arrayやobjectのパース時に,これらに相当するPerlのArrayとHashを生成して要素を追加するようにしています.

構文規則とそれにともなう処理をまとめて書けるので,ちょっとした記法(特殊なURLとかおれおれWiki記法みたいまもの)をパースするのに便利そうです.Parse::RecDescent自体,BNFをパースして実行するDSL*2を提供していて,内部でevalとかしまくってるぽいので,このコードを読んでみるのもおもしろそうですねー.

*1:もうすこし拡張された文法もパースできるっぽい?

*2:というか文字列をパースしてるのでDSLではないか

IO::MooseでもSlurpできるよ

JPerl Advent Calendar 2008でファイルをslurpする方法を紹介したけど,IO::Mooseっていうモジュールもあったんだねぇ.

#!/usr/bin/env perl
use strict;
use warnings;
use Perl6::Say;
use IO::Moose qw(File);

my $content = IO::Moose::File->new( filename => './pl.pl' )->slurp;
say $content;

といってもわざわざMooseなモジュールを使う必要はないかなぁ.モジュールがMooseで書かれてる利点はなんだろ.自分がモジュールを作るときに使えるときれいに使えるだろうけどなぁ.

perldoc perlxstutを読んだときのメモ

正月番組をみながら[http://perldoc.perl.org/perlxstut.html#EXAMPLE-3:title=perldoc perlxstut]を読んだ.メモをとったのでせっかくだからはっつけておくよ.

ちょっとメモがはしょり気味なのと,うちの理解があやしいところがあってこころもとないですが,perlxstut読むときの参考にでもなればばば.

Example1

$ h2xs -A -n Mytest

で雛形がつくれる.生成されたMytest.xsにXSのコードを書く

値を返さないHello, WorldだとCODE:以下にprintfを書くだけでOK

void
hello()
    CODE:
        printf("Hello, world!\n");

CODE: みたいな部分はXS特有の書き方.あとで展開されてCのコードになる.

$ perl Makefile.PL
$ make

するとビルドされる.このとき作られるMytest.cをみるとXSから生成されたCのコードを見れる.

t/Mytest.tにコードを書いてみて実行結果を確認できる.

use Test::More tests => 9;                                                 
BEGIN { use_ok('Mytest') };

Mytest::hello();

Example2

引数をとって値を返す関数をXSで定義してみる.さっき作ったMytest.xsに以下を追加.

int
is_even(input)
    int input
    CODE:
        RETVAL = (input % 2 == 0);
    OUTPUT:
        RETVAL

inputで整数値を受け取り,RETVALに結果を代入する.RETVALはXSで容易される返り値のための変数.

OUTPUT: のところに変数を書くと返り値として使われる.

Example3

引数に渡ってきた変数自体を変更することで値を返す破壊的な関数を定義してみる.Mytest.xsに以下を追加.

void
round(arg)
    double arg
    CODE:
        if (arg > 0.0) {
            arg = floor(arg + 0.5);
        }
        else if (arg < 0.0) {
            arg = ceil(arg - 0.5);
        }
        else {
            arg = 0.0;
        }
    OUTPUT:
        arg

このように仮引数argに代入してそのままOUTPUT:で返却すると,argで渡ってきた変数に変更が加えられる.

my $i;                                                                     

$i = -1.5;                                                                 
Mytest::round($i);                                                         
is $i, -2.0; # ok

ということ.

ちなみに,XS => C変換はXSUBPPというのがやってるらしい.

TYPEMAPについて

TYPEMAPはCの型とPerlの値を自動的にマッピングする仕組み.

typemapファイルに型の対応や変換する方法が書いてある.(macportsのPerl5.8.8だと /opt/local/lib/perl5/5.8.8/ExtUtils/typemap にある)

Example4

Cライブラリのヘッダの情報にモジュールテンプレートを作る.ヘッダを準備してから.

$ h2xs -O -n Mytest2 ./Mytest2/mylib/mylib.h

のようにヘッダを引数に指定して実行.

くわえて,Cのライブラリも一緒にビルドするためのMakefile.PLの書き方の解説など.詳細は割愛.

XSのなかでデフォルトで用意されているもの以外のTYPEMAPが必要な場合は,モジュールディレクトリ直下のtypemapファイルに書く.

const char * T_PV
.xsファイルの構成
MODULE = Mytest2                PACKAGE = Mytest2

xsファイルのこの行より上は普通のCのコードが書ける.この行より下はXSのコードになる.XSのコードはxsubppコマンドでCのコードに変換される.

通常のXSでは,実際に呼び出されるCの関数はこの行より上の部分か,外部のライブラリで定義される.XSではPerlとそのCの関数のGlueだけが定義される.XSにちょくせつ関数の機能を書くのは例外的だ.

XSUBもうちょい詳細

XSはかしこいので,

double
foo(a, b, c)
    int         a
    long        b
    const char *c
    CODE:
        RETVAL = foo(a,b,c);
    OUTPUT:
        RETVAL

本来このように書かないといけないところが,

double
foo(a, b, c)
    int         a
    long        b
    const char *c

のように省略して書ける.

XSUBの引数についてもうちょっと
int
foo(a, b)
    char    &a
    char *   b

XSで&aとかくと,Cの関数にaではなくaのアドレスがわたるようになる.ポインタをわたすときは*とbの間にスペースが必要なので注意.

引数スタック

生成されたCのコードからはST(n)マクロで引数を取得できる.

XSのコードで引数をOUTPUTでそのままかえすと引数自体が変更される.例えば以下のコードは,

void                                                                       
round(arg)                                                                 
    double arg                                                             
    CODE:
        # do something                                                          
    OUTPUT:                                                                
        arg 

次のようなCのコードに変換される.ST(0)に記録されていたargを処理したあと,ST(0)戻している.

    double  arg = (double)SvNV(ST(0));
    # do something
    sv_setnv(ST(0), (double)arg);

Example 5

Arrayを返す関数の例.次のように書くことで,関数の返り値としてArrayを返せる.

void
statfs(path)
    char * path
    INIT:
        int i;
        struct statfs buf;
    PPCODE:
        i = statfs(path, &buf);
        if (i == 0) {
            XPUSHs(sv_2mortal(newSVnv(buf.f_bavail)));
            XPUSHs(sv_2mortal(newSVnv(buf.f_bfree)));
            XPUSHs(sv_2mortal(newSVnv(buf.f_blocks)));
            XPUSHs(sv_2mortal(newSVnv(buf.f_bsize)));
            XPUSHs(sv_2mortal(newSVnv(buf.f_ffree)));
            XPUSHs(sv_2mortal(newSVnv(buf.f_files)));
            XPUSHs(sv_2mortal(newSVnv(buf.f_type)));
        }
        else {
            XPUSHs(sv_2mortal(newSVnv(errno)));
        }

MacOSXだとstatfs構造体がどのヘッダファイルに定義されてるかわからなくてコンパイルできなかった.

この例で新しくでてきたこと
  • INITディレクティブは引数のデコード処理の直後に実行されるので変数の宣言に丁度良い
  • PPCODEディレクティブは返り値の処理を自分で行うときに使う.
  • 返り値をArrayで返すために値をスタックにつむにはXPUSHマクロを使う.
  • 返り値スタックのSVはmortalになってる.mortalってなに?
  • XPUSHマクロは呼び出されるたびにスタックを拡張するので無駄がある.EXTENDマクロで予めスタックを拡張してからPUSHsマクロを使うと効率が良い

Example6

引数にArrayのリファレンスをとって,HashのArrayのリファレンスを返す関数の例.Perlのデータ構造を操作するちょっと複雑な例.(コードは長いので割愛)

この例で新しくでてきたこと
  • typemapを使わずにSV *を受けとってSV *を返してる.返してるのはArrayリファレンス一つなのでPPCODEを使う必要がない.
  • 引数で受け取った値のValidationをINITでやっている.
	    if ((!SvROK(paths)) # リファレンスか?
		|| (SvTYPE(SvRV(paths)) != SVt_PVAV) # Arrayのリファレンスか?
		|| ((numpaths = av_len((AV *)SvRV(paths))) < 0)) # 値がはいってるか?
	    {
		XSRETURN_UNDEF; # undefを返すマクロ
	    }
  • Arrayは内部ではAV *で表現される.PerlのArray操作関数と似たマクロが用意されてる.(av_len, av_fetch, av_push など).スタックと同様に事前にArrayを拡張しておくと効率がよくなる.
av_extend(results, numpaths);
  • Hashは内部ではHV *で表現される.ArrayといっしょでPerlと似た操作用マクロが用意されている.
  • リファレンスはnewRV関数で作る.引数のSV *はAV *やHV *をキャストして渡せる.逆にSvRVでリファレンスからSV *を得た時は自分で適切な型にキャストする必要がある.

Example7 (Coming Soon)

Example8 (Coming Soon)

Example9

ひらいているファイルをXSに渡す方法.

# PerlIOがちゃんと理解できていないのでいろいろあやしい.

stdio.hで定義されてるfputsをPerlで使うには,以下のように書く.

	#define PERLIO_NOT_STDIO 0
	#include "EXTERN.h"
	#include "perl.h"
	#include "XSUB.h"

	#include <stdio.h>

	int
	fputs(s, stream)
		char *          s
		FILE *	        stream

typemapがうまくやってくれるおかげでPerlIOレイヤをうまくとおるようになってる?

じぶんでPerlIOつかう場合は以下のPerlIO_putsのようにPerlIO用の関数を使う.

	typedef PerlIO *	OutputStream;
	int
	perlioputs(s, stream)
		char *          s
		OutputStream	stream
	CODE:
		RETVAL = PerlIO_puts(stream, s);
	OUTPUT:
		RETVAL

PerlIOからFILE構造体をとりだして使うこともできる.

	int
	perliofputs(s, stream)
		char *          s
		OutputStream	stream
	PREINIT:
		FILE *fp = PerlIO_findFILE(stream);
	CODE:
		if (fp != (FILE*) 0) {
			RETVAL = fputs(s, fp);
		} else {
			RETVAL = -1;
		}
	OUTPUT:
		RETVAL

まとめ

  • XSまわりの仕組みがどうなっているのか,だいたいわかった
  • XSはいろいろと空気を読んでやってくれるので,Cのコードはそんなにかかなくて良い.typemapすごい
  • Perl内部系の話(SV, AV, HV とかのはなし)は最低限だけだったので勉強したい