2009/12/31
最近、GoのGTKバインディングを作ってるのだけど、先日ようやく簡易twitterクライアントを作れるまでに至ったのだが、そのtwitterクライアントの「タイムライン更新」ボタンを押した時に画面をブロックさせずに画面を更新する方法を考えてた。
Perl-GTKならばCoroを使ってこうするだろうか。
use strict;
use warnings;
use Gtk2 '-init', '-threads-init';
use AnyEvent;
use Coro;
use Coro::Timer;
my $window = Gtk2::Window->new('toplevel');
$window->signal_connect(
destroy => sub {
exit;
}
);
my $vbox = Gtk2::VBox->new;
my $button = Gtk2::Button->new('click me');
$button->signal_connect(
clicked => sub {
$button->set_sensitive(0);
async {
Coro::Timer::sleep 3;
Gtk2::Gdk::Threads->enter;
$button->set_sensitive(1);
Gtk2::Gdk::Threads->leave;
};
}
);
$vbox->add($button);
$window->add($vbox);
$window->show_all;
Gtk2->main;
ここで
Coro::Timer::sleep
しているのは、生の
sleep
を使うとCoroがスケジュール割り当てとして介入出来なくなるのを回避する為。非同期で動作はするものの、スレッド動作になるので「
Gtk2::Threads->enter
」と「
Gtk2::Threads->leave
」でUIに関連する処理をブロックしなければなりません。つまり例えばボタン押下時の処理が5段階あり、その都度UIの何かしらを変更しなければならない場合、「
Gtk2::Threads->enter
」と「
Gtk2::Threads->leave
」でそれぞれブロックしなければならない事になります。
平たく言うと、スレッドを使う予定の無かったUI処理を、スレッド対応にするには多少のコード修正が必要になるという事です。
今回、goで作った簡易twitterクライアントの「タイムライン更新」ボタンを非同期対応するのに費やしたコードはたった2行。元のコード
button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
button.SetSensitive(false);
if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
}
button.SetSensitive(true);
}, nil);
となっていたコードに「go」で実行されるブロックを追加しただけ。
button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
button.SetSensitive(false);
go func() {
if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
}
button.SetSensitive(true);
}();
}, nil);
何が言いたいのかというと、groutineはgo組み込みであって、Coroがsleepやhttp_getを置き換えようとしている(CORE::GLOBAL上書きもあるけど)物とは違い、言語として非同期をサポートしているという事。もちろんgoにおいてもボタンクリック時にsleepを呼ぶとUIはブロックされるのだけど、実際sleepなんて使わないし、そもそもgoroutineはスレッドじゃない。
そう、goroutineはスレッドじゃない。
A Tutorial for the Go Programming Language
Go has its own model of process/threads/light-weight processes/coroutines, so to avoid notational confusion we call concurrently executing computations in Go goroutines.
http://golang.org/doc/go_tutorial.html
実際にはgoが内部でscheduleを行って実行する処理連鎖で実現している。なのでgtkを呼び出す場合においてもgdk_threads_enter/gdk_threads_leaveを呼び出す必要もない。なぜなら並行とは言えど同時実行ではないのでコンテキストスイッチも意識しなくて良い。
故に上記の様な 「
go func(){}()
」ブロックだけで非同期処理が書けるという訳。例えばcursesでCUIアプリを書いても画面が崩れる事は無いだろう(未確認)。
UIの開発に有利な言語にはある程度、共通する物があると思う。
- 匿名関数をコールバックとして扱える
- サードパーティでも良いので非同期処理の仕組みがある
- GCが効く
この内、2個か3個あてはまるとUIを扱う上で有利になると思う。Javascriptはこの点(組み込みで非同期が扱える:というかそういうものしか扱えない?)素晴らしいし、jQueryを使えばAjax/UIとして作り手側はかなりの恩恵が得られる。Perlは「
sub {}
」で匿名関数が使えるがので優秀だが非同期となるとスレッドに頼らざるを得ないのが難点。rubyも同様。pythonは匿名関数が使えないのが少し悲しい(でもpygtkは好きだ)。結局の所、メインループと非同期処理をどう扱うかがネックになっている。例えば各言語でCOMを扱う場合、STA(Single Thread Apartment)しか対応していないCOMは、言語側でメインループをグルグル回すしかない。まぁ言語として用途が違うんだ(JavascriptはそもそもUI記述言語だろ...とか)から、比べちゃならんだろ...とも言えるし。
goにおいては上記3点あてはまるが、デストラクタが無いのが一番の難点。
これからのUIはどんどん非同期な物になって行くんだろうな。
10年程前のUIと言えば、非同期なUIはあまり見られなかった。VB6においても定期的に「
DoEvents
」を実行してプログレスバーを更新するという、なんちゃって非同期処理が横行していた頃だ。最近はVB.NETでコンポーネント自身がスレッドサポート(実際にはデリゲート)しているので簡単に非同期処理が書ける様になったが、これら全てにおいてもやはり
非同期対応するつもりの無かったUI処理を、非同期対応にするには多少のコード修正が必要になる
には違い無いし、その点でgoには期待している。
2009/12/22
GoのGTKバインディングを現在作業中なのですが、ようやく簡単なTwitterクライアントくらいならば作れる程度に出来上がってきたのでご報告。
mattn's go-gtk at master - GitHub
Go binding for GTK
http://github.com/mattn/go-gtk
イベントハンドラがJavaScriptっぽく書けるので、結構見通し良いコードになります。
package main
import "gtk"
import "gdk-pixbuf"
import "unsafe"
import "http"
import "json"
import "io"
import "os"
import "strconv"
import "strings"
import "reflect"
func url2pixbuf(url string) *gdkpixbuf.GdkPixbuf {
if r, _, err := http.Get(url); err == nil {
n, _ := strconv.Atoi64(r.GetHeader("Content-Length"));
t := r.GetHeader("Content-Type");
b := make([]byte, n);
io.ReadFull(r.Body, b);
var loader *gdkpixbuf.GdkPixbufLoader;
if strings.Index(t, "jpeg") >= 0 {
loader, _ = gdkpixbuf.PixbufLoaderWithMimeType("image/jpeg");
} else {
loader, _ = gdkpixbuf.PixbufLoaderWithMimeType("image/png");
}
loader.SetSize(24, 24);
loader.Write(b);
loader.Close();
return loader.GetPixbuf();
}
return nil;
}
func main() {
gtk.Init(&os.Args);
window := gtk.Window(gtk.GTK_WINDOW_TOPLEVEL);
window.SetTitle("Twitter!");
window.Connect("destroy", func(w *gtk.GtkWidget, args []unsafe.Pointer) {
gtk.MainQuit();
}, nil);
vbox := gtk.VBox(false, 1);
scrolledwin := gtk.ScrolledWindow(nil, nil);
textview := gtk.TextView();
textview.SetEditable(false);
textview.SetCursorVisible(false);
scrolledwin.Add(textview);
vbox.Add(scrolledwin);
buffer := textview.GetBuffer();
tag := buffer.CreateTag("blue", map[string] string {
"foreground": "#0000FF", "weight": "700" });
button := gtk.ButtonWithLabel("Update Timeline");
button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
n, _ := strconv.Atoi64(r.GetHeader("Content-Length"));
b := make([]byte, n);
io.ReadFull(r.Body, b);
j, _ := json.Decode(string(b));
arr := reflect.NewValue(j).(*reflect.SliceValue);
for i := 0; i < arr.Len(); i++ {
data := arr.Elem(i).(*reflect.InterfaceValue).Elem().(*reflect.MapValue);
icon := data.Elem(reflect.NewValue("user")).(*reflect.InterfaceValue).Elem().(*reflect.MapValue).Elem(reflect.NewValue("profile_image_url")).(*reflect.InterfaceValue).Elem().(*reflect.StringValue).Get();
var iter gtk.GtkTextIter;
buffer.GetStartIter(&iter);
buffer.InsertPixbuf(&iter, url2pixbuf(icon));
name := data.Elem(reflect.NewValue("user")).(*reflect.InterfaceValue).Elem().(*reflect.MapValue).Elem(reflect.NewValue("screen_name")).(*reflect.InterfaceValue).Elem().(*reflect.StringValue).Get();
text := data.Elem(reflect.NewValue("text")).(*reflect.InterfaceValue).Elem().(*reflect.StringValue).Get();
buffer.Insert(&iter, " ");
buffer.InsertWithTag(&iter, name, tag);
buffer.Insert(&iter, ":" + text + "\n");
}
}
}, nil);
vbox.PackEnd(button, false, false, 0);
window.Add(vbox);
window.SetSizeRequest(300, 400);
window.ShowAll();
gtk.Main();
}
実行すると以下の様な画面になります。
まだ、完成じゃないです。がんばります!
2009/12/13
やっぱり少し私とは認識が違う気がする。
おごちゃんの雑文 » Blog Archive » 「はまちちゃん」をspamと断じる思考停止
この穴を「カンチョウ」で済ませているところが、はまちちゃんのサジ加減なのだ。
「スキだらけの後ろ」があることは、彼にはわかっている。わかっていてそこを突かないのは一見親切で礼儀正しい態度に見える。でも、そういった非常に突きやすい、単純な穴があることは、わかる人にはわかってしまっていて、気がついてないのは運営者だけという状態になっているのが、「Amebaなう」だったわけだ。はまちちゃんのやったのは「カンチョウ」かも知れないけれど、やれることがわかっている悪意の者であれば、
後ろから刺す
ことだって出来るわけだ。「悪の組織」がはまちちゃんの彼女をどーこーしたら… とかだってないとは言えない。Amebaには首相官邸のブログだってあるわけで、ポストにちょっとした仕掛けを入れるだけで、
公式チャネルからデマを流す
ことだって可能になる。仕手筋とかなら5分デマが流れりゃ十分満足だろう。Amebaには日本の最終兵器とも言われるデスブログだってあるわけでw
http://www.nurs.or.jp/~ogochan/essay/archives/2156
カンチョーで済ましてあげてるだけ、やさしい?
違うでしょ、肩をトントンと叩いて「ちょっと、スキだらけですよ。世の中ひどい人ばかりなので、気をつけないとカンチョーされますよ。」と言ってあげるのが
やさしいんじゃないの?
おまけに傍観者とし面白い話かもしれないけどカンチョーでどんだけ人が動くか想像してもいいんじゃないかな?復旧にはSE一人じゃなかったかもしれないし、対策検討会の様な物もあったかもしれない。SE一人終日動かせば1日で原価数万飛びますよ。それ十分被害でしょ。
カンチョーがどんな印象を与えたのかは受けた者でないと分かんないよ。
はまちちゃんと脆弱性報告のあり方 - 世界線航跡蔵
これは「隙があった」んじゃないだろう。「開腹したまま内臓が露出している」んだ。
ところが、どうも現実の医師とは違ってこの世界の、特にAmebaみたいな大きな会社の開発者はその辺の意識が甘い。「手術したけど、まー、内臓が見えててもすぐに死ぬ訳じゃないし、適当に皮被せておけばいいよね」とか思ってる。下手したら開腹後に塞ぐべきという常識的なことを考えすらしない。で、そこら中を内臓が見えたままの患者が歩いている。
http://yugui.jp/articles/851
それも違う気がする。
この件で傍観している人たちは技術者だ。そしてその脆弱性は技術者でしか分からない。これはいわば院内感染ですよ。脆弱な病棟に気付かず通院/入院している一般の人達と、そこに「院内感染だー!」と叫ぶ人でしょ。彼らにはそれが冗談なのか何なのか分かってない。確かに今回のはまちちゃんの行為を「カンチョウ」と命名したのは私だが、このカンチョウで不安になった人がいたかもしれないし、Amebaの印象を悪くした人もいるかもしれない。
カンチョーで痔になってたら軟膏塗ってくれるの?はたまたカンチョーで筋うんこ付いてたら、パンツ洗ってくれるの?
カンチョーだってやり方次第では十分犯罪ですよ。
ただし言いっておきたいのは、
私はこういったサービス全て注意勧告してあげるべきとは思ってない。注意して効かなかったサービスは勝手に潰れるだろうし、言い方変えれば潰れるべきだと思う。
私は脆弱性を見つけて注意勧告する事はとても大切だと思うが、それを手法交えて公に広める事については賛同出来ない。セキュリティを扱う人たちにとってはそれがステータスかもしれないけし、知名度を上げるチャンスなのかもしれないけど、私はそんなチャンスは欲しくないし、それで知名度を上げられたとしても嬉しくない。
私ならばもっと技術者らしい知名度の上げ方をする。ただそれだけの話ですよ。
ちなみに全然関係ない話ですが、私が中学生だった時の友達は、「起立!礼!着席!」のタイミングで前に座っていた同級生にカンチョウを食らわそうとしてたが、思っていた以上にズボンが固かった為、中指第二関節を骨折した事がありますよ!カンチョウには十分気をつけましょう。
2009/12/10
Tatsumakiって面白そうとは思ってたけど、触った事なかったので、勉強してみた。
なるほど面白そう。と思ったのとPSGIでも動かせるんだーと感動しました。今日は勉強がてら、以前紹介した事もあるDiggのDUI Streamと混ぜて遊んでみた。
Digg the Blog » Blog Archive » DUI.Stream and MXHR
We call this technique MXHR (short for Multipart XMLHttpRequests), and we wrote an addition to our Digg User Interface library called DUI.Stream to implement it. Specifically, DUI.Stream opens and reads multipart HTTP responses piece-by-piece through an XHR, passing each chunk to a JavaScript handler as it loads.
http://blog.digg.com/?p=621
miyagawa's Tatsumaki at master - GitHub
Plack-based nonblocking Web framework for IO-bound delayed response, server push (streaming) and long-poll comet
http://github.com/miyagawa/tatsumaki
ルートとなるHTMLからjavascriptでストリームへのエンドポイントへリクエストさせ、そこからmultipart/mixedで送出されるgif画像(静止画)を断続的に送出し、DUI Streamでパースしながら1個のIMGタグを高速に切り替えます。
名前をTatsumakiSenpukyakuと名付けました。
mattn's TatsumakiSenpukyaku at master - GitHub
竜巻旋風脚
http://github.com/mattn/TatsumakiSenpukyaku
実行するとポート9999でサーバが起動し、「せーのー」と勢いを付けたあとに、高速な「竜巻旋風脚」が始まります。
javascriptからタイマでリクエストしているのではなく、サーバから断続的に、切断せずに送出されています。上記DUIのリンク先にある
画像を使ったデモでも分かる通り、ストリーミングによりパラパラと表示されていた画像コンテンツがまるで動画の様に見せる事が出来ます。
ネタっぽい作りですが、へーこんな事出来るんだー...てな感じで動かしてみて下さい。
2009/12/04
GtkTreeModelとか面倒くさい部分に差し掛かってます。でもこれないとコンボボックスもツリーもリストも作れないんすよね。
いまんところこんな感じ。
イベントクロージャが内包出来てJavaScriptを書いているかの様にGTKプログラミングが出来ています。
button := gtk.ButtonWithLabel("Button with label");
button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
print("button clicked: ", button.GetLabel(), "\n");
dialog := gtk.MessageDialog(
button.GetTopLevelAsWindow(),
gtk.GTK_DIALOG_MODAL,
gtk.GTK_MESSAGE_INFO,
gtk.GTK_BUTTONS_OK,
entry.GetText()
);
dialog.Response(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
println("Dialog OK!")
}, nil);
dialog.Run();
dialog.Destroy();
}, nil);
buttons.Add(button);
完成はまだまだ先です。頑張ります。
2009/12/02
os0xさん。お疲れ様でした。次の活躍も期待しています。
株式会社ALBERTを退社します - 0xFF
ファイル整理とかしていて2005年当時に書いたプログラムとかが出てきて、あまりのアレさに感慨深くなりました…
http://d.hatena.ne.jp/os0x/20091201/1259626454
私も以前フォルダの中を色々探索してたら、8年前のソースコードが出て来て懐かしくなりました。某IRCチャネルでは一度晒した事があるのですが、ネタとして面白そうだったのでブログに転載します。
8年前の私は、何故かtelnetに興味があり、Windows標準のtelnetは何故色が出ないんだ!なぜあんなショボいんだ!と思って、自前でtelnetを作っていました。
最初はvectorに登録する予定でしたが、面倒くさくなってやめてしまいました。
xterm互換です。NTLM認証対応です。Win32 APIでエスケープシーケンス表現しています。utf-8には対応していません><。とても長いです。変に凝ってます。そしてAPIゴリゴリです。懐かしいと感じると同時に、センスの無さに笑いました。
sTelNet.c
sshが標準な今、telnetのソースコードなんて無意味に近いですが、興味のある方だけ、どうぞw
2009/11/28
やっぱり新しい言語が出来たらGUIだよね!
って事でGTKバインディング作ります!
mattn's go-gtk at master - GitHub
gtk extension for go
http://github.com/mattn/go-gtk
Goでは継承が使えないので、例えばGtkBoxを継承したGtkVBox/GtkHBox、GtkWidgetを継承したGtkWindow/GtkButton/GtkLabel...が上手く表現出来ません。
出来ないというか、GtkButtonにGtkWidgetと同じメソッドを生やそうと思うと、同じコードを書かなければなりません。
色々と模索した結果、gtk.Window()やgtk.Button()で返すのは常にGtkWidgetとし、GtkWindowやGtkButton独自のメソッドを呼び出したい場合には
(>k.GtkWindow{window.Widget}).SetTitle("GTK Go!");
こういう風に型変換して貰う事にしました。まぁコンポジションぽい物?
C言語バージョンにおいても同じ処理をする時には
gtk_window_set_title(GTK_WINDOW(w), "GTK Go!");
とする訳だし、似てると言えば似てる訳だし...
要は、Widget毎にGtkWidgetからGtkXXXへの変換関数なんか作ってらんねぇよ。バカ!
という訳です。
現状、コールバック(gtk_signal_connect)も動作しています。少しハマった理由としては、goroutineを勝手にスレッドだと思っていたので、スレッド間通信を想定してpipeのコードを書いていましたが、よく考えたらscheduled coroutineだし、自分のwrite pipeを自分でreadしてどうするよ...って気付いたのです。
結局変数ポーリングに変えて現状上手く動いています。
GoでGTKのプログラミングをすると、以下の様なコードになります。
package main
import (
"os";
"gtk";
"unsafe";
)
func main() {
gtk.Init(&os.Args);
window := gtk.Window(gtk.GTK_WINDOW_TOPLEVEL);
(>k.GtkWindow{window.Widget}).SetTitle("GTK Go!");
vbox := gtk.VBox(0, 1);
label := gtk.Label("ハローワールド");
(>k.GtkBox{vbox.Widget}).PackStart(label, 0, 1, 0);
entry := gtk.Entry();
(>k.GtkEntry{entry.Widget}).SetText("入力エリア!");
vbox.Add(entry);
button := gtk.ButtonWithLabel("こんにちわ!こんにちわ!");
button.Connect("clicked", func(widget *gtk.GtkWidget, data unsafe.Pointer){
println("button clicked");
println((>k.GtkButton{button.Widget}).GetLabel());
}, nil);
vbox.Add(button);
window.Add(vbox);
window.ShowAll();
gtk.Main();
}
そして実行画面。
まだ、GtkWindow/GtkButton/GtkLabel/GtkVBox/GtkHBox/GtkEntryのそれぞれ一部のメソッドしか動いていませんが、宜しければ遊んでみて下さい。
そしてもし「おおし!おいらも作るお!」って方が居られたら、ぜひgithubでforkして下さい。
2009/11/25
そろそろgoでライブラリを作る頃かなーと思って、migemo(cmigemo)を使う物を書いてみた。
mattn's go-migemo at master - GitHub
migemo extension for go
コードの中では
KoRoNさんのcmigemoを使った。コードは少ないけど実は少しハマって、今日はそれを書き記したい。
migemoでは、正規表現文字列やパターン文字列をunsigned char*で引数として扱っているんですが、cgoを使ったC言語ライブラリの取り込みを行う場合、char*と型が合わなくてコンパイルエラーが発生する。しかしC言語の様に
*C.uchar(p)
等と書けない(これだとucharの参照になってしまう)Go君は、致し方なくchar*を引数に持つwrapper関数を用意するしかないんだけど、実はcgoに食わせるgoファイルでは
package migemo
import "C";
と言った様に、Cのコードが書ける。ここにGoで扱い易い型のwrapperを書けば良い。今回の例だとunsigned char*の引数を持つ関数をchar*で渡せる(C.CString)関数を用意している事になる。
これにより、いちいち別ファイルにwrapper関数用意したりMakefileにwrapperをビルドする為のターゲットを書かなくても良い。
これは便利だ。
話戻してmigemo拡張ですが、現状Open/Close/Load/Queryの4メソッドを持っています。
Queryで渡したパターンによるマッチし得る複数の正規表現が得られます。
package main
import (
"fmt";
"regexp";
"strings";
"migemo";
)
func main() {
var pattern = "goGengo";
var match = "go言語";
m := migemo.Open("../dict/utf-8.d/migemo-dict");
s := migemo.Query(m, pattern);
if (regexp.MustCompile(s).Match(strings.Bytes(match))) {
fmt.Printf("%s は %s にマッチします!\n", pattern, match);
} else {
fmt.Printf("%s は %s にマッチしません!\n", pattern, match);
}
}
実行すると
goGengo は go言語 にマッチします!
と出力されます。
地味に使えるかも。
2009/11/24
最初Goがリリースされた時には、自分がPortingするんだーとか意気込んでましたが、google codeにホスティングされている方のほうが良い物作ってくれそうだったので、ずっと見守ってました。
hectorchu-go-windows - Project Hosting on Google Code
go for Windows
http://code.google.com/r/hectorchu-go-windows/
Windows PEのリンカも入って、適当な物ならばコンパイル&リンク出来る様になってます。
ただ一般的なWindows Portingの問題として、POSIXなAPIであるpipeやfork、ソケットディスクリプタ等といったWindowsで完全に模倣出来ない部分が残っています。例えばhttpクライアントを扱うhttpパッケージは、netパッケージに依存し、かつ内部ではos.Fdを使う為にそのままのコードではWindowsで実行出来ません。さらにgodoc等ファイルおよび外部プログラムを扱う様な物は内部でos.Pipe()、os.Fork()が呼ばれており、こちらもWindowsなAPIで置き換えなければなりません。
現在の所、src/pkg/osとは別にsrc/pkg/windows/osというフォルダで、Win32 APIを使ってosパッケージを模倣され様としています。移植という点ではこれからになりますね。Issue Trackerが開かれていないのでバグ報告出来ないですが、もし気になる事があればメーリングリストに投げようかなと思っています。
ちなみに、スレッドまわりの実装は既に入っているのでgoroutineは動きます。
ぜひ遊んでみましょう!
2009/11/23
昨日書いたGoのIRCボットに手を入れ、mecabで要素解析する様にした。色々弄ってたら、某所で有名な「悪く言うなbot」になってた。
goによるmecab wrapperの実装がある事をtokuhiromさんに教えてもらったので、さっそく使わせて頂いた。
package main
import (
"fmt";
"http";
"bytes";
"json";
"strings";
"io";
"mecab";
)
func get_json(url string, data map[string]string) json.Json {
q := "";
for key, val := range data {
if (len(q) > 0) { q += "&" }
q += fmt.Sprintf("%s=%s", key, http.URLEscape(val));
}
bb := &bytes.Buffer{};
bb.Write(strings.Bytes(q));
r, err := http.Post(url + "?" + q, "application/x-www-form-urlencoded", nil);
if err == nil {
b, _ := io.ReadAll(r.Body);
r.Body.Close();
j, _, _ := json.StringToJson(string(b));
return j
}
return nil
}
func get_sid(nick string) string {
j := get_json("http://webchat.freenode.net/e/n", map[string]string{"nick":nick});
return j.Elem(1).String();
}
func login(nick *string) string {
sid := get_sid(*nick);
for {
j := get_json("http://webchat.freenode.net/e/s", map[string]string{"s":sid});
if j == nil {
break;
}
for i := 0; i < j.Len(); i++ {
data := j.Elem(i);
println(data.String());
if (data.Elem(0).String() != "c") {
continue;
}
if (data.Elem(1).String() == "433") {
*nick += "_";
sid = get_sid(*nick);
}
if (data.Elem(1).String() == "376") {
return sid;
}
}
}
return "";
}
func join(sid string, room string) bool {
j := get_json("http://webchat.freenode.net/e/p", map[string]string{"s":sid, "c":"JOIN " + room});
if j == nil {
return false;
}
return true;
}
func say(sid string, room string, msg string) bool {
j := get_json("http://webchat.freenode.net/e/p", map[string]string{"s":sid, "c":"NOTICE " + room + " :" + msg});
if j == nil {
return false;
}
return true;
}
func listen(sid string, nick string, room string) {
for {
j := get_json("http://webchat.freenode.net/e/s", map[string]string{"s":sid});
if j == nil {
continue;
}
for i := 0; i < j.Len(); i++ {
data := j.Elem(i);
println(data.String());
if (data.Elem(0).String() != "c") {
continue;
}
if (data.Elem(1).String() != "PRIVMSG") {
continue;
}
line := data.Elem(3);
if (line.Elem(0).String() == nick) {
continue;
}
s := line.Elem(1).String();
m := mecab.New2("");
r := mecab.SparseToStr(m, s);
ss := strings.Split(r, "\n", 999);
for l := 0; l < len(ss); l++ {
st := strings.Split(ss[l], "\t", 2);
if (len(st) < 2) {
continue;
}
if (strings.Index(st[1], "固有名詞") != -1 ||
strings.Index(st[1], "人名") != -1 ||
strings.Index(st[1], "組織") != -1) {
say(sid, room, st[0] + "は良い奴だ。悪く言うな。俺が許さん。");
}
}
}
}
}
func main() {
nick := "go-japan";
room := "#mattn";
sid := login(&nick);
if (len(sid) > 0) {
if (join(sid, room)) {
listen(sid, nick, room);
}
}
}
channelに固有名詞や人名等が現れると「XXXは良い奴だ。悪く言うな。俺が許さん。」と発言します。
mecabライブラリについては、やっぱりfeatureとかsurfaceを扱いたいのでこれからに期待したいと思います。まぁ、現状でも上記の様にstrings.Split使えば出来なくないんですけどね。
なんとなくだけど、だんだん「C++書きの為のGo言語」と言われてる理由が分かって来た気がする。
とは言っても、freenodeのWebインタフェース使ってJSONやり取りして、発言するだけの物。
以前C++で作った全裸botの簡易版といった所か。
動かすと、freenodeの"golang-daisuki"(仮)にJOINして「郷です!ジャパーーーン!」と発言した後に死亡します。
嫌がらせには持って来いですね。
package main
import (
"fmt";
"http";
"bytes";
"json";
"strings";
"io";
)
func get_json(url string, data map[string]string) json.Json {
q := "";
for key, val := range data {
if (len(q) > 0) { q += "&" }
q += fmt.Sprintf("%s=%s", key, http.URLEscape(val));
}
bb := &bytes.Buffer{};
bb.Write(strings.Bytes(q));
r, err := http.Post(url + "?" + q, "application/x-www-form-urlencoded", nil);
if err == nil {
b, _ := io.ReadAll(r.Body);
r.Body.Close();
j, _, _ := json.StringToJson(string(b));
return j
}
return nil
}
func get_sid(nick string) string {
j := get_json("http://webchat.freenode.net/e/n", map[string]string{"nick":nick});
return j.Elem(1).String();
}
func login(nick string) (string, string) {
sid := get_sid(nick);
for {
j := get_json("http://webchat.freenode.net/e/s", map[string]string{"s":sid});
if j == nil {
break;
}
for i := 0; i < j.Len(); i++ {
data := j.Elem(i);
println(data.String());
if (data.Elem(0).String() != "c") {
continue;
}
if (data.Elem(1).String() == "433") {
nick += "_";
sid = get_sid(nick);
}
if (data.Elem(1).String() == "376") {
return sid, nick;
}
}
}
return "", "";
}
func join(sid string, room string) bool {
j := get_json("http://webchat.freenode.net/e/p", map[string]string{"s":sid, "c":"JOIN " + room});
if j == nil {
return false;
}
return true;
}
func say(sid string, room string, msg string) bool {
j := get_json("http://webchat.freenode.net/e/p", map[string]string{"s":sid, "c":"NOTICE " + room + " :" + msg});
if j == nil {
return false;
}
return true;
}
func main() {
nick := "go-japan";
sid, _ := login(nick);
if (len(sid) > 0) {
if (join(sid, "#golang-daisuki")) {
say(sid, "#golang-daisuki", "郷です!ジャパーーーン!");
}
}
}
作ってみた感想としては、httpまわりにもう少しPOSTに便利なメソッドが欲しいのと、Postメソッドのデフォルトがcheckedになっている(Postメソッドでは変えれない)ので古くさいサーバで動かないんじゃないかと思ったり。
ただC++版に比べて幾らか短く書けたし、今回のコードでは例外等処理してないけど、例外処理を書きたくなった時にも簡単に実装が出来るのも分かった。
ところでpublicメソッドはメソッド名を先頭大文字にするってルールはいいけど、少しだけオモチャっぽく感じてしまうのは私だけだろうか。
2009/11/19
ふと思いつきで書いたコードを実行してしまって死にかけた。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <memory.h>
int main(void) {
char *p = NULL;
struct sigaction sa_sigint;
memset(&sa_sigint, 0, sizeof(sa_sigint));
sa_sigint.sa_handler = (void (*)(int))fork;
sa_sigint.sa_flags = SA_RESTART;
if (sigaction(SIGSEGV, &sa_sigint, NULL) < 0) {
perror("sigaction");
exit(1);
}
sleep(1);
puts("僕は死にましぇん!");
return *p = 0;
}
Ctrl-CとかCtrl-Zとか押しまくってようやく止めた。
良い子は真似しちゃ駄目よ。
今日、TypePadが提供するMicroblogを作ってみた。
http://mattn.typepad.com/
とてもグラフィカルで、reblogもついててfavoriteもついててカッコイイのだが、これを見て最近twitterのBeta機能として盛り込まれているReblogボタンの存在に疑問を感じた。
twitterではこれまでRT(Retweet)する際にはユーザがRTという接頭語を入れ、さらにその前にコメントを付与してポストするのだけれど、実はこのボタン、押すとそのまま自動でRetweetされる。まぁコメントが入れれない訳なんですが、私が疑問を感じたのはそこじゃない。
「何故ボタンにしたのか」
私が思うにtwitterが起こした革命の一つが
「テキストの可能性」へ着目した事だと思う。140字以内で、かつ太字に出来る訳でもアンカーを貼れる訳でもなく、かつ宛名をGUIで指定出来る訳でもない。単なるテキストだけで、宛先、タグ、評価、位置などを表しているのだ。昨今リッチコンテンツが流行する中、メインとなるコンテンツの入力は、twitterサービス開始当初と変わらずテキストエリアなのだ。
このテキストによる表現は、replyこそ当初から入っていたが他の物については誰が取り決めた訳でもなく、ユーザが勝手に始め、後追いでオフィシャルが機能追加していったのが実状だ。
そしてそれは今や明文化され、一種のルール(nanoformat/twitter-syntax)になっている。
microblogging-nanoformats · Microformats Wiki
http://microformats.org/wiki/twitter-nanoformats
Twitter Syntax · Microformats Wiki
http://microformats.org/wiki/twitter-syntax
なのになぜ、ボタンにしたのか...。テキストでの表現があってこそ、Greasemonkeyによる拡張や、他のサービス連携が発展したのだと私は思うし、twitter自身が例えば画像添付や自動処理なんてすべきじゃないとも思う。
サービスを面白くする要因は、サービスが提供する機能以上に、ユーザが作るコンテンツが重要なんだと信じているが、この自動化の流れはユーザが作る新たなルール(コンテンツ)を阻害する可能性もあると思う。
twitterのあるべき姿は140字のテキストエリアであって、ボタン一つでアクションを起こせる様な、流行に流される物じゃない。今後このRetweetボタンがどの様に機能していくか分からないが、
コンテンツを作るのはサービス提供側じゃなく、ユーザなんだという部分を忘れないでいて欲しいし、これからのtwitterがそうある事を信じたい。
2009/11/14
amachangのtwitterタイムラインの奴をhttp.Getでやってみた。
早速 Go 言語を試してみる! - IT戦記
http.Get は何故か動かなかったので net.Dial を使った。
http://d.hatena.ne.jp/amachang/20091111/1257928890
こうかな?わかりません><
package main
import (
"fmt";
"json";
"io";
"http";
)
func main() {
if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
b, _ := io.ReadAll(r.Body);
j, _, _ := json.StringToJson(string(b));
for i := 0; i < j.Len(); i++ {
data := j.Elem(i);
fmt.Printf("%s: %s\n",
data.Get("user").Get("screen_name"),
data.Get("text"));
}
}
}
今日、始めてGo触ってみたけど、重大な欠点を見つけた。
# vi twitter.go
コードかきかき...
# 8g twitter.go
うし!コンパイルだ!
twitter.go:7: imported and not used: net
おろっエラーだ
# vi twitter.go
直すぞ!直すぞ!
# !8
もっかいコンパイルだ!
bash: !8: event not found
><
言語仕様で、multiple-valueをちゃんとmultiple-valueで受けないといけないのが堅すぎる気がする。
pythonとかC++とか言われてるけど、私はluaなんじゃないかと思った。
2009/11/13
goを弄ってるといちいち8g(5g?)とか8l(5l?)とかでコンパイル、ビルドする手間が必要なんだけど、これってquickrun.vimの設定で便利になるんじゃないか?と思ったので設定した。
thincaさんバージョンのquickrun.vimを入れた後、vimrcで以下の様に記述する。
let g:quickrun_config = {
\ 'go': {
\ 'command': '8g',
\ 'exec': ['8g %s', '8l -o %s:p:r %s:p:r.8', '%s:p:r %a', 'rm -f %s:p:r']
\ }
\}
環境によっては8g/8lを書き換える必要があります。
あとは拡張子goのファイルで<leader>r(mapleaderを設定してなければ\r)で、コンパイル、リンク、実行までやってくれて、まるでスクリプト言語を書いている様な開発効率が得られる!
thinca++
2009/11/12
Gyazoアプリがgithubにあがってるのを知った。
gyazo's Profile - GitHub
Gyazo is Open Source!
http://github.com/gyazo
GyazoWinならびにGyazoのLinux版にもお世話になっているので結構嬉しい。今日見たら
ESCキーで中断出来る様になってた。嬉しい。
よし!私もGyazoアプリ作るぞ!と思って、考えたけど結局思いついたのがこれしか無かった。
コマンドプロンプトから実行すると、コマンドプロンプトをキャプチャしてgyazoするアプリ、GyazoCmd!
mattn's GyazoCmd at master - GitHub
gyazo interface to windows command prompt
http://github.com/mattn/GyazoCmd
実行するとこんな風になります!
だっ....誰得!汗
2009/11/08
たまにはプログラミングに全く関係ない事でも書いてみる。
先日、はてなブックマーク経由でとある対戦アクションゲーム大会の動画を見てて、最近ゲームなんて全くしていなかった私はある事に気が付いた。
最近のゲームは必殺技名を叫ばない。
なんでだろ...。昔のゲームと言えば「昇竜拳!」「竜巻旋風脚!」「バーンナックル!」「虎煌拳」等と叫んだものだ。
子供達はそれで必殺技を覚え、学校で話し、真似して遊んだもんだ。でも最近のゲームは必殺技を叫ばない。どうやって友達と学校で情報共有するんだろうか。攻略本やネットで調べるのか?それとも最近じゃ真似する時も叫ばないのか?
叫ぶからこそ必殺技っぽいと思っていた時代は古いのか?例えば学校で「カイザーウェーブ!」って叫びながら大きく胸を突き出し、腕を振り出す真似を叫ばずやったとしたら、はたしてそれが「カイザーウェーブ」と分かるだろうか。
叫ぶからこそ、ゲームを知らない人でも「あー、ゲームの必殺技かなんかだろうな...」って気付くだろうし。叫んだからこそ女子が見て「男子幼稚!」ってなったものが、はたして叫ばなかったらそれがゲームの必殺技と分かるだろうか。知らない人が見たらカイザーウェーブの動きなんて、公園に現れる変質者の挙動に見え兼ねない。きっと女子に「男子変態!」って言われるだろう。
あの、古き良き時代は何処に行ってしまったんだろう...。
全然関係ないけど、必殺技って技を繰り出す前、もしくは繰り出している最中に叫ぶ物だと思うんだけど、そういう点でいうと北斗の拳のケンシロウは少し卑怯だと放映当時の私は子供ながら感じていた。
ケンシロウは技を繰り出してから技名を叫ぶのだ。
アタタタタタタタ...お前はもう死んでいる!
これだと、技の最中に失敗してたとしても言い訳付くんですよ。
アタタタタタタタ...(あっ!ミスった)...お前はもう...全身打撲!
経絡秘孔を付き間違ったとしても、「あっ!外した!」と感じたならば、必殺技名を言わなければいいんですよ。ただ単に指が体に食い込んで痛いだけの技になるんですよ。
はい、どうでもいいですね。
最近は必殺技を叫ばないのが普通なんだろうか。
必殺技を叫ぶのは昭和なんだろうか...
2009/11/05
できなくはない...かな。
Vim-users.jp - Hack #98: VimScriptで疑似乱数を生成する
VimScriptには、残念ながら疑似乱数を生成するための関数が存在しません。
reltimestr()やreltime()はVimが+reltimeでコンパイルされていないと動作しません。
http://vim-users.jp/2009/11/hack98/
+reltimeでなくても乱数を発生させられる。
しかも擬似乱数じゃなく。
:call libcallnr("", "srand", localtime())
:echo libcallnr("", "rand", -1)
-1はダミーね
Windowsの場合は
:call libcallnr("libcmt.dll", "srand", localtime())
:echo libcallnr("libcmt.dll", "rand", -1)
とか
:call libcallnr("msvcrt.dll", "srand", localtime())
:echo libcallnr("msvcrt.dll", "rand", -1)
とかで動くと思う。ためしてない。
2009/10/30
最近、kazuhoさんが作った「C」で遊んでいるのですが(いまごろかい!)、これWindowsでも使いたいなーなんて思ったのでポーティングしてみた。
C - a pseudo-interpreter of the C programming language
Perl や Ruby では、ワンライナーで処理が書けて便利です。でも、なぜか C では書くことができません。仕事上の都合で、小さな処理を C 言語で書く必要があったので、ワンライナーも書くことのできる C 言語のインタプリタ(?)を作ってみました。
http://labs.cybozu.co.jp/blog/kazuho/archives/2006/01/large_c.php
目指せバイナリアン (C-0.06)
C-0.06 をリリースします。
http://labs.cybozu.co.jp/blog/kazuho/archives/2006/05/c-0_06.php
これがあるとコマンドプロンプトから
C:¥>C
puts("kazuho");
^D
kazuho
C:¥>
こんな事が出来たり、
C:¥>C -e "printf("""hello world¥n""");
hello world
C:¥>
こんな事が出来たりします。コマンドプロンプトなのでクォートのエスケープ2重打ちがめんどくさいですが(実際にはクオートの中のクォートなので3重になります)、なれれば簡単ですし昔なつかし
nyacusなんかを使えばシングルクォートでも行けるはずです。(cygwin?何それ)
コンパイルや実行には
mingw32が必要です。
コンパイルは以下の様に簡単。
C:¥C-0.06¥>gcc -o C.exe C.c
mingw32が出力するa.exeに対応しています。ちょっと弄ればMSVCにも対応出来るんじゃないかな。
kazuhoさんに感謝しつつ、Version画面に「Win32 Porting」として名前を入れさせて頂いています。
mattn's C-win32 at master - GitHub
win32 port of C(a pseudo-interpreter of the C programming language)
http://github.com/mattn/C-win32
Windowsユーザでコマンドプロンプト使いで、mingw32が入っててC言語をこよなく愛する皆さんにどうぞ...。
対象範囲せま!
昨日IRCで
security guyの人と話してて、twitterに新しく導入された機能「list」には少し不満があるという話をしてた。
ざっと上げると
- 個人単位のlistであり、追加するにはそのlistの持ち主でしか追加出来ない。
- UIが痛い。listについての画面遷移、追加削除がやりにくい。
- 他人のlistを自分のlistにimportしたいが出来ない。
こんな感じ。まだまだbetaだからこれから良くなるのかもしれないけど、さすがにimportは欲しいよね...って事で3分程度で作ってみた。
APIは昨日調べてなんとなく分かってたので、だいぶ楽に作れた。
#!/usr/bin/env perl
use strict;
use warnings;
use LWP::UserAgent;
use JSON;
use Config::Pit;
my $copy_from = $ARGV[0] || 'hasegawayosuke/javascript';
my $copy_to = $ARGV[1] || 'mattn_jp/javascripter';
my $config = pit_get("twitter.com", requires => {
username => 'username in twitetr.com',
password => 'password in twitetr.com',
});
my $ua = new LWP::UserAgent;
$ua->env_proxy;
$ua->credentials(
"twitter.com:80", "Twitter API",
$config->{username} => $config->{password}
);
my $json = from_json($ua->get("http://twitter.com/$copy_from/members.json")->decoded_content);
for (@{$json->{users}}) {
$ua->post("http://twitter.com/$copy_to/members.json", {id => $_->{id}});
}
使い方は
# perl copy-to-mylist-from-others.pl hasegawayosuke/javascript mattn_jp/javascripter
こんな感じ。実行するときにはジャイアンのテーマを脳内再生しながら実行して下さい。
2009/10/29
C言語ってlambdaが書けないのでGUIのコールバックなんかを作る場合には関数を用意しなければならないのですが...
Lambda abstractions in C++ vs. Scheme
1. Simple lambda-expressions
#define Lambda(args,ret_type,body) \
class MakeName(__Lambda___) { \
public: ret_type operator() args { body; } }
http://okmij.org/ftp/c++-digest/Lambda-CPP-more.html
なるほどねー。これなら
Lambda((int a, int b), int, return a+b) foo;
std::cout << foo(1, 2) << std::endl;
こんな書き方も出来る訳か。ただGUIなんかのコールバックだと関数ポインタが必要だし、operator()のアドレスなんて取る必要もないからstaticで宣言した関数ポインタで十分かな。
fltkだとこんな感じにmainの外に関数宣言しなくても良くなるね。
#include <fltk/Window.h>
#include <fltk/Button.h>
#include <fltk/ask.h>
#include <fltk/run.h>
#include <stdlib.h>
int main(void) {
#define lambda(args,ret_type,block) \
class { public: static ret_type func args {block;} }
lambda((fltk::Widget* wid, void* data), void, fltk::alert("don't click!")) button_clicked;
fltk::Window w(0, 0, 300, 100, "lambda window");
w.begin();
fltk::Button b(10, 10, 280, 80, "click me!");
b.callback(&button_clicked.func, 0);
w.end();
w.show();
return fltk::run();
}
mainしか宣言したく無い人にはいいかも。
先日知ったんだけど、最近のVisual Studioに付いてるコンパイラだと
auto func [&](int a, int b) -> int { return a+b; };
std::cout << func(1, 2) << std::endl;
こんな書き方が出来る
らしい。
2009/10/23
githubが高速化に成功した様です。
How We Made GitHub Fast - GitHub
Now that things have settled down from the move to Rackspace, I wanted to take some time to go over the architectural changes that we’ve made in order to bring you a speedier, more scalable GitHub.
...
For our data serialization and RPC protocol we are using BERT and BERT-RPC.
http://github.com/blog/530-how-we-made-github-fast
データのシリアライズおよびRPC(リモートプロシージャコール)としてBERT-RPCを選んだ様です。
BERT and BERT-RPC 1.0 Specification
BERT and BERT-RPC are an attempt to specify a flexible binary serialization and RPC protocol that are compatible with the philosophies of dynamic languages such as Ruby, Python, PERL, JavaScript, Erlang, Lua, etc. BERT aims to be as simple as possible while maintaining support for the advanced data types we have come to know and love. BERT-RPC is designed to work seamlessly within a dynamic/agile development workflow. The BERT-RPC philosophy is to eliminate extraneous type checking, IDL specification, and code generation. This frees the developer to actually get things done.
http://bert-rpc.org/
BERT-RPCはBERT(Binary ERlang Term)という名前の通り、ERlangのterm_to_binary/1で生成されるフレキシブルなバイナリデータ交換の仕組みで、これをRPCに利用した物がBERT-RPCです。
BERT
BERT (Binary ERlang Term) is a flexible binary data interchange format based on (and compatible with) Erlang's binary serialization format (as used by erlang:term_to_binary/1).
http://bert-rpc.org/
erlangで使える、integer,float,atom,tupple,bytelist,list,binaryをシリアライズして転送する事が出来ます。仕様書はerlangの拡張仕様ページに掲載されています。
External Term Format
The external term format is mainly used in the distribution mechanism of Erlang.
http://erlang.org/doc/apps/erts/erl_ext_dist.html
詳しくは
githubのブログエントリでも説明されています。
最近ではMessagePackやProtocolBuffer、Thriftなどバイナリフォーマットを使ったRPCが流行ですが、このErlangのバイナリフォーマットは無駄の無いシリアライズ形式になっているかと思います。
実際には型識別と、それに後続するデータで構成され、型によってはデータ部の前にデータ長が付加されます。
Erlangのシリアライズでは、Erlangのprotocolバージョン131から開始されますが、その前に全体長を付与した物がRPCで使われています。RPCの本体はtuppleと、その中のATOMで構成されています。
実装を見たい方は以下のrubyによる実装を見る事が出来ます。
mojombo's bert at master - GitHub
BERT (Binary ERlang Term) serialization library for Ruby.
http://github.com/mojombo/bert
mojombo's bertrpc at master - GitHub
BERTRPC is a Ruby BERT-RPC client library.
http://github.com/mojombo/bertrpc
またerlangによるシリアライズ実装、およびBERT-RPCを使ったRPCサーバの実装は以下から参照出来ます。
mojombo's erlectricity at master - GitHub
Erlectricity exposes Ruby to Erlang and vice versa
http://github.com/mojombo/erlectricity
mojombo's ernie at master - GitHub
Ernie is an Erlang/Ruby BERT-RPC Server.
http://github.com/mojombo/ernie
例えば上記githubブログへのリンク先で説明されている様に
require 'ernie'
mod(:calc) do
fun(:add) do |a, b|
a + b
end
end
というサーバのコードに対しては
require 'bertrpc'
svc = BERTRPC::Service.new('localhost', 9999)
svc.call.calc.add(1, 2)
というクライアントのコードが書けるのですが、内部ではErlangでシリアライズされ、先頭にデータ長が付与された以下のイメージでデータ送信が行われます。
{call,calc,add,[1,2]}
そしてバイト列としては以下の様になります。
0000000: 0000 0021 8368 0464 0004 6361 6c6c 6400 ...!.h.d..calld.
0000010: 0463 616c 6364 0003 6164 646c 0000 0002 .calcd..addl....
0000020: 6101 6102 6a a.a.j
先頭4バイトにデータ長(0x21:33バイト)、バージョン(0x83:131)、tupple(0x68)、tuppleアイテム数(4)、atom ext(0x64)...と続きます。
今日はこのBERT-RPCで動く足し算サーバををC言語から呼び出すサンプルを書いてみました。
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
int
main(int argc, char *argv[]) {
int sock;
struct sockaddr_in serv_addr;
short nport = htons(9999);
struct hostent *host_ent;
char *host = (char*)strdup("127.0.0.1");
char *ptr;
FILE *sockfp;
ptr = strchr(host, ':');
if (ptr) {
*ptr++ = 0;
if (atoi(ptr) == 0) {
struct servent *serv_ent;
serv_ent = getservbyname(ptr, "tcp");
if (serv_ent)
nport = serv_ent->s_port;
} else {
nport = htons(atoi(ptr));
}
}
ptr = host;
if ((host_ent = gethostbyname(ptr)) == NULL) {
sock = -1;
return -1;
}
memset((char *)&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
memcpy(&serv_addr.sin_addr, host_ent->h_addr, host_ent->h_length);
serv_addr.sin_port = nport;
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
return;
}
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
close(sock);
sock = -1;
return;
}
char n1;
short n2;
int n4;
n4 = htonl(33);
write(sock, &n4, sizeof(n4));
n1 = 131;
write(sock, &n1, sizeof(n1));
n1 = 104;
write(sock, &n1, sizeof(n1));
n1 = 4;
write(sock, &n1, sizeof(n1));
n1 = 100;
write(sock, &n1, sizeof(n1));
n2 = htons(4);
write(sock, &n2, sizeof(n2));
write(sock, "call", 4);
n1 = 100;
write(sock, &n1, sizeof(n1));
n2 = htons(4);
write(sock, &n2, sizeof(n2));
write(sock, "calc", 4);
n1 = 100;
write(sock, &n1, sizeof(n1));
n2 = htons(3);
write(sock, &n2, sizeof(n2));
write(sock, "add", 3);
n1 = 108;
write(sock, &n1, sizeof(n1));
n4 = htonl(2);
write(sock, &n4, sizeof(n4));
n1 = 97;
write(sock, &n1, sizeof(n1));
n1 = 1;
write(sock, &n1, sizeof(n1));
n1 = 97;
write(sock, &n1, sizeof(n1));
n1 = 2;
write(sock, &n1, sizeof(n1));
n1 = 106;
write(sock, &n1, 1);
char buf[256];
ptr = buf;
int x = 0, n, l;
l = recv(sock, buf, sizeof(buf), 0);
printf("recv size %d\n", l);
for (n = 0; n < l; n++) {
x = (x + 1) % 80;
printf("%02X ", (unsigned char)buf[n]);
if (x == 0) putchar('\n');
}
putchar('\n');
n4 = (unsigned int) (
(*(ptr+0) << 24) +
(*(ptr+1) << 16) +
(*(ptr+2) << 8) +
(*(ptr+3) << 0));
printf("total length = %d\n", n4);
ptr+=4;
n1 = *ptr;
printf("version = %d\n", (unsigned char)n1);
ptr++;
n1 = *ptr;
if (n1 == 104) printf("tupple extension\n");
ptr++;
n1 = *ptr;
printf("number of tupple items %d\n", n1);
ptr++;
n1 = *ptr;
if (n1 == 100) printf("atom extension\n");
ptr++;
n2 = (unsigned short) (
(*(ptr+0) << 8) +
(*(ptr+1) << 0));
printf("atom length = %d\n", n2);
ptr+=2;
char atom[200] = {0};
memcpy(atom, ptr, n2);
printf("atom name = %s\n", atom);
ptr+=n2;
n1 = *ptr;
if (n1 == 97)printf("small int\n");
ptr++;
n1 = *ptr;
printf("int %d\n", n1);
close(sock);
sock = -1;
return 0;
}
分かりやすい様にコメントに種別を書いていますが、実用するには構造化したり抽象化する必要があります。ただし言語によっては型が見合わない場合もあるので、擬似するコードを書かなければならない可能性もあります。まだrubyとjavascriptにしか実装がない様なので、どなたかチャレンジしてみてはいかがでしょうか。
2009/10/16
まぁ手順通りだけど...
Ubuntu 9.04で作りました。以下手順。
必要なソフトウェアのインストール
# sudo apt-get install texlive-xetex latex-cjk-xcjk pandoc texlive-latex-recommended
ちなみにtexlive-latex-recommendedを入れないと"kvoptions.styが無いよ!"と怒られるので注意。
progitのリポジトリをcloneする
# git clone git://github.com/progit/progit.git
git入れてない人は
プロジェクトページのdownloadボタンで...
フォントを設定する
latexというフォルダに移動し、config.xmlというファイルがあるのでfontを設定する。"
Japan"なんて
郷ひろみばりのフォント持ってないのでIPAGothicを指定した。
diff --git a/latex/config.yml b/latex/config.yml
index c8f0d61..305e262 100644
--- a/latex/config.yml
+++ b/latex/config.yml
@@ -9,7 +9,8 @@ zh:
font: AR PL UMing CN
fig: "图 "
ja:
- font: Japan
+ font: IPAGothic
+ mono: IPAGothic
fig: "図"
tab: "表"
ru:
あとは実行
# ./makepdf ja
jaフォルダの中にmain.pdfというファイルが出来る。
ウマー! 印刷して持ち歩こう。
2009/10/14
Google Waveにinviteされた。人からじゃなくて前に申し込んでたのが来たらしい。
ブログで招待しようかと思ったけど、いつもお世話になっている人たちにIRCで「いりますか?」と尋ねたら40分くらいで完売。
って事でタイトルは「Google Waveに招待します」じゃなく「Google Waveに招待しました」になっちゃいました。
2009/10/09
tokuhiromさんがnanowwwという、C++から簡単にHTTPが扱えるクライアントライブラリを書いてくれたので、botを書いてみた。
tokuhirom's nanowww at master - GitHub
C++ lightweight, fast, portable HTTP client library
http://github.com/tokuhirom/nanowww
picojsonは
以前ご紹介した通り、kazuhoさんが書いたC++からSTLと親和性の高いJSONパーサです。
今回はtokuhiromさんが、C++で、もちろんSTLと親和性の高いHTTPクライアントライブラリを書いてくれました。
これを使えば、例えばhttp://example.com/fooというURLにPOSTで"name=hasegawa"を送信するとした場合
nanowww::Client client;
nanowww::Request req("POST", "http://example.com/foo", "name=hasegawa");
nanowww::Response res;
req.set_header("Content-Type", "application/x-www-form-urlencoded");
if (client.send_request(req, &res))
std::cout << res.content() << std::endl;
else
std::cout << client.errstr() << std::endl;
これだけのコードでHTTP通信出来てしまいます。
C++らしくていいですね。CURLよりはインタフェースが優れていると思います。ただ現状はPROXYに対応していなかったり、Windowsで使えなかったりしますので、用途を絞られますが、今後に期待したいと思います。
さて今日はこれを使って、IRCに常駐し、指定のチャネルに入り、指定の単語が現れると「それXXX関係ないだろ!jk」と発言する、「関係ないbot」を書きました。実際にIRCプロトコルを喋っている訳でなく、webchat.freenode.netのAjax通信を、nanowwwとpicojsonによる擬似Ajax通信を行って実現しています。
以下、少々長いですが全ソースコード。
#include "picojson.h"
#include "nanowww.h"
using namespace std;
using namespace picojson;
using namespace nanowww;
#ifdef _WIN32
static char* utf8_to_str_alloc(const char* utf8) {
size_t in_len = strlen(utf8);
wchar_t* wcsdata;
char* mbsdata;
size_t mbssize, wcssize;
wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, in_len, NULL, 0);
wcsdata = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t));
wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, in_len, wcsdata, wcssize + 1);
wcsdata[wcssize] = 0;
mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsdata, -1, NULL, 0, NULL, NULL);
mbsdata = (char*) malloc((mbssize + 1));
mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsdata, -1, mbsdata, mbssize, NULL, NULL);
mbsdata[mbssize] = 0;
free(wcsdata);
return mbsdata;
}
#endif
value get_json(string url, map<string, string>& postdata) {
value v;
char error[256];
string data;
map<string, string>::iterator it;
for (it = postdata.begin(); it != postdata.end(); it++) {
if (data.size()) data += "&";
data += it->first + "=";
data += it->second;
}
Client client;
Request req("POST", url.c_str(), data.c_str());
Response res;
req.set_header("Content-Type", "application/x-www-form-urlencoded");
if (client.send_request(req, &res)) {
const char* ptr = res.content().c_str();
string err = parse(v, ptr, ptr + strlen(ptr));
if (!err.empty()) cerr << err << endl;
}
return v;
}
int
main(int argc, char* argv[]) {
map<string, string> postdata;
value v;
bool loop;
if (argc != 4) {
std::cerr << "usage: kankeinaibot [nick] [channel] [word]" << std::endl;
return -1;
}
string nick = argv[1];
string channel = argv[2];
string word = argv[3];
postdata.clear();
postdata["nick"] = nick;
v = get_json("http://webchat.freenode.net/e/n", postdata);
string sid = v.get<array>()[1].get<string>();
loop = true;
postdata.clear();
postdata["s"] = sid;
while (loop) {
v = get_json("http://webchat.freenode.net/e/s", postdata);
array a = v.get<array>();
for (array::iterator it = a.begin(); it != a.end(); it++) {
if (!it->is<array>()) continue;
array line = it->get<array>();
if (line[0].to_str() == "c") {
if (line[1].to_str() == "443") {
postdata["nick"] += "_";
v = get_json("http://webchat.freenode.net/e/n", postdata);
sid = v.get<array>()[1].get<string>();
break;
}
if (line[1].to_str() == "376") {
loop = false;
break;
}
}
}
}
loop = true;
postdata.clear();
postdata["c"] = "JOIN #";
postdata["c"] += channel;
postdata["s"] = sid;
v = get_json("http://webchat.freenode.net/e/p", postdata);
while (loop) {
v = get_json("http://webchat.freenode.net/e/s", postdata);
array a = v.get<array>();
for (array::iterator n = a.begin(); n != a.end(); n++) {
array line = n->get<array>();
if (line[0].to_str() == "c") {
array aa = line[3].get<array>();
if (aa.size() > 1 && aa[0].to_str() != nick) {
cout << aa[0].serialize() << endl;
cout << aa[1].serialize() << endl;
if (!strncmp(aa[1].to_str().c_str(), word.c_str(), word.size())) {
map<string, string> saydata;
saydata["s"] = sid;
std::stringstream s;
s << "PRIVMSG #" << channel << " :" <<
"それ" << word << "関係ないだろ!jk";
saydata["c"] = s.str();
cout << saydata["c"] << endl;
v = get_json("http://webchat.freenode.net/e/p", saydata);
}
}
string s = line[3].serialize();
#ifdef _WIN32
char* p = utf8_to_str_alloc(s.c_str());
cout << p;
cout << " ";
free((void*)p);
cout << endl;
#else
cout << s << endl;
#endif
}
}
}
return 0;
}
CURLを使った場合のレスポンスwriterを書かなくてもいいので、かなりスッキリしましたね。
ソースはgithubにも置いてあるのでよろしければ試してみて下さい。
mattn's kankeinai-bot at master - GitHub
関係ないbot
http://github.com/mattn/kankeinai-bot
えっ?全然botじゃない?
bot関係ないだろ!jk
2009/10/08
Javaで...
class test{public static void main(String[]a){for(int n=1;n<101;n++)System.out.println(n%15>0?n%3>0?n%5>0?""+n:"Buzz":"Fizz":"FizzBuzz");}}
むー139バイトで46位。遠いなー。
じゃぁ
Rubyで...
puts (1..100).map{|i|i%15==0%1?:FizzBuzz:i%3==0?:Fizz:i%5==0?:Buzz:i}
むむー。72バイトで65位。遠いよー。
なっ...ならば
Luaで!!!汗...
for v=1,100 do print(v%15>0 and(v%3>0 and(v%5>0 and v or"Buzz")or"Fizz")or"FizzBuzz")end
88バイトで11位。ムキーーーーッ!
寝る!
2009/10/07
以前、
Growl For Windowsの
紹介記事で
gntp-sendという単純なCのプログラム(パスワードハッシュのみサポート)を書いたのですが、github上で
psinnottさんという方がforkしてUDP送信機能を付けてくれました。オープンソースの素晴らしい所ですね。
さらにlibgrowlというライブラリとしても使える様にしたので、アプリケーションに簡単に組み込む事が出来ます。
GNTPプロトコルは今のところWindowsだけしか使えませんが、UDP送信であればアイコンは出ないものの、他のプラットフォームでもGrowl出来る様になっています。現在WindowsとLinuxで動作確認出来ています。
なお、Windowsの場合はGrowlNotifyというDLLエントリポイントをrundll32用にエクスポートしてありますので
C:\>rundll32 growl.dll,GrowlNotify localhost:23053,MyApp,MyNotify,これはすごい,よーわからんけどね!,http://mattn.kaoriya.net/images/logo.png
と実行する事でGrowl For WindowsでGrowlされる様になっています。gntp-sendという付属コマンドもありますがコマンドラインからrundll32経由で使えて便利ですね!
誰得な機能っていわないで!><
blosxomにはnotfoundプラグインがあるので入れた。デフォルトのテンプレートのままだと見栄えが宜しくないので、"page.notfound"というファイルを作って各テンプレートファイルを引っ付けた物をベースに文言などを書いた。
見た目は
こんな感じ。
2009/10/05
spiritlooseさんがPSGIなapacheモジュールを書いてくれたのでWindowsにポーティングしてみた。
mod_psgi を実装してみた - spiritlooseのはてなダイアリー
PSGI を実装したApache2モジュール。
http://d.hatena.ne.jp/spiritloose/20091002/1254467284
ちょっと気持ち悪いpatchになるけど、一応動いている。
巷にあるライブラリなどではWindowsポーティングされた時にUN*Xライクな型を自前で定義している事があるんだけど、Apacheモジュールのapr.hなんかや、Perlなんかでuid_t/gid_t/pid_tなんかが定義されている事が多い。
apr.h
typedef int uid_t;
perl.h
typedef long uid_t;
もちろんこのヘッダを同時に読み込むとエラーになるんですが、こういう場合に私がよく使う手として
#define uid_t _uid_t
#include <apr.h>
#undef uid_t
#include <perl.h>
てな具合に前の宣言を逃してやって、後の宣言を有効に出来る。今回はこの気持ち悪いhackを使ってWindowsにポーティングしました。
Commit 03fb93ebf27f0b98c3fbcc616ce736129e747912 to mattn's mod_psgi - GitHub
http://github.com/mattn/mod_psgi/commit/03fb93ebf27f0b98c3fbcc616ce736129e747912
今の所ご機嫌よく動いていて
use strict;
use warnings;
my $count = 0;
my $handler = sub {
my $content = "Hello World".$count++;
return [ 200, [ "Content-Type" => "text/plain", "Content-Length" => length($content) ], [ $content ] ];
};
こんなPSGIなアプリもうまくちゃんとリクエスト毎にカウントアップされています。
もちろんpreforkだと同じ結果が現れる事がありますが
まだマージして頂いていないので、捨てコードになるかもしれませんが...
AWSWORD:perl:
2009/10/02
tthttpd(tinytinyhttpd)にiratqqさんがIPv6対応をコードを入れてくれました。通常起動でIPv4、IPv6どちらも扱える様になっていて、引数で
# tthttpd -4
とすればIPv4だけで
# tthttpd -6
とすればIPv6だけで起動する様になります。iratqq++
また、静的なファイルをサーブする場合にはこれまでreadしながらsendしていたのですが、sendfile(2)を使う様にしました。Windowsの場合は
昨日Plackの高速化対応した時の様にTransmitFileを使うようにしました。
小さいサイズのファイルだと差は出ませんが、数メガくらいのファイルから格段に差が出て速くなりました。
mattn's tinytinyhttpd at master - GitHub
tiny tiny httpd
http://github.com/mattn/tinytinyhttpd
最近熱い
PSGI/Packですが、サーバのStandaloneではかなり高速なパフォーマンスが出ているらしく、試してみようと思った所、Windowsでは動かない箇所があったのでいろいろやった作業履歴。
まず、Standaloneサーバが速いと言われる理由としてsendfile(2)を
使っているのですが、Windowsにはsendfile(2)がありません。しかしソケット前提であるならば
TransmitFileというAPIがあります。
sendfile(2)と同じようにZeroCopyでファイルから指定ディスクリプタに流し込むAPIです。
Plackが使っているSys::Sendfileに対してこのAPIを使う様に
Windowsポーティングしました。
Sys::Sendfileのauthorにもpull requestを送りました。次にPlackにてWindowsで使えないAPIを叩いている部分に対するpatchを書きました。
これで一応、高速化対応は完了。
Plackに付属の画像を返すpsgiアプリケーションを起動してベンチマークを取ってみました。
# plackup -s Standalone -a eg/dot-psgi/image.psgi
ベンチマーク結果は以下の通り。まず高速化対応前。
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software: Plack-Server-Standalone/0.01
Server Hostname: localhost
Server Port: 8080
Document Path: /
Document Length: 2397678 bytes
Concurrency Level: 1
Time taken for tests: 1.219 seconds
Complete requests: 10
Failed requests: 0
Write errors: 0
Total transferred: 23978730 bytes
HTML transferred: 23976780 bytes
Requests per second: 8.21 [#/sec] (mean)
Time per request: 121.876 [ms] (mean)
Time per request: 121.876 [ms] (mean, across all concurrent requests)
Transfer rate: 19213.49 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 2 4.9 0 16
Processing: 109 119 10.9 125 141
Waiting: 0 5 7.5 0 16
Total: 109 120 12.9 125 141
Percentage of the requests served within a certain time (ms)
50% 125
66% 125
75% 125
80% 141
90% 141
95% 141
98% 141
99% 141
100% 141 (longest request)
そして対応後
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software: Plack-Server-Standalone/0.01
Server Hostname: localhost
Server Port: 8080
Document Path: /
Document Length: 2397701 bytes
Concurrency Level: 1
Time taken for tests: 0.578 seconds
Complete requests: 10
Failed requests: 0
Write errors: 0
Total transferred: 23978960 bytes
HTML transferred: 23977010 bytes
Requests per second: 17.30 [#/sec] (mean)
Time per request: 57.813 [ms] (mean)
Time per request: 57.813 [ms] (mean, across all concurrent requests)
Transfer rate: 40504.51 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 3 6.6 0 16
Processing: 31 55 22.4 47 94
Waiting: 0 3 6.6 0 16
Total: 31 58 24.5 47 109
Percentage of the requests served within a certain time (ms)
50% 47
66% 47
75% 63
80% 94
90% 109
95% 109
98% 109
99% 109
100% 109 (longest request)
だいたいRequests perl secondで2倍くらい速くなってます。すばらしい。
miyagawa++ kazuho++ Plack++
2009/09/30
ASINを指定すると、Amazonのアフィをヨロシク表示してくれるblosxomのプラグインで
awsxomというのがあるのですが、以前それをItemSearchにも対応させ、以降何回か使ってました。ただ最近は記事を書く際に文章で頭が一杯になってしまい、毎回アフィを貼るのを忘れてしまうという難病にかかってしまったせいでawsxomを
amazonの仕様変更に追従させるのを忘れてました。
で、案の定
先ほどの記事をポストした際にASIN書いたら見事に記事が壊れて泣くハメに...
ええいと重い腰を上げて修正してみました。
修正方法は
hail2uさんが書いた物をベースに修正しました。
あまりawxsomの原形を留めていないので修正後のファイルで...
#!/usr/bin/perl
package awsxom;
use strict;
use LWP::UserAgent;
use CGI qw/:standard/;
use FileHandle;
use URI::Escape;
use Digest::SHA::PurePerl qw(hmac_sha256_base64);
my $asoid = "XXXXXX-22";
my $devkey = "XXXXXXXXXXXXXXXXXXXX";
my $secret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
my $cachedir = "$blosxom::plugin_state_dir/aws_cache";
my $EXPIRE = 24 * 7;
my $default_template = "awsxom";
my $VERSION = '1.4';
my $ua_name = "awsxom $VERSION";
my $endpoint = "ecs.amazonaws.jp";
my $unsafe = "^A-Za-z0-9\-_.~";
my $debug_mode = 0;
sub start {
if (!-e $cachedir) {
my $mkdir_r = mkdir($cachedir, 0755);
warn $mkdir_r
? "blosxom : aws plugin > \$cachedir, $cachedir, created.\n"
: "blosxom : aws plugin > mkdir missed:$!";
$mkdir_r or return 0;
}
1;
}
sub story {
my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
$$body_ref = conv($$body_ref);
1;
}
sub foot {
my($pkg, $currentdir, $foot_ref) = @_;
$$foot_ref = conv($$foot_ref);
1;
}
sub conv {
$_ = shift;
s/(?:ASIN|ISBN):([A-Z0-9]{10}):(.*?):/to_html_asin($1,$2)/ge;
s/(?:ASIN|ISBN):([A-Z0-9]{10})/to_html_asin($1,$default_template)/ge;
s/(?:AWSWORD):([a-zA-Z0-9_]*?):/to_html_word($1,$default_template)/ge;
return $_;
}
sub to_html_asin {
my ($asin, $template) = @_;
my $cache = "$cachedir/$asin.xml";
my $outfile = "$cachedir/$asin.html";
my $q = CGI->new;
$q->param("AWSAccessKeyId", $devkey);
$q->param("AssociateTag", $asoid);
$q->param("Timestamp", sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000Z", sub { ($_[5]+1900, $_[4]+1, $_[3], $_[2], $_[1], $_[0] ) }->(gmtime(time))));
$q->param("Service", "AWSECommerceService");
$q->param("Operation", "ItemLookup");
$q->param("ItemId", $asin);
$q->param("ResponseGroup", "Medium,Offers");
$q->param("Version", "2009-01-06");
my @p = $q->param();
foreach (@p) {
$_ = escape($_) . "=" . escape($q->param($_));
}
my $qs = join("&", sort(@p));
my $signature = hmac_sha256_base64("GET\n$endpoint\n/onca/xml\n$qs", $secret) . "=";
my $url = "http://$endpoint/onca/xml?$qs&Signature=" . escape($signature);
if (!(-e $cache) || (-M $cache > ($EXPIRE / 24))) {
my $ua = new LWP::UserAgent;
$ua->agent($ua_name);
$ua->timeout(60);
my $rtn = $ua->mirror($url, $cache);
}
my $content = getFile($cache);
my %detail = parseXML($content, $asin);
my $form;
if (!defined($detail{"ErrorMsg"})) {
my $fh = new FileHandle;
if ($fh->open("< $blosxom::datadir/$template.html")) {
$form = join '', <$fh>;
$form =~ s/\$(\w+)/$detail{$1}/ge;
$fh->close();
}
}
else {
$form = "<p>" . $detail{"ErrorMsg"} . "</p>";
}
return $form;
}
sub to_html_word {
my ($word, $template) = @_;
my $cache = "$cachedir/$word.xml";
my $outfile = "$cachedir/$word.html";
my $q = CGI->new;
$q->param("AWSAccessKeyId", $devkey);
$q->param("AssociateTag", $asoid);
$q->param("Timestamp", sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000Z", sub { ($_[5]+1900, $_[4]+1, $_[3], $_[2], $_[1], $_[0] ) }->(gmtime(time))));
$q->param("Service", "AWSECommerceService");
$q->param("Operation", "ItemSearch");
$q->param("Keywords", $word);
$q->param("SearchIndex", "Books");
$q->param("ResponseGroup", "Medium,Offers");
$q->param("Version", "2009-01-06");
my @p = $q->param();
foreach (@p) {
$_ = escape($_) . "=" . escape($q->param($_));
}
my $qs = join("&", sort(@p));
my $signature = hmac_sha256_base64("GET\n$endpoint\n/onca/xml\n$qs", $secret) . "=";
my $url = "http://$endpoint/onca/xml?$qs&Signature=" . escape($signature);
if (!(-e $cache) || (-M $cache > ($EXPIRE / 24))) {
my $ua = new LWP::UserAgent;
$ua->agent($ua_name);
$ua->timeout(60);
my $rtn = $ua->mirror($url, $cache);
}
my $content = getFile($cache);
$content =~ s!.*?(<Item>.*?</Item>).*!$1!is;
my $asin = "";
$asin = $1 if ($content =~ /<ASIN>([^<]*)<\/ASIN>/);
return "" if !$asin;
my %detail = parseXML($content, $asin);
my $form;
if (!defined($detail{"ErrorMsg"})) {
my $fh = new FileHandle;
if ($fh->open("< $blosxom::datadir/$template.html")) {
$form = join '', <$fh>;
$form =~ s/\$(\w+)/$detail{$1}/ge;
$fh->close();
}
}
else {
$form = "<p>" . $detail{"ErrorMsg"} . "</p>";
}
return $form;
}
sub getFile {
my $cache = shift;
my $fh = new FileHandle;
$fh->open($cache);
my @data = <$fh>;
$fh->close();
my $content = join('', @data);
return undef if (!$content);
return $content;
}
sub parseXML {
my ($buf, $asin) = @_;
my %detail;
$detail{"Link"} = "http://www.amazon.co.jp/exec/obidos/ASIN/$asin/ref=nosim/$asoid";
$detail{"Asin"} = $1 if ($buf =~ /<ASIN>([^<]*)<\/ASIN>/);
$detail{"ProductName"} = $1 if ($buf =~ /<Title>([^<]*)<\/Title>/);
$detail{"Catalog"} = $1 if ($buf =~ /<Binding>([^<]*)<\/Binding>/);
$detail{"ReleaseDate"} = $1 if ($buf =~ /<PublicationDate>([^<]*)<\/PublicationDate>/);
$detail{"ReleaseDate"} = $1 if ($buf =~ /<ReleaseDate>([^<]*)<\/ReleaseDate>/);
$detail{"Manufacturer"} = $1 if ($buf =~ /<Manufacturer>([^<]*)<\/Manufacturer>/);
$detail{"ImageUrlSmall"} = $1 if ($buf =~ /<SmallImage>[^<]*?<URL>([^<]*)<\/URL>/);
$detail{"ImageUrlMedium"} = $1 if ($buf =~ /<MediumImage>[^<]*?<URL>([^<]*)<\/URL>/);
$detail{"ImageUrlLarge"} = $1 if ($buf =~ /<LargeImage>[^<]*?<URL>([^<]*)<\/URL>/);
$detail{"Availability"} = $1 if ($buf =~ /<Availability>([^<]*)<\/Availability>/);
$detail{"ListPrice"} = $1 if ($buf =~ /<LowestNewPrice>.*?<FormattedPrice>([^<]*)<\/FormattedPrice>/);
$detail{"OurPrice"} = $1 if ($buf =~ /<ListPrice>.*?<FormattedPrice>([^<]*)<\/FormattedPrice>/);
$detail{"UsedPrice"} = $1 if ($buf =~ /<LowestUsedPrice>.*?<FormattedPrice>([^<]*)<\/FormattedPrice>/);
$detail{"Author"} = $1 if ($buf =~ /<Author>([^<]*)<\/Author>/);
if ($buf =~ /<Errors>.*?<Message>([^<]*)<\/Message>/) {
$detail{"ErrorMsg"} = $1;
}
return %detail;
}
sub print_debug {
return if (!$debug_mode);
my $fd = new FileHandle;
$fd->open("/path/to/log/output/directory/logfile.log", "a");
print $fd "$_[0]";
$fd->close();
}
sub escape {
my $s = shift;
$s =~ s/([^\0-\x7F])/do {
my $o = ord($1);
sprintf("%c%c", 0xc0 | ($o >> 6), 0x80 | ($o & 0x3f));
}/ge;
return uri_escape($s, $unsafe);
}
1;
ちなみに、オリジナルに追加した機能は「AWSWORD:perl」と書くとItemSearch結果の1番目を表示する...というモノグサ機能です。
久々
tthttpd(tinytinyhttpd)を触っていて、動いてるだろうと思ってたkeep-aliveのコードが動いていなかった事に愕然としながら修正してたら、以前
smegheadさんが報告してくれていたmingw32で落ちる問題の原因を発見。
記憶の底で、_beginthread()したらCloseHandle()しなきゃいねないと思い込んでいたんですが、どうやら_beginthread()じゃなくて_beginthreadex()の場合だけだった(
参照)。_beginthread()の場合はスレッド終了時にリソース回収されるらしい。
他いろいろと修正してkeep-aliveが動くようになり、ベンチマーク取ってみた。
テストは「helloworld」と書かれたhello.txtをabでGETする物。
まずはconnection closeの場合
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software:
Server Hostname: localhost
Server Port: 8080
Document Path: /hello.txt
Document Length: 11 bytes
Concurrency Level: 10
Time taken for tests: 0.993 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 160000 bytes
HTML transferred: 11000 bytes
Requests per second: 1006.77 [#/sec] (mean)
Time per request: 9.933 [ms] (mean)
Time per request: 0.993 [ms] (mean, across all concurrent requests)
Transfer rate: 157.31 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 1
Processing: 1 10 13.0 6 128
Waiting: 0 10 12.9 6 128
Total: 2 10 13.0 6 129
Percentage of the requests served within a certain time (ms)
50% 6
66% 7
75% 7
80% 7
90% 15
95% 31
98% 59
99% 86
100% 129 (longest request)
そしてconnection keep-aliveの場合
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software:
Server Hostname: localhost
Server Port: 8080
Document Path: /hello.txt
Document Length: 11 bytes
Concurrency Level: 10
Time taken for tests: 0.559 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 1000
Total transferred: 185472 bytes
HTML transferred: 11088 bytes
Requests per second: 1789.60 [#/sec] (mean)
Time per request: 5.588 [ms] (mean)
Time per request: 0.559 [ms] (mean, across all concurrent requests)
Transfer rate: 324.14 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 1
Processing: 0 6 19.7 1 263
Waiting: 0 5 19.0 0 262
Total: 0 6 19.8 1 263
Percentage of the requests served within a certain time (ms)
50% 1
66% 4
75% 4
80% 4
90% 11
95% 22
98% 46
99% 80
100% 263 (longest request)
ちなみに同じマシン(Ubuntu9.10 on CeleronM 1.5GHz 500M)で取ったapache2のベンチ。
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: Apache/2.2.11
Server Hostname: localhost
Server Port: 80
Document Path: /~mattn/hello.txt
Document Length: 11 bytes
Concurrency Level: 10
Time taken for tests: 0.771 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 1000
Total transferred: 464920 bytes
HTML transferred: 11000 bytes
Requests per second: 1297.49 [#/sec] (mean)
Time per request: 7.707 [ms] (mean)
Time per request: 0.771 [ms] (mean, across all concurrent requests)
Transfer rate: 589.09 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.5 0 7
Processing: 6 8 3.3 7 28
Waiting: 6 7 3.1 6 27
Total: 6 8 3.3 7 28
Percentage of the requests served within a certain time (ms)
50% 7
66% 7
75% 7
80% 7
90% 12
95% 16
98% 18
99% 27
100% 28 (longest request)
あまりパフォーマンスは意識してなかった割には、まぁまぁな速度(Request per secondでapache2の1.5倍速くらい?)が出たんじゃないかと思います。ただ機能も小さいですしapacheにも幾らかモジュールが入っちゃってますから参考値にしかすぎませんが...
とりあえずmingw32で不意に落ちる問題が解決したのでこれからパフォーマンスも気にしながらやって行こうかなーと思います。
2009/09/24
apacheでperlを扱うなら、CGI/FCGI/mod_perlといった選択肢があるのですが、mod_perliteという軽量なapacheモジュールも存在します。
sodabrew's mod_perlite at master - GitHub
A lightweight Apache module for Perl scripts
http://github.com/sodabrew/mod_perlite/
mod_perlは結構複雑ですが、mod_perliteは仕組みも簡素で分かりやすく速度パフォーマンスもそこそこあるという代物になっています。
mod_perliteについては
hidekさんの紹介記事を見ていただくと分かるかと思います。実はこのmod_perlite、絶賛の声はあれどPOSTメソッドが動かないという重大なバグを持ったまま今日に至ります。
先日からWindows向けにportingしている最中にその事実に気づき、「ええぃどうせならPOST出来る様にしてしまえ!」とpatchを書いたのが今日の昼。githubでpull requestしたのでauthorの方が今頃吟味検討してmergeするかどうかを判断しておられる頃かと思います。
変更差分を見られたい方は
この辺で...
何個か動作確認を行い、
blosxom、
MTOS(Movable Type Open Source)、
Catalyst(CGI)、
MENTAが動作するのは確認しました。




ただし完璧に動作するという訳でもなく、mod_perliteは内部でperlスクリプトをrun_fileするという簡素な仕様が故に、perl内でシグナルハンドラを書き換えられてしまうと、勝手に終了されてしまうという問題があります。デフォルトだと標準出力されるように$SIG{__DIE__}、$SIG{__WARN__}を書き換えてあるのですが、高性能なWAFはだいたいそのWAF専用のエラー画面を持っていて、内部でシグナルハンドラを上書きしている事があります。この場合、run_fileしてるだけなのでhttpサーバごとダウンするという、とてもおちゃめな動きになります。
とりあえずPOSTが動くようになってWindowsでも動かせる様になったので残る大きな問題は、この$SIG問題だと認識しています。
この辺はおそらくtieを使えばクリア出来るんじゃないかと思ってます。
2009/09/14
今日、子供の自転車の「こま(補助輪)」を取った。
※「こま」って関西オンリーぽい
そろそろこまなしで乗れる様にしなきゃいけないし、本人も望んでいた。
フラフラと倒れそうになりながら、私に支えられながら、なんとか家まで帰り付いた。
家に帰ってから、「そういえば、プログラマって職業でもこういった時期ってあるんだろうか。どんな時期がこまを取る時期なんだろ。」って考えた。
先輩から与えられた仕事だけをコツコツとこなした頃から、自分で自分がやらなきゃいけない仕事を見つけ出せる頃までにあるのか...
はたまた、顧客から言われた仕様を信じて開発していた頃から、顧客とヒアリングして仕様を煮詰められる様になれる頃にあるのか...
別にこまが付いたままでも何処までだって行けるだろうし、速度も出せるだろう。
実際、ウチの子もこまが付いたままでも結構なスピードが出せる。
しかしながら、自分で「もっと上手く乗りたい」って望んだからこそ「こまを取りたい」という気持ちが沸いて来たんだろうな。
コケても泣かないウチの子に、少しだけ成長を垣間見た。
2009/09/07
個人的には一番使っていて無いとちょっと不便に感じる自作のグリモンといえば「
Google Reader Full Feed」なのですが、最近メインのブラウザをGoogle Chromeに変えた事もあり、使えずにちょっぴり不便になってました。
しかしながらGoogle Chromeに移植するとなれば簡単には行かないだろう事が分かっていたので移植するのを躊躇していました。先日、はてなブックマーク数を表示するGoogle Chrome Extensionも作った事だし、少しは知識もついたので、ようやく重い腰をあげて作ってみました。
最初は移植を考えてましたが、結構元にしているLDR FullFeedのコードがまばらになっていてメンテナンス性も悪かったので、今回は元のコードを捨てて1から作り直しました。とはいっても中で使っている部品などはConstellationさんの物や、os0xさんの物を使わせて頂いています。感謝
画面キャプチャは以下みたいな感じです。
操作感はFirefox版とほとんど同じになってます。
今のところ、残課題はSITEINFOをGearsを使ってキャッシュする事くらいだと思ってますが、もしよければ使ってみた感想など頂けると助かります。
ソースは例のごとく、githubに置いてあります。
mattn's chrome-grff at master - GitHub
google reader full feed for chrome
http://github.com/mattn/chrome-grff/tree/master
インストールは以下のリンクから...
chrome-grff.crx
追記
キーは g じゃなく、z ですので、お気をつけて。
この文章は
perl.jonallen.info に掲載されていた記事の和訳です。意訳が含まれている可能性があります。間違いがあればご連絡下さい。
by Jon Allen (JJ) - posted on Wednesday, 26 August 2009
ここ2、3年にわたって、Perlでの開発はCatalystやDBIx::Class、Moose等のエキサイティングな新技術により変わってました。
しかしながら、これらや他のツールに共通して言える事が1つあります - それらはこれらがPerl本体の配布物ではなくCPANの一部という事です。共有ホスティングサーバなど信頼されている環境においては、ユーザはルート権限なしでCPANモジュールをシステムにインストールする事が難しいでしょう。 ただ幸い、単純解があります - それが local::lib です。
local::lib の紹介
local::lib は CPAN ディストリビューションをホームディレクトリににインストールできる様にあらゆる設定を行うPerlモジュールです。これはルート権限が必要ない事を意味していて、システムのPerlや他のユーザに干渉せずに確実にインストール出来る事を意味しています。
もちろん、local::lib それ自信も root 権限なしにインストール出来る為、システム管理者を苦しめる必要性は全くありません。
インストールウォークスルー
クリーンインストールされた Ubuntu 9.04 で local::lib 使用するデモンストレーションを行います。
まず最初にhttp://search.cpan.org/dist/local-libで最新版をダウンロードしデスクトップへ .tar.gz を保存します。
次に、Terminalセッションを開いて、以下のコマンドを入力してアーカイブを解凍します:
cd Desktop
tar -zxf local-lib-1.004004.tar.gz
cd local-lib-1.004004
"bootstrap"にする事を local::lib に教えてあげる必要があります。これでホームディレクトリにperl5というフォルダが作成され local::lib をそこにインストールする様 toolchain に命令します。
perl Makefile.PL --bootstrap
インストールが自動的に構成される様に一度か二度尋ねられます。デフォルト値('yes')で良いのでただ単にEnterキーを叩きましょう。
設定が終了したら以下のコマンドを入力し local::lib をビルド、テスト、インストールして下さい:
make
make test
make install
もうちょっとです。残りは1工程です。インストールされたモジュールがどこに保存されるかを Perl に教えてあげる為に幾つかの環境変数を設定します。
echo 'eval $(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)' >>~/.bashrc
全て完了です。実行していたターミナルウィンドウを全て閉じて、新しい設定が反映される様に、新しいTerminalセッションを開きましょう。
local::lib のテスト
ちゃんと動くか簡単なモジュールをインストールしてみましょう。新しいターミナルウィンドウを開き、CPAN シェルを起動します:
perl -MCPAN -eshell
CPAN プロンプトが開き、以下を入力します
install Acme::Time::Baby
モジュールをダウンロードしてビルド、テスト、インストールします - マニュアルを見ることもないでしょう。exit をタイプして CPAN シェルから抜けたら簡単なワンライナーでインストールされたモジュールをテスト出来ます:
perl -MAcme::Time::Baby -E 'say babytime'
CPAN を恐れるな
さて動作確認です。Catalyst をインストールしましょう。
Catalyst は多くのモジュールに依存した巨大なモジュールですが CPAN.pm でセットアップするので問題はありません。しかしながら初期状態では、CPAN シェルが依存を見付ける度にインストールすべきかどうか尋ねて来て、とても退屈になります。最初ですから自動的に依存に従う様 CPAN.pm に教えてあげます。
perl -MCPAN -eshell
o conf prerequisites_policy follow
o conf build_requires_install_policy yes
o conf commit
注意 それでも時折、モジュールがそれら自身の構成に依存した質問をして来るので何が起きるか目を光らせておくべきでしょう。
インストールを開始する準備が整いました。
install Catalyst::Devel
CPAN.pm が全ての Catalyst 依存モジュールをビルドしてくれます。少々時間が経って、以下のメッセージが出ていればインストール成功です:
FLORA/Catalyst-Devel-1.19.tar.gz
/usr/bin/make install -- OK
ちゃんと動作しているかを検証するので CPAN シェルを終了して新しい Catalyst アプリケーションを作成しましょう。
~/perl5/bin/catalyst.pl MyApp
cd MyApp
perl Makefile.PL
注意 catalyst.pl スクリプトは ~/perl5/bin ディレクトリにあります - ここが local::lib の標準的なスクリプトインストール先です。
Catalystアプリケーションはスタンドアロンの開発用サーバを含んでいるので、簡単にアプリケーションを実行できます:
script/myapp_server.pl -r -d
これで http://localhost:3000をブラウズすれば、光り輝くCatalystのテストページが表示されます!
Notes
-
いくつかの Perl モジュールは速度を向上やC言語のライブラリ関数と強調する為にC言語で書かれています。CPANの全てを使用するにはCコンパイラをマシンにインストールする必要があります。Linuxや他のUNIXシステムで大概Cコンパイラが既にインストールされていますが(which ccをタイプしてみましょう)、そうでない場合でもベンダは GCC パッケージを提供しているはずです。Macでは Xcode developer tools (OS XをDVDからインストール)をインストールすると必要な物は全て揃うでしょう。
-
私がテストで使用しているシステム Ubuntu 9.04 は Perl 5 version 10.0 を含んでいます。以下のコマンドで Perl がどのバージョンかをチェックできます。
perl -v
もし Perl 5 のリリースよりも古い物、すなわち v5.8.9 以前のバージョンをお使いであれば、システム管理者かOSベンダーにアップグレードを求めるべきしょう。 Perl5 version 10.0 では smart matching、named regex captures、a switch statement や state variables といった多くの新機能が導入されています - http://perldoc.perl.org/perl5100delta.html を参照してと全ての新機能を確認して下さい。
-
インストール済みのモジュールを全て削除してやり直すのであれば、ローカルの ~/.cpan と ~/perl5 ディレクトリを削除するだけです:
rm -rf ~/.cpan
rm -rf ~/perl5
そして ~/.bashrc を編集し以下の行を削除します:
eval $(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)
以上に示した手順で local::lib がインストール出来ます。
2009/09/03
こんな拡張が欲しい人なんて半ば病気ですよ。
夜になるとエアコン無しに過ごせる涼しい季節になって来ました。皆さん如何お過ごしでしょうか。
最近Google Chromeを使っているのですが、
AutoPagerizeのGoogle Chrome拡張を入れてみて感動し、被はてなブックマーク数を画面下に表示する拡張が欲しくなったので、つい勢いで作ってしまいました。
はいはい。病気病気
スクリーンショットはこんな感じ
os0xさんの記事をふんだんに参考にしながらなんとか作り上げました(os0x++)。
以下その作成手順。
まず、manifest.jsonを用意しました。
manifest.json
{
"name": "hatena bookmark counter",
"description": "hatena bookmark counter for google chrome. hatena bookmark is social bookmark web service. see 'http://b.hatena.ne.jp/'. this extension show image of numbers which how many users bookmarked the URL.",
"version": "0.0.1",
"icons": { "normal": "hbcount.gif" },
"permissions": [ "tabs" ],
"toolstrips": [ "hbcount.html" ],
"content_scripts": [
{
"js": [ "hbcount.js" ],
"matches": [ "http://*/*" ]
}
]
}
今回の拡張は、ページがロードされた瞬間に発動するjavascriptと、Google Chrome起動中に画面下で常駐するためのtoolstripsと呼ばれるHTMLコンテンツを使いました。
まずユーザスクリプト側。
hbcount.js
if (window == top) {
var port = chrome.extension.connect();
port.onMessage.addListener(function(data) {
location.href = data.url;
});
port.postMessage({url: location.href});
}
ユーザスクリプト側からはtoolstripのコントロールに対してアクセス出来ませんのでpostMessageでメッセージを送信し被はてなブックマーク数アイコンを更新します。なおtoolstripにあるアイコンをクリックすると、はてなブックマークエントリページを開く様になっているのですが、toolstrip側から現在開いているタブのコンテンツにはアクセスする事も出来たのですが、少し危険を感じたのでpostMessageを使いtoolstrip側からユーザスクリプト側にURLを返信してlocation.hrefでの画面遷移をさせています。
真ん中のイベント待ちはその為の物です。なお、topかどうかを確認しているのは現在のURL以外でもこの拡張が走ってしまうのを防止している小細工です。例えばiframeなんかでアフィが表示されているとそれに対してもこの拡張が実行されてしまいます。
次に本体であるtoolstripのソースは以下の通り。
hbcount.html
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<script type="text/javascript">
window.onload = function() {
var hbcount = document.getElementById("hbcount");
chrome.extension.onConnect.addListener(function(port) {
function update(url) {
hbcount.src = 'hbcount.gif';
hbcount.onclick = null;
if (url.match(/^chrome:\/\//)) return;
url = url.replace(/#/g, '%23');
hbcount.src = 'http://b.hatena.ne.jp/entry/image/' + url;
hbcount.onclick = function() { port.postMessage({'url': 'http://b.hatena.ne.jp/entry/' + url}); }
}
port.onMessage.addListener(function(data) { update(data.url); });
chrome.tabs.onSelectionChanged.addListener(function(id, props) {
chrome.tabs.get(id, function(tab) { update(tab.url); });
});
});
};
</script>
<img src="hbcount.gif" id="hbcount" style="cursor: pointer;" title="はてなブックマーク"/>
ユーザスクリプトからメッセージを受け取りupdate関数にて更新します。アイコンクリック時のメッセージ返信を行う処理もここにあります。
その下にある処理ではタブが切り替わったタイミングで現在の被ブックマーク数を更新する処理になります。この場合、いくらmanifest.jsonでhttp://のみと宣言していてもタブに対するイベント登録を行うのでchrome://といったURLも対象になってしまいます。update関数でURLを避けているのはその為です。
さて、ここから話がそれ始めます。
Google Chromeの拡張はchromeブラウザ自身でパッキングする事が出来ます。ただchromeへのパスをいちいち書いてられないのでMakefileを書きました。
Makefile
ifndef CHROME
ifneq ($(windir),)
DEST = "$(shell pwd)\chrome-hbcount"
CHROME = "$(USERPROFILE)/Local Settings/Application Data/Google/Chrome/Application/chrome.exe"
else
DEST = $(shell pwd)/chrome-hbcount
CHROME = $(shell which crxmake)
ifeq ($(CHROME),)
CHROME = $(shell which google-chrome)
endif
ifeq ($(CHROME),)
CHROME = $(shell which chromium-browser)
endif
ifeq ($(CHROME),)
CHROME = chrome
endif
endif
endif
SRCS = hbcount.gif hbcount.html hbcount.js manifest.json
all : chrome-hbcount.crx
first : $(SRCS)
@-rm -r $(DEST)
@mkdir $(DEST)
@cp $(SRCS) $(DEST)/.
$(CHROME) --pack-extension=$(DEST)
chrome-hbcount.crx : $(SRCS)
@-rm -r $(DEST)
@mkdir $(DEST)
@cp $(SRCS) $(DEST)/.
$(CHROME) --pack-extension=$(DEST) --pack-extension-key=chrome-hbcount.pem
clean:
@-rm *.crx
@-rm -r $(DEST)
初回だけmake first、以降はmakeでビルドです。
WindowsでもLinuxでもMacでも使える様に、かつgoogle-chromeが入っていない場合にはchromium-browserを使う様になっています。ちなみにcrxmakeというのは
Constellationさんが書いたchrome拡張のパッケージングツールです(Constellation++)。chromeを起動する事なくパッケージング出来ます。なおchrome-hbcountという部分を書き換えれば、他の拡張でも使えるかもしれません。
さらに拡張を書いていると拡張のバージョンがちょくちょくあがります。
Google Chrome(chromium-browser)の拡張にはupdate_urlという設定項目があり、決まった形のXMLファイルを用意しておけば自動アップデート出来る様になる予定があるそうなのですが、拡張を書き換える度にいちいちupdate.xmlを書き換えてアップロードするも面倒くさそうだったので、手順を自動化出来る様にしてみました。簡単には
- manifest.jsonからバージョンを抜き出す
- 抜き出したバージョンでupdate.xmlを更新する
- githubのダウンロードページにあるファイルを一旦削除する
- 拡張モジュール(crx)とupdate.xmlをアップロードする
という一連の流れをPerlスクリプトにしてみました。
dist-upload.pl
#!/usr/bin/perl
use strict;
use warnings;
use JSON;
use Perl6::Slurp;
use WWW::Mechanize;
use Net::GitHub::Upload;
use Config::Pit;
my $config = pit_get('github-upload', require => {
'login' => 'your login id on github.com',
'password' => 'your password on github.com',
});
my $manifest = from_json(slurp 'manifest.json');
my $id = $manifest->{id};
my $version = $manifest->{version};
open my $fh, '>update.xml';
print $fh <<EOF;
<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
<app appid="$id">
<updatecheck codebase="http://cloud.github.com/downloads/mattn/chrome-hbcount/chrome-hbcount.crx" version="$version" />
</app>
</gupdate>
EOF
close $fh;
my $mech = WWW::Mechanize->new;
$mech->get('https://github.com/login');
$mech->submit_form(
form_number => 2,
fields => {
login => $config->{login},
password => $config->{password},
});
$mech->get('http://github.com/mattn/chrome-hbcount/downloads');
for my $form (@{$mech->forms}) {
if ($form->action =~ /^http:\/\/github.com\/mattn\/chrome-hbcount\/downloads\//) {
print "deleting ".$form->action."\n";
$mech->request($form->click);
}
}
chomp(my $user = `git config github.user`);
chomp(my $token = `git config github.token`);
my $gh = Net::GitHub::Upload->new(
login => $user,
token => $token,
);
print "uploading chrome-hbcount.crx\n";
$gh->upload( repos => 'mattn/chrome-hbcount', file => 'chrome-hbcount.crx' );
print "uploading update.xml\n";
$gh->upload( repos => 'mattn/chrome-hbcount', file => 'update.xml' );
こちらもmattnとかchrome-hbcountという部分を書き換えれば、他の拡張で使えるかもしれません。ちなみにNet::GitHub::Uploadはtypesterさんのモジュールです(typester++)。
これで、あとは
- 拡張を書き換える
- manifest.jsonのバージョンを上げる
- make
- dist-upload.pl
という単純な手順で皆様に最新の拡張を自動ダウンロードして頂ける...予定です。まだupdate.xmlを使った自動アップデート機能は動いてないらしいです。
さて、はてなブックマークカウンタ拡張に話を戻して...
コードおよび拡張ファイルはgithubに置いてあります。Downloadのページからでもダウンロード出来ますが、以下のリンクでインストール出来る様になっています。
hatena bookmark counter for google chrome
リポジトリは以下のリンク先にあります。
mattn's chrome-hbcount at master - GitHu
hatena bookmark counter for google chrome
http://github.com/mattn/chrome-hbcount/tree/master
ライセンスはBSDにしましたので、どうぞお好きに使って下さい。流用してdelicious拡張なんてのもいいですね。
2009/09/02
以前、
Growl For Windowsの
紹介記事で
gntp-sendという単純なCのプログラム(パスワードハッシュのみサポート)を書きました。その後、GNTPプロトコルの仕様に合わせパスワードハッシュ以外にもAES/DES/3DESな暗号化通信もサポートしたPerlモジュール、
Growl::GNTPを書きました。
C/C++言語から暗号モジュールを扱う上でcryptライブラリはライセンス制約がありますが、public domainな
Crypto++を使えばな制約もなくなります。
久々boostを触ってみようとリハビリがてらboost::asioとcrypto++を使って、暗号化通信をサポートしたGNTP Growlプログラムを書いてみた。
crypto++には、ハッシュアルゴリズムや暗号アルゴリズムがごった煮で含まれており、フィルタとして使ったりCBCモードで使ったりと、かなり便利になっています。
例えば乱数を得たいのであれば
CryptoPP::SecByteBlock salt(8);
CryptoPP::AutoSeededRandomPool rng;
rng.GenerateBlock(salt.begin(), salt.size());
こんな感じに、またMD5 Digestのhex文字列を取得したいのであれば
CryptoPP::SecByteBlock passtext(CryptoPP::Weak1::MD5::DIGESTSIZE);
CryptoPP::Weak1::MD5 hash;
hash.Update((byte*)password_.c_str(), password_.size());
hash.Update(salt.begin(), salt.size());
hash.Final(passtext);
CryptoPP::SecByteBlock digest(CryptoPP::Weak1::MD5::DIGESTSIZE);
hash.CalculateDigest(digest.begin(), passtext.begin(), passtext.size());
といった感じに。さらにCBCモードで文字列を暗号化したいならば
CryptoPP::CBC_Mode<CryptoPP::DES>::Encryption
encryptor(passtext.begin(), iv.size(), iv.begin());
std::string cipher_text;
CryptoPP::StringSource(text, true,
new CryptoPP::StreamTransformationFilter(encryptor,
new CryptoPP::StringSink(cipher_text)
)
);
こんな風に書くことも出来る。これだけそろえば出来たも同前。
boost::asioの便利なiostreamを使って
asio::ip::tcp::iostream sock(hostname, port);
sock << "こんにちわこんにちわ!";
綺麗な書き方も出来る。ちなみにboost::asioを使えばWindowsやUNIX上での特有なコードが現れる事がないので、同じソースでWindowsやLinuxでもコンパイル出来てしまうので非常にありがたいですね。
以下、全体のソースです。ヘッダファイルになってます。
#ifndef gntp_h
#define gntp_h
#define CRYPTOPP_ENABLE_NAMESPACE_WEAK 1
#include <sstream>
#include <iostream>
#include <string>
#include <cryptopp/osrng.h>
#include <cryptopp/files.h>
#include <cryptopp/hex.h>
#include <cryptopp/md5.h>
#include <cryptopp/des.h>
#include <cryptopp/aes.h>
#include <cryptopp/filters.h>
#include <cryptopp/modes.h>
#include <asio.hpp>
class gntp {
private:
static inline std::string to_hex(CryptoPP::SecByteBlock& in) {
std::string out;
CryptoPP::HexEncoder hex( NULL, true, 2, "" );
hex.Attach(new CryptoPP::StringSink(out));
hex.PutMessageEnd(in.begin(), in.size());
return out;
}
void send(const char* method, std::stringstream& stm) {
asio::ip::tcp::iostream sock(hostname_, port_);
if (!sock) return;
CryptoPP::SecByteBlock salt(8), iv(8);
rng.GenerateBlock(salt.begin(), salt.size());
rng.GenerateBlock(iv.begin(), iv.size());
if (!password_.empty()) {
CryptoPP::SecByteBlock passtext(CryptoPP::Weak1::MD5::DIGESTSIZE);
CryptoPP::Weak1::MD5 hash;
hash.Update((byte*)password_.c_str(), password_.size());
hash.Update(salt.begin(), salt.size());
hash.Final(passtext);
CryptoPP::SecByteBlock digest(CryptoPP::Weak1::MD5::DIGESTSIZE);
hash.CalculateDigest(digest.begin(), passtext.begin(), passtext.size());
CryptoPP::CBC_Mode<CryptoPP::DES>::Encryption
encryptor(passtext.begin(), iv.size(), iv.begin());
std::string cipher_text;
CryptoPP::StringSource(stm.str(), true,
new CryptoPP::StreamTransformationFilter(encryptor,
new CryptoPP::StringSink(cipher_text)
)
);
sock << "GNTP/1.0 "
<< method
<< " DES:" << to_hex(iv)
<< " MD5:" << to_hex(digest) << "." << to_hex(salt)
<< "\r\n"
<< cipher_text << "\r\n\r\n";
} else {
sock << "GNTP/1.0 "
<< method
<< " NONE\r\n"
<< stm.str() << "\r\n";
}
while (1) {
std::string line;
if (!std::getline(sock, line)) {
break;
}
if (line.find("GNTP/1.0 -ERROR") == 0)
throw "failed to register notification";
if (line == "\r") break;
}
}
std::string application_;
std::string hostname_;
std::string port_;
std::string password_;
CryptoPP::AutoSeededRandomPool rng;
public:
gntp(std::string application = "gntp-send", std::string password = "",
std::string hostname = "localhost", std::string port = "23053") :
application_(application),
password_(password),
hostname_(hostname),
port_(port) { }
void regist(const char* name) {
std::stringstream stm;
stm << "Application-Name: " << application_ << "\r\n";
stm << "Notifications-Count: 1\r\n";
stm << "\r\n";
stm << "Notification-Name: " << name << "\r\n";
stm << "Notification-Display-Name: " << name << "\r\n";
stm << "Notification-Enabled: True\r\n";
stm << "\r\n";
send("REGISTER", stm);
}
void notify(const char* name, const char* title, const char* text, const char* icon = NULL) {
std::stringstream stm;
stm << "Application-Name: " << application_ << "\r\n";
stm << "Notification-Name: " << name << "\r\n";
if (icon) stm << "Notification-Icon: " << icon << "\r\n";
stm << "Notification-Title: " << title << "\r\n";
stm << "Notification-Text: " << text << "\r\n";
stm << "\r\n";
send("NOTIFY", stm);
}
};
#endif
使い方は非常に簡単。
#include "gntp.h"
int main(void) {
gntp client("my growl application", "my-password-is-secret");
client.regist("my-event");
client.notify("my-event", "タイトル", "本文", "http://mattn.kaoriya.net/images/logo.png");
return 0;
}
これだけで暗号化通信をサポートしたGNTP Growl通信が出来てしまいます。ちなみにDESな部分をAESに変えればそのままAES暗号通信する事が出来ます。CBC素晴らしい。
本当はboost 1.40.0に含まれる新しい機能を触るつもりだったのですが、まったく触れてもいませんね...苦笑
追記
githubにソースあげておいた。
mattn's gntppp at master - GitHub
GNTP++: gntp client library writen in C++
http://github.com/mattn/gntppp/tree/master
2009/08/26
vimを使っていて人のスクリプトの一部が気に入らない場合、直接書き換える事もするのですが、最近はGLVS(GetLatestVimScripts)を使う事の方が多く、せっかく書き換えたスクリプトを新しいアップデートで上書きされたりして悲しい事になったりします。書き換えて違うファイル名で保存する...なんてのも方法かもしれませんが、いっそvimscriptの中のある関数だけ書き換えられればいいんじゃないか...と思って書き換える方法を考えてみました。
まず、vimにはグローバルスコープ、スクリプトスコープ、ローカルスコープとあり、スクリプトスコープとは1スクリプトファイル内で実行される関数と変数群に位置します。
通常、スクリプトスコープ内の関数には<SID>というスクリプトID識別が付与され、ファイル単位でユニークに格納されています。
ただ実際には識別子が付いているだけで、外部から参照も出来ますしfunction()で関数リファレンスを取る事も出来ます。スクリプトファイルに対する<SID>は
:scriptnames
で一覧出来ますから、この出力をredirで盗み取ってしまえばいいのです。
ちなみにウチのubuntuで:scriptnamesを実行した結果は以下の通り。
1: /usr/share/vim/vimrc
2: /usr/share/vim/vim72/debian.vim
3: /usr/share/vim/vim72/syntax/syntax.vim
4: /usr/share/vim/vim72/syntax/synload.vim
5: /usr/share/vim/vim72/syntax/syncolor.vim
6: /usr/share/vim/vim72/filetype.vim
7: /home/mattn/.vimrc
8: /usr/share/vim/vim72/syntax/nosyntax.vim
9: /usr/share/vim/vim72/ftplugin.vim
10: /usr/share/vim/vim72/indent.vim
11: /home/mattn/.vim/plugin/autodate.vim
12: /home/mattn/.vim/plugin/calendar.vim
13: /home/mattn/.vim/plugin/codepad.vim
...
この左数字部分がIDです。これをsplitとmapを使い、スクリプトファイル名から<SID>を得られるハッシュマップ(vim語ではDictionary)に変換します。
function! GetScriptID(fname)
let snlist = ''
redir => snlist
silent! scriptnames
redir END
let smap = {}
let mx = '^\s*\(\d\+\):\s*\(.*\)$'
for line in split(snlist, "\n")
let smap[tolower(substitute(line, mx, '\2', ''))] = substitute(line, mx, '\1', '')
endfor
return smap[tolower(a:fname)]
endfunction
ちなみに、tolowerしてるのはほぼWindows用で、ちょっとした大文字小文字の違いで<SID>が取れなくなって悲しくならない為のおまじないです。
次に書き換えるべき関数リファレンスを取得します。<SID>内で定義される関数リファレンスは
<SNR>SID_foo
という識別になります。これをfunction()関数に渡してあげれば関数リファレンスが取得出来ます。
これだけでも、実はスクリプトスコープ内の関数を外部から呼び出せてウマーなのですが、ここから本題。
関数を書き換えるにはfunction!と「!」付きで宣言すれば良いのですが引数が決められません。書き換え関数の引数を外部から指定して貰っても良いのですが面倒ですよね。書き換え前の関数引数と書き換え後の関数引数が同じである事は書き換えた側の責任でもありますし、call()関数を使えば引数を配列として呼び出す事も出来ます。そこで以下の様なトリックを使いました。
function! funcA(...)
return call('funcB', a:000)
endfunction
a:000とは可変個引数を宣言した際に使える引数リストです。受け取った引数リストをcall()関数に渡しています。後はこれを動的に実行してやれば書き換え関数の完成です。全体のコードだと以下の様になりました。
function! GetScriptID(fname)
let snlist = ''
redir => snlist
silent! scriptnames
redir END
let smap = {}
let mx = '^\s*\(\d\+\):\s*\(.*\)$'
for line in split(snlist, "\n")
let smap[tolower(substitute(line, mx, '\2', ''))] = substitute(line, mx, '\1', '')
endfor
return smap[tolower(a:fname)]
endfunction
function! GetFunc(fname, funcname)
let sid = GetScriptID(a:fname)
return function("<SNR>".sid."_".a:funcname)
endfunction
function! HookFunc(funcA, funcB)
if type(a:funcA) == 2
let funcA = substitute(string(a:funcA), "^function('\\(.*\\)')$", '\1', '')
else
let funcA = a:funcA
endif
if type(a:funcB) == 2
let funcB = substitute(string(a:funcB), "^function('\\(.*\\)')$", '\1', '')
else
let funcB = a:funcB
endif
let oldfunc = ''
redir => oldfunc
silent! exec "function ".funcA
redir END
let g:hoge = oldfunc
exec "function! ".funcA."(...)\nreturn call('" . funcB . "', a:000)\nendfunction"
endfunction
さて、このスクリプトの使い方。例えば引数で与えられた文字列XXXを使い「Hello: XXX」を表示するスクリプト内関数s:funcAがあったとして、これを「GoodNight: XXX」と表示するスクリプト内関数s:funcBに書き換えたいとします。それぞれ関数リファレンスを得て書き換えたいタイミングでHookFuncを呼べば、それ以降は書き換えられた関数が実行されます。
so hookfunc.vim
function! s:foo(text)
echo "Hello, " . a:text
endfunction
function! s:bar(text)
echo "GoodNight, " . a:text
endfunction
call s:foo("World")
call s:bar("World")
call HookFunc(GetFunc(expand("%:p"), "foo"), GetFunc(expand("%:p"), "bar"))
call s:foo("World")
ちなみにfuncAとfuncBは違うスクリプトファイルに宣言されていても問題ありません。
イメージはコード内のコメントで分かって頂けると思います。時と場合によっては使えそうな機能ですね。
まぁ、ほとんど使い道ないでしょうが...苦笑
2009/08/25
ネタ的にはZIGOROuさんかhasegawaさんのネタっぽいが...
@if(0)==(0) ECHO OFF
CScript.exe
GOTO :EOF
@end
function wsock_ConnectionRequest(reqId) {
if (socket.State != 0) socket.Close();
socket.Accept(reqId);
}
function wsock_DataArrival(bytesTotal) {
var data = script.Run('GetData', socket, bytesTotal);
socket.SendData([
"HTTP/1.1 200 OK",
"Connection: closed",
"Content-Type: text/html;",
"",
"Hello World! " + new Date(),
""
].join("\n"));
WScript.Sleep(1000);
socket.Close();
socket.Listen();
}
var socket = WScript.CreateObject('MSWinsock.Winsock', 'wsock_');
var script = WScript.CreateObject('ScriptControl');
script.language = 'VBScript';
script.AddObject('WScript', WScript);
script.AddCode([
'Function GetData(socket, bytesTotal):',
' Dim data:',
' socket.GetData data, vbString, bytesTotal:',
' GetData = data:',
'End Function'
].join(''));
socket.Bind(8080);
socket.Listen();
while (socket.State != 9) {
WScript.Sleep(100);
}
GetDataがByRefなので、ScriptControlを使ってます。
2009/08/24
すぎゃーんさんのirssi全裸プラグインをベースに、
kazuhoさんのpicojsonを使い、
freenodeのwebchat機能をAPI的に使って、全裸発言リプライするIRC botを作ってみた。
コンパイルは以下の通り
# g++ zenra-bot.cc -lmecab -lcurl
Windows(mingw32)でも動作します。
実行は第一引数にnick名、第二引数にチャネル名を指定します。
# ./zenra-bot zenra-bot #zenra-room
常駐している間は、誰かの発言をZENRIZE(全裸的変換)してつぶやきます。
例えば
会社に行く
と誰かが発言すれば
全裸で会社に行く
とつぶやいてくれます。
とても有用ですね!
よろしければどうぞ。
2009/08/21
テストも兼ねて...
追記
これだけだとなんなので...。
RSSもしくはAtomにrel="hub" href="http://pubsubhubbub.appspot.com"のlink要素を追加し、このpluginのhub_urlにRSSもしくはAtomのURLを指定すれば動きます。
2009/08/19
だいたいに10分くらいだと思う。
まずtinytinyhttpd(tthttpd)とmercurialは既に入っている事として
1. リポジトリを作る
# cd /path/to/repo
# hg init
2. hgwebdir.cgiを用意する
mercurialに含まれるhgwebdir.cgiを配置する。
# cd /path/to/web
# cp /path/to/hgwebdir.cgi .
mercurialのソースパッケージに入ってる
3. hgweb.configを用意する
hgwebdir.cgi用の設定ファイルを書く。
[paths]
repo = /path/to/repo
4. hgrcを作る
.hg/hgrcを作る(もしくは修正する)。
[web]
name = repo
description = my private repo
contact = mattn@example.com
push_ssl = false
allow_push = mattn iratqq hasegawa
allow_pull = *
allow_read = *
allow_archive = bz2, gz, zip
encoding = utf-8
5. パスワードファイルを書く
mattn:ggrks
iratqq:kkrkr
hasegawa:xss
プレインテキストなので要注意
6. tthttpdの設定ファイルを書く
Windowsならば
[global]
port=8080
root=c:/path/to/repo
indexpages=hgwebdir.cgi
[mime/types]
cgi=@c:/python26/python.exe
[authentication]
/=POST,Admin!,c:/path/to/passwd
Unixならば
[global]
port=8080
root=/path/to/repo
indexpages=hgwebdir.cgi
spawn_executable=on
[authentication]
/=POST,Admin!,/path/to/passwd
こんな感じ。
7. tthttpdを起動する
設定ファイルを指定して起動
# tthttpd -vvv -c repo.conf
うごいた。
8. clone/pushしてみる
# hg clone http://localhost:8080/hgwebdir.cgi/repo
destination directory: repo
no changes found
updating working directory
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
# cat > README
嫁
^D
# hg add README
# hg commit -m "first import"
# hg push
pushing to http://localhost:8080/hgwebdir.cgi/repo
searching for changes
http authorization required
realm: Admin!
user: mattn
password:
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
うごいた。
最新版のtinytinyhttpdでしか動きません。
tthttpd.exeを含んだリポジトリフォルダごと持って歩けば、どこでもmercurialサーバになるね。
2009/08/18
色々と修正を施して、sinatraのdispatch.cgiも動くようになっています。またBASIC認証をサポートしましたので、tDiaryのupdate.rbだけに認証をかけられる様になっています。実際には、ターゲットURL、メソッド(GET/POSTなど)、ユーザ・パスワード一覧という設定が可能になったので、複数ユーザを扱う事も出来ます。
ただ現状、プレインテキストのみ対応ですので、これについては今後なんとかして行きたいと思っています。
本当ならば.htaccessを読み込ませたいのですが、.htaccessはcrypt/md5を使って暗号化されており、libcryptを使うとなれば依存が増えると同時に、ライセンス的にもグレーになってきますので、少し悩んでいます。どこかにPublic Domainで書かれたlibcrypt知りませんか?
2009/08/17
ずいぶん前からXMLRPCをC++で扱うのに小さいライブラリないかなーと思ってました。
書いたといっても結構前からあったのですが、いらん所を削ぎ落として簡素なXMLRPCライブラリとして仕立て上げました。
簡素とは言えど、一般的な物ならば色々動きます。
例えば、ブログに一気に、しかも機械的にポストしたい場合とか
おれは今すぐXMLRPCしたいんだ!そしてどうしてもC++でXMLRPCしたいんだー!
って事ないですか?MovablebTypeをインストールしたけど、XMLRPCするのにC++コンパイラは入ってるけど、PerlやRubyやPythonは入ってない...とか悲しすぎます。
今回紹介する"tinyxmlrpc"はそんな、小さい様で大きな問題を解決出来るかもしれないライブラリです。
mattn's tinyxmlrpc at master - GitHub
tiny xmlrpc library written in C++
http://github.com/mattn/tinyxmlrpc/tree/master
今githubが不調でpushが反映されていません。リポジトリからのcloneは出来る様です。
ソースはC++で書いてます。WindowsとUNIX(Linuxでだけ動作確認)で動作します。
機能としては、STLのvectorとかmapとかに親和性高くなる様になっています。内部ではlibxmlでパースしていてcurlで通信しています。
小ささをウリにしていますが、libxmlとcurlのリンクは必須です。
サンプルコードを以下に示します。
#include "tinyxmlrpc.h"
#include <iostream>
class vargs {
private:
tinyxmlrpc::value::Array args_;
public:
vargs& operator <<(tinyxmlrpc::value arg) {
args_.push_back(arg);
return *this;
}
tinyxmlrpc::value::Array& list() {
return this->args_;
}
vargs& first() {
this->args_.clear();
return *this;
}
};
int main(int argc, char* argv[]) {
#ifdef _WIN32
WSAData wsadata;
WSAStartup(MAKEWORD(2,0), &wsadata);
#endif
if (argc != 4) return -1;
std::string endpoint = argv[1];
std::string user = argv[2];
std::string pass = argv[3];
try {
tinyxmlrpc::value::Array req;
tinyxmlrpc::value res;
tinyxmlrpc::value::Struct entry;
vargs args;
res = tinyxmlrpc::call(endpoint,
"metaWeblog.getRecentPosts",
(args.first() << "1" << user << pass << 3 << true).list());
if (!tinyxmlrpc::failed(res)) {
for(int n = 0; n < res.size(); n++) {
std::vector<std::string> members = res[n].listMembers();
std::vector<std::string>::const_iterator it;
std::cout << "{" << std::endl;
for(it = members.begin(); it != members.end(); it++) {
std::string val = res[n][*it].to_str();
std::cout << " " << it->c_str() << "=" << val.c_str() << std::endl;
}
std::cout << "}" << std::endl;
}
} else {
std::cerr << res << std::endl;
}
entry["title"] = "hasegawaにいじめられた";
entry["description"] = "今日は、wassrでhasegawaにいじめられた。悲しかった。";
entry["dateCreated"] = "";
res = tinyxmlrpc::call(
endpoint,
"metaWeblog.newPost",
(args.first() << "1" << user << pass << entry << 1).list());
if (!tinyxmlrpc::failed(res)) {
std::cout << "result:" << res << std::endl;
} else
std::cerr << res << std::endl;
} catch(tinyxmlrpc::value::Exception& e) {
std::cerr << e.message << std::endl;
}
return 0;
}
これを使ってコマンドラインから引数として、
MetaWeblogをサポートしているブログのエンドポイントURL、
ユーザ、
パスワードを指定すると、「hasegawaにいじめられた」というエントリがポストされます。
タイトルや本文に、特に意味はありません。すごいですね!
ライセンスはまだ決めてませんが、今のところBSDを予定しています。使ってみて下さい。
コードはgithubにあるので、patchウェルカムです。
今後は、この記事の為だけに仕上げた疑いのある汚いコードを綺麗にしていくつもりです。
参考文献:
C++で軽量Webサーバ書いた。
ReverseHttp面白いですね。
ReverseHttp
Tunnel HTTP over HTTP, in a structured, controllable, securable way. Let programs claim part of URL space, and serve HTTP, all by using an ordinary HTTP client library.
http://www.reversehttp.net/
ただ勘違いされやすいのが「何がReverseなの」という部分。通常ブラウザからリクエストが送信され、それに対する応答がサーバから返されます。ReverseHttpはサーバで何かアクションが起きた場合に、ブラウザ側がその通知を受信する...なんて事が出来るプロトコルです。仕組みはcometというlong pollに似た仕組みで、サイトのdemoを観るとなんなく理解出来るかと思います。
例えば何が出来るのか...
ローカルPC内で動作するファイアウォール内のwebアプリを外部に公開する
rubyにhookoutというライブラリがあり、これを使用するとrackアプリがさも外部に公開されているかの様に振舞う事が出来ます。
paulj's hookout at master - GitHub
Expose Ruby applications to the web via ReverseHTTP
http://github.com/paulj/hookout/tree/master
グローバルIPが無くても、webアプリを公開出来るなんて素晴らしい!
なお、ReverseHttpはプロトコルですのでhookout以外にも同様のソフトウェアはあります。例えばmiyagawaさんが書いたAnyEvent::ReverseHttpに含まれるeg/proxy.plを使うとローカルPC内のwebアプリを外部に公開する事が出来ます。
外部で起きたアクションをローカルPCに通知させる
例えば、はてなブックマークで自分のサイトがブックマークされた瞬間にデスクトップPCが反応したらどうしますか?
スターを付けに行きませんか(笑)?ReverseHttpを使えば出来るのです。
今日はこの「はてなブックマーク通知」をやってみたいと思います。
使う材料は以下の通り。
- hookout : 上記で紹介したrackアプリを公開するライブラリ
- sinatra : ruby製webアプリケーションフレームワーク
- ruby_gntp : snakaさん作のruby用Growl For Windowsインタフェース
こんだけ。
上記のサイトからhookoutを取得してインストールし、以下のsinatraアプリケーションを作成します。
my-hatebu-growler.rb
require 'rubygems'
require 'sinatra'
require 'ruby_gntp'
growl = GNTP.new
growl.register({
:app_name => "はてブ",
:notifies => [{
:name => "hatenabookmark",
:enabled => true,
}]
})
post '/' do
return "ng" if params[:status] !~ /add|update/
user = params[:username]
text = "#{params[:comment]}\r\r#{params[:title]}\r#{params[:url]}"
icon = "http://www.hatena.ne.jp/users/#{user[0,2]}/#{user}/profile.gif"
p params[:status]
growl.notify({
:name => "hatenabookmark",
:title => user,
:text => text,
:icon => icon,
})
'ok'
end
config.ru
require 'my-hatebu-growler'
set :run, false
run Sinatra::Application
これを以下の様に起動します。
hookout -a http:/www.reversehttp.net/reversehttp -n my-hatebu-growler-application -R config.ru start
my-hatebu-growler-applicationの部分は適当な物に変えて下さい。
起動すると以下の様に出力されます。
Bound to location http://my-hatebu-growler-application.www.reversehttp.net/
このURLを、はてなブックマークにwebhook登録します。
あとは、じっとブクマされるのを待ちます。
デタ━━━゚(∀)゚━━━!!
秋の夜長に、こんなツールお一つどうでしょうか。
追記1
HTTP::Engine::Interface::ReverseHTTPもあるよとmiyagwawaさんに教えてもらいました。
hookout for HTTP::Engineらしいです。
追記2
例では分かり易くする為にwebhook APIのキー認証を省いていますが、本当はちゃんと判定する必要があります。
追記3
PerlでHTTP::Engine::Interface::ReverseHTTPを使ってみた。ネットワークGrowlにはアイコンが使える仕組みがないのが残念。
2009/08/16
furyu-teiさんの記事でXSLTの場合はendpointを変えないといけない事が分かった。
AmazonのProduct Advertising API認証プロキシ(REST版・GAE用)ソース
XSLTを使用する場合(Styleオプション指定時)、http://webservices.amazon.co.jp/onca/xmlやhttp://ecs.amazonaws.jp/onca/xmlで指定すると認証エラーに。専用のエンドポイント(http://xml-jp.amznxslt.com/onca/xml)の指定が必要らしい。
http://d.hatena.ne.jp/furyu-tei/20090703/paproxy
お陰様で動くようになりました。
XSLとjQuery/HTMLだけで作る、amazon最速検索
2009/08/05
先日「
jjencodeをApacheのmod_ext_filterに仕込む」という記事で
hasegawaさんのjjencodeをgoogle chromeなんかで使われているjavascriptエンジン"v8"で動かしてみたのですが、あまりに遅いですし、一回javascriptエンジンが走ってしまうという事がボトルネックに繋がっているんだ...という勝手な推測の元、「jjencodeをv8に依存しない形でc++に移植しよう」と思い始めたのがこの記事をポストする30分前。今出来上がりました。テストしながら書いてます。
まず、オリジナルのまま関数で移植しました。ふつーのC++のコードです。
#include <sstream>
#include <iostream>
#include <string>
using namespace std;
string jjencode(string gv, string& text) {
stringstream r, s;
const char* b[] = { "___", "__$", "_$_", "_$$", "$__", "$_$", "$$_", "$$$", "$___", "$__$", "$_$_", "$_$$", "$$__", "$$_$", "$$$_", "$$$$" };
for (int i = 0; i < text.size(); i++) {
char n = text.at( i );
if (n == 0x22 || n == 0x5c) {
s << "\\\\\\" << dec << n;
} else if ((0x20 <= n && n <= 0x2f) || (0x3A <= n == 0x40) || (0x5b <= n && n <= 0x60) || (0x7b <= n && n <= 0x7f)) {
s << dec << n;
} else if ((0x30 <= n && n <= 0x39) || (0x61 <= n && n <= 0x66)) {
if (!s.str().empty()) r << "\"" << s.str() << "\"+";
r << gv << "." << b[ n < 0x40 ? n - 0x30 : n - 0x57 ] << "+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n == 0x6c) {
if (!s.str().empty()) r << "\"" << s.str() << "\"+";
r << "(![]+\"\")[" << gv << "._$_]+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n == 0x6f) {
if (!s.str().empty()) r << "\"" << s.str() << "\"+";
r << gv << "._$+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n == 0x74) {
if( !s.str().empty() ) r << "\"" << s.str() << "\"+";
r << gv << ".__+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n == 0x75) {
if( !s.str().empty() ) r << "\"" << s.str() << "\"+";
r << gv << "._+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n < 128) {
if (!s.str().empty()) r << "\"" << s.str();
else r << "\"";
r << "\\\\\"+";
stringstream ss;
string ns;
ss << oct << int(n) && ss >> ns;
string s08i = "01234567";
for (string::iterator it = ns.begin(); it != ns.end(); it++) {
if (s08i.find(*it) != string::npos)
r << gv << "." << b[ *it-'0' ] << "+";
else
r << *it;
}
s.str("");
s.clear(stringstream::goodbit);
}else{
if (!s.str().empty()) r << "\"" << s;
else r << "\"";
r << "\\\\\"+" << gv << "._+";
stringstream ss;
string ns;
ss << hex << int(n) && ss >> ns;
string s16i = "0123456789";
string s16a = "abcdef";
for (string::iterator it = ns.begin(); it != ns.end(); it++) {
if (s16i.find(tolower(*it)) != string::npos)
r << gv << "." << b[ *it-'0' ] << "+";
else
if (s16a.find(tolower(*it)) != string::npos)
r << gv << "." << b[ tolower(*it)-'a'+10 ] << "+";
else
r << *it;
}
s.str("");
s.clear(stringstream::goodbit);
}
}
if (!s.str().empty()) r << "\"" << s.str() << "\"+";
s.str("");
s.clear(stringstream::goodbit);
s <<
gv << "=~[];" <<
gv << "={___:++" << gv +",$$$$:(![]+\"\")["+gv+"],__$:++"+gv+",$_$_:(![]+\"\")["+gv+"],_$_:++" <<
gv+",$_$$:({}+\"\")["+gv+"],$$_$:("+gv+"["+gv+"]+\"\")["+gv+"],_$$:++"+gv+",$$$_:(!\"\"+\"\")[" <<
gv+"],$__:++"+gv+",$_$:++"+gv+",$$__:({}+\"\")["+gv+"],$$_:++"+gv+",$$$:++"+gv+",$___:++"+gv+",$__$:++"+gv+"};" <<
gv+".$_=" <<
"("+gv+".$_="+gv+"+\"\")["+gv+".$_$]+" <<
"("+gv+"._$="+gv+".$_["+gv+".__$])+" <<
"("+gv+".$$=("+gv+".$+\"\")["+gv+".__$])+" <<
"((!"+gv+")+\"\")["+gv+"._$$]+" <<
"("+gv+".__="+gv+".$_["+gv+".$$_])+" <<
"("+gv+".$=(!\"\"+\"\")["+gv+".__$])+" <<
"("+gv+"._=(!\"\"+\"\")["+gv+"._$_])+" <<
gv+".$_["+gv+".$_$]+" <<
gv+".__+" <<
gv+"._$+" <<
gv+".$;" <<
gv+".$$=" <<
gv+".$+" <<
"(!\"\"+\"\")["+gv+"._$$]+" <<
gv+".__+" <<
gv+"._+" <<
gv+".$+" <<
gv+".$$;" <<
gv+".$=("+gv+".___)["+gv+".$_]["+gv+".$_];" <<
gv+".$("+gv+".$("+gv+".$$+\"\\\"\"+" << r.str() << "\"\\\"\")())();";
return s.str();
}
int main(void) {
string line, content;
while(!cin.eof()) {
getline(cin, line);
content += line;
}
cout << jjencode("$", content) << endl;
}
これで前回失敗したjqueryで試してみました。結果は...
30秒以内では戻ってこず断念。
あれーと思って、「そうか。一回結果を格納しちゃってるのがまずいのか!そうなら逐次出力すればいい!」とまた勝手な推測の元、stringstreamに溜め込まずcoutに吐き出す様修正しました。
#include <sstream>
#include <iostream>
#include <string>
using namespace std;
int main(void) {
const char* b[] = { "___", "__$", "_$_", "_$$", "$__", "$_$", "$$_", "$$$", "$___", "$__$", "$_$_", "$_$$", "$$__", "$$_$", "$$$_", "$$$$" };
string gv = "$";
cout <<
gv << "=~[];" <<
gv << "={___:++" << gv +",$$$$:(![]+\"\")["+gv+"],__$:++"+gv+",$_$_:(![]+\"\")["+gv+"],_$_:++" <<
gv+",$_$$:({}+\"\")["+gv+"],$$_$:("+gv+"["+gv+"]+\"\")["+gv+"],_$$:++"+gv+",$$$_:(!\"\"+\"\")[" <<
gv+"],$__:++"+gv+",$_$:++"+gv+",$$__:({}+\"\")["+gv+"],$$_:++"+gv+",$$$:++"+gv+",$___:++"+gv+",$__$:++"+gv+"};" <<
gv+".$_=" <<
"("+gv+".$_="+gv+"+\"\")["+gv+".$_$]+" <<
"("+gv+"._$="+gv+".$_["+gv+".__$])+" <<
"("+gv+".$$=("+gv+".$+\"\")["+gv+".__$])+" <<
"((!"+gv+")+\"\")["+gv+"._$$]+" <<
"("+gv+".__="+gv+".$_["+gv+".$$_])+" <<
"("+gv+".$=(!\"\"+\"\")["+gv+".__$])+" <<
"("+gv+"._=(!\"\"+\"\")["+gv+"._$_])+" <<
gv+".$_["+gv+".$_$]+" <<
gv+".__+" <<
gv+"._$+" <<
gv+".$;" <<
gv+".$$=" <<
gv+".$+" <<
"(!\"\"+\"\")["+gv+"._$$]+" <<
gv+".__+" <<
gv+"._+" <<
gv+".$+" <<
gv+".$$;" <<
gv+".$=("+gv+".___)["+gv+".$_]["+gv+".$_];" <<
gv+".$("+gv+".$("+gv+".$$+\"\\\"\"+";
cout.flush();
stringstream s;
while(!cin.eof()) {
char n;
cin.get(n);
if (n == 0x22 || n == 0x5c) {
s << "\\\\\\" << dec << n;
} else if ((0x20 <= n && n <= 0x2f) || (0x3A <= n == 0x40) || (0x5b <= n && n <= 0x60) || (0x7b <= n && n <= 0x7f)) {
s << dec << n;
} else if ((0x30 <= n && n <= 0x39) || (0x61 <= n && n <= 0x66)) {
if (!s.str().empty()) cout << "\"" << s.str() << "\"+";
cout << gv << "." << b[ n < 0x40 ? n - 0x30 : n - 0x57 ] << "+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n == 0x6c) {
if (!s.str().empty()) cout << "\"" << s.str() << "\"+";
cout << "(![]+\"\")[" << gv << "._$_]+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n == 0x6f) {
if (!s.str().empty()) cout << "\"" << s.str() << "\"+";
cout << gv << "._$+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n == 0x74) {
if( !s.str().empty() ) cout << "\"" << s.str() << "\"+";
cout << gv << ".__+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n == 0x75) {
if( !s.str().empty() ) cout << "\"" << s.str() << "\"+";
cout << gv << "._+";
s.str("");
s.clear(stringstream::goodbit);
} else if (n < 128) {
if (!s.str().empty()) cout << "\"" << s.str();
else cout << "\"";
cout << "\\\\\"+";
stringstream ss;
string ns;
ss << oct << int(n) && ss >> ns;
string s08i = "01234567";
for (string::iterator it = ns.begin(); it != ns.end(); it++) {
if (s08i.find(*it) != string::npos)
cout << gv << "." << b[ *it-'0' ] << "+";
else
cout << *it;
}
s.str("");
s.clear(stringstream::goodbit);
}else{
if (!s.str().empty()) cout << "\"" << s;
else cout << "\"";
cout << "\\\\\"+" << gv << "._+";
stringstream ss;
string ns;
ss << hex << int(n) && ss >> ns;
string s16i = "0123456789";
string s16a = "abcdef";
for (string::iterator it = ns.begin(); it != ns.end(); it++) {
if (s16i.find(tolower(*it)) != string::npos)
cout << gv << "." << b[ *it-'0' ] << "+";
else
if (s16a.find(tolower(*it)) != string::npos)
cout << gv << "." << b[ tolower(*it)-'a'+10 ] << "+";
else
cout << *it;
}
s.str("");
s.clear(stringstream::goodbit);
}
cout.flush();
}
if (!s.str().empty()) cout << "\"" << s.str() << "\"+";
cout << "\"\\\"\")())();" << endl;
}
これで前回失敗したjqueryで試してみました。結果は...
またもや30秒以内では戻ってこず断念。
おかしいな...と思ってmod_ext_filterを通さず実行したらjqueryのファイル全体でも1.5秒程で終了しているので、どうやらmod_ext_filter側が蓄積してしまっているのが原因だと分かりました。 ... orz
まーそうやわなー。あたりまえやなー。勝手な推測しすぎです。
ただ、一度作ったjavascriptファイルをこのツールで変換して静的ファイルとして置く事は出来るはずですので、利用価値がない...とまでは言えないかもしれません。なお、jQueryの場合は$を潰すので変数gvを$以外のものにしないと動きません。
そもそもvimでOOはしんどい。
function! Class_Prototype() dict
return self
endfunction
function! Class_Override(...) dict
if a:0 == 0|throw "Invalid Parameter"|endif
let class = copy(self)
let class.__NAME__ = a:1
if type(a:2) == type(class.New)
let class.New = a:2
else
let class.New = self.New
endif
let class.Super = self
return class
endfunction
function! Class_New(...) dict
let instance = copy(self)
call remove(instance, "New")
call remove(instance, "Override")
let instance.Super = self
return instance
endfunction
function! Class_ToString() dict
return self.__NAME__
endfunction
let Object = {
\ "__NAME__" : "Object",
\ "Prototype": function("Class_Prototype"),
\ "Override": function("Class_Override"),
\ "Super": {},
\ "New": function("Class_New"),
\ "ToString": function("Class_ToString")}
function! Human_Sing() dict
echo self.perfix . "は" . self.name . "。" . self.title . "\n"
return self
endfunction
function! Human_New(...) dict
let instance = copy(self)
let instance.perfix = a:1
let instance.name = a:2
let instance.title = a:3
let instance.Sing = function("Human_Sing")
return instance
endfunction
let Human = Object.Override("Human", function("Human_New"))
function! Gian_Boxing(who) dict
echo a:who . "のくせに生意気だぞ!!!\n"
return self
endfunction
let Gian = Human.Override("Gian", {})
let Gian.Boxing = function("Gian_Boxing")
let Dekisugi = Human.Override("Dekisugi", {})
silent! unlet gian
silent! unlet dekisugi
let gian = Gian.New("俺", "ジャイアン", "ガキ大将")
let dekisugi = Dekisugi.New("僕", "出来杉", "優等生")
call dekisugi.Sing()
call gian.Sing().Boxing("のび太").Boxing("スネ夫")
僕は出来杉。優等生
俺はジャイアン。ガキ大将
のび太のくせに生意気だぞ!!!
スネ夫のくせに生意気だぞ!!!
つかわねー!
参考文献
JSでthisをそのまま返す関数を作っておくと便利?
PHP で引数をそのまま返す関数を作っておくと便利
2009/08/04
smegheadさんがtthttpdの
patchを書いてくれました。感謝!
その後、デフォルトページがCGIの場合に上手く動かないバグを修正しました。
お礼とは言っては何ですが(Windowsなんかイラネかもしれませんが)、smegheadさんが作っておられるC言語で書かれたBTS「
starbug1」をWindowsにポーティングしてみました。
以下patch
diff -u starbug1-1.3.01.orig/hook.c starbug1-1.3.01/hook.c
--- starbug1-1.3.01.orig/hook.c 2009-07-12 20:37:55.000000000 +0900
+++ starbug1-1.3.01/hook.c 2009-08-04 10:11:19.125000000 +0900
@@ -4,7 +4,11 @@
#include <cgic.h>
#include <dirent.h>
#include <sys/stat.h>
+#ifndef _WIN32
#include <dlfcn.h>
+#else
+#include <windows.h>
+#endif
#include "data.h"
#include "alloc.h"
#include "util.h"
@@ -12,6 +16,24 @@
#include "hook_data.h"
#include "simple_string.h"
+#ifdef _WIN32
+#define dlopen(x,y) (void*)LoadLibrary(x)
+#define dlsym(x,y) (void*)GetProcAddress((HMODULE)x,y)
+#define dlclose(x) FreeLibrary((HMODULE)x)
+const char* dlerror() {
+ static char szMsgBuf[256];
+ FormatMessage(
+ FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL,
+ GetLastError(),
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ szMsgBuf,
+ sizeof szMsgBuf,
+ NULL);
+ return szMsgBuf;
+}
+#endif
+
static void put_env_a(char* name, char* value, char* buf)
{
sprintf(buf, "%s=%s", name, value);
diff -u starbug1-1.3.01.orig/util.c starbug1-1.3.01/util.c
--- starbug1-1.3.01.orig/util.c 2009-07-25 22:38:03.000000000 +0900
+++ starbug1-1.3.01/util.c 2009-08-04 11:39:35.078125000 +0900
@@ -698,9 +698,21 @@
char locale_utf8[DEFAULT_LENGTH];
sprintf(locale_utf8, "%s.UTF-8", locale);
d("locale: %s\n", locale_utf8);
+#ifndef _WIN32
setenv("LANG", locale_utf8, 1); /* FreeBSD(さくらインターネット)でsetlocaleが動作しない場合があったため、環境変数を書き換える。 */
+#else
+ {
+ char envstr[256];
+ strcpy(envstr, "LANG=");
+ strcat(envstr, locale_utf8);
+ putenv(envstr);
+ }
+#endif
d("setlocale: %s\n", setlocale(LC_ALL, locale_utf8));
d("bindtextdomain: %s\n", bindtextdomain("starbug1", "locale"));
d("textdomain: %s\n", textdomain("starbug1"));
+#ifdef _WIN32
+ bind_textdomain_codeset("starbug1", "utf-8");
+#endif
}
/* vim: set ts=4 sw=4 sts=4 expandtab fenc=utf-8: */
ビルドに必要なツール類は
gnuwin32なんかで取得して下さい。
見事起動出来ました。
ありがとうございました。
2009/08/03
以下はてなスターの引用に使う時の単語集
このド変態めが
ウホッ!
誰コレ!
なんでamachang無いの?
俺も入れろ
2009/08/01
普段はあまりブラウザを立ち上げてなくて、vimでコーディングしてる時なんかに調べたい事があればw3mを使っています。
ただ良いコンテンツを見つけて、「うむ。はてブしたい!」と思った時にイチイチFirefox起動するのも面倒臭かったので、w3mから直接ブックマーク出来る様にした。
まずは、perlからはてなブックマークする仕組み。これはAtom APIを使った単純な物。
#!/usr/bin/perl
use warnings;
use strict;
use Config::Pit;
use XML::Atom::Client;
use XML::Atom::Entry;
use XML::Atom::Link;
my $url = shift;
print "\n$url\n";
print "はてなブックマーク: ";
my $summary = <STDIN>;
my $config = pit_get( "b.hatena.ne.jp", require => {
"username" => "your username in b.hatena.ne.jp",
"password" => "your password in b.hatena.ne.jp",
});
my $entry = XML::Atom::Entry->new;
my $link = XML::Atom::Link->new;
$link->rel('related');
$link->type('text/html');
$link->href($url);
$entry->add_link($link);
$entry->summary($summary);
my $client = XML::Atom::Client->new;
$client->username( $config->{username} );
$client->password( $config->{password} );
my $edit_uri = $client->createEntry( "http://b.hatena.ne.jp/atom/post", $entry )
or warn $client->errstr;
Config::Pitを使ってる所と、はてブコメントを入力している部分がちょっと違うだけ。
このスクリプト "hb.pl" を $HOME/.w3m/hb.pl として置き、$HOME/.w3m/keymap に以下の行を追加する。
keymap m EXTERN "/usr/bin/env perl ~/.w3m/hb.pl '%s'"
これでw3mから"m"をタイプすれば、コメント入力が表示され
http://mattn.kaoriya.net/
はてなブックマーク: [これはすごい]よーわからんけどね!
確定させればブックマーク出来る。中止は CTRL-C ね。
快適快適。
2009/07/30
こまったねー。
例えば、divにclickとdblclickを両方割り当てた場合、「トンッ、トントン!」とクリックとダブルクリックを発生させた場合
- Firefox/Google Chromeだと「トントン」で2クリックが発生
- IEだと「トントン」では1クリックしか発生しない
なんて動きになってる。以下検証に使ったコード。
<html>
<head>
<script type="text/javascript">
(function() {
var reg = function(e, t, f) {
if (window.attachEvent)
e.attachEvent('on'+t, f);
else
e.addEventListener(t, f, false);
};
reg(window, 'load', function(e) {
var c1 = 0, c2 = 0;
var panel = document.getElementById('click-panel');
var updateStatus = function() {
panel.innerHTML = "click:" + c1 + " dblclick:" + c2;
};
reg(panel, 'click', function() { c1++; updateStatus() });
reg(panel, 'dblclick', function() { c2++; updateStatus() });
reg(panel, 'mouseout', function() { c1 = c2 = 0; updateStatus() });
updateStatus();
});
})();
</script>
</head>
<body>
<div id="click-panel" style="background: blue; width: 600; height: 300"></div>
<body>
</html>
例えば、図を描きたい場合にクリックで多角形を描き、ダブルクリックで完了なんてユーザインタフェースは良くある話。
上の例で言えば
- Firefox/Google Chromeは「click:3 dblclick:1」
- IEだとは「click:2 dblclick:1」
なにそれ。前回クリックした時の(new Date).getTime()覚えておいて一定間隔以上でクリック発生...なんてコード、簡単だけど書きたくないよ。(ノД`)ウワーン
書いたといっても結構前からあったのですが、いらん所を削ぎ落として軽量Webサーバとして仕立て上げました。
軽量とは言えど、CGIを使って結構色々動きます。
例えば、ソースアーカイブを解凍したらCGIがあって、apacheから見える場所にコピーして...とか面倒くさかったりしますよね。
おれは今すぐWebサーバを起動したいんだ!そして今いるディレクトリのファイルをWebサーバからサーブしたいんだー!
って事ないですか?blogソフトウェアをダウンロードして今すぐ試したいけど、apacheインストールされてなかった...とか悲しすぎます。
今回紹介する"tinytinyhttpd"(tthttpd)はそんな、小さい様で大きな問題を解決出来るかもしれないソフトウェアです。
mattn's tinytinyhttpd at master - GitHub
tiny tiny httpd
http://github.com/mattn/tinytinyhttpd/tree/master
リンク先に、動いてる様子のキャプチャがあります。
ソースはC++で書いてます。WindowsとUNIX(Linuxでだけ動作確認)で動作します。
機能としては、マルチスレッドサーバ、カスタマイズ、ディレクトリスティング、CGI起動が可能です。
小ささをウリにしているのでインストールとか、レジストリとか、面倒くさいものは要りません。exeファイルだけあれば起動します。
一応、設定ファイルの記述も可能で
[global]
port=8080
root=c:/temp/mtos
indexpages=index.html,index.php
charset=cp932
[mime/types]
cgi=@c:/strawberry/perl/bin/perl.exe
php=@c:/progra~1/php/php-cgi.exe
という設定ファイルを"-c"オプションで指定する事も出来ます。デフォルトではポート8080番でカレントディレクトリをドキュメントルートとし、phpとperlが適当なパスでCGI起動される様になっています(パスが違う場合は設定ファイルから変更出来ますし、拡張子rbに対してrubyを追加する事も出来ます)。
私が試した限りですが、以下のソフトウェアの動作が確認出来ています。
- MTOS(Movable Type Open Source) (perl)
- WordPress (php)
- blogn Plus (php)
- tDiary (ruby)
- PukiWiki (php)
- NucreusCMS (php)
- blosxom (perl)
などなど。CGIで動くならば大体動きます。
ライセンスはBSDライセンスとします。使ってみて下さい。
コードはgithubにあるので、patchウェルカムです。
今後は、コードのブラッシュアップと、拡張なんかを考えて行きたいなーと思ってます。
2009/07/23
miyagawaさんのAnyEvent::Twitter::Streamを使って「NullPointerException」をtrackして「Ga!!」を返信するスクリプトを書いてみた。trackにマルチバイトが使えないのが残念。「がっ!!」にしても良いけど、出来れば欧米の方達にも反応したい。
#!/usr/bin/perl
use strict;
use warnings;
use Config::Pit;
use AnyEvent::Twitter::Stream;
use Net::Twitter::Lite;
binmode STDOUT, ":utf8";
my $config = pit_get("nullpo-ga", require => {
"username" => "your username on twitter.com",
"password" => "your password on twitter.com"
});
my $nt = Net::Twitter::Lite->new(
username => $config->{username},
password => $config->{password},
);
my $done = AnyEvent->condvar;
my $streamer = AnyEvent::Twitter::Stream->new(
username => $config->{username},
password => $config->{password},
method => "track",
( track => 'NullPointerException' ),
on_tweet => sub {
my $tweet = shift;
print "$tweet->{user}{screen_name}: $tweet->{text}\n";
eval { $nt->update("\@$tweet->{user}{screen_name} Ga!!") };
},
on_error => sub {
my $error = shift;
warn "ERROR: $error";
$done->send;
},
on_eof => sub {
$done->send;
},
);
$done->recv;
追記
miyagawaさんから返信のupdateでブロックしちゃうのでAnyEvent::Twitter使えば良いと教えて貰いました。
#!/usr/bin/perl
use strict;
use warnings;
use Config::Pit;
use AnyEvent::Twitter;
use AnyEvent::Twitter::Stream;
binmode STDOUT, ":utf8";
my $config = pit_get("nullpo-ga", require => {
"username" => "your username on twitter.com",
"password" => "your password on twitter.com"
}
);
my $done = AnyEvent->condvar;
my $twitty = AnyEvent::Twitter->new(
username => $config->{username},
password => $config->{password},
);
my $streamer = AnyEvent::Twitter::Stream->new(
username => $config->{username},
password => $config->{password},
method => "track",
( track => 'NullPointerException' ),
on_tweet => sub {
my $tweet = shift;
print "$tweet->{user}{screen_name}: $tweet->{text}\n";
$twitty->update_status("\@$tweet->{user}{screen_name} Ga!!", sub {
my ($twitty, $status, $js_status, $error) = @_;
if (defined $error) {
warn "ERROR: $error";
}
}
);
},
on_error => sub {
my $error = shift;
warn "ERROR: $error";
$done->send;
},
on_eof => sub {
$done->send;
},
);
$done->recv;
MOONGIFTでKeepNoteってのを知った。
MOONGIFT: » Evernoteのような個人用スクラップブック「KeepNote」:オープンソースを毎日紹介
今回紹介するオープンソース・ソフトウェアはKeepNote、マルチプラットフォームで動作するメモアプリケーションだ。
http://www.moongift.jp/2009/07/keepnote/
試してみた。WYSIWYGエディタでなかなか良い。
MOONGIFTでは
GTKとあって、Windows上での動作がちょっと不安定な気もする。マルチプラットフォームで動作するメモ環境に興味のある方は試してみよう。
と書かれていたけど、少し手を加えるだけで結構使い勝手が良くなった。
以下、WindowsでKeepNoteを使う場合のフリカケ。
日付表示でロケールを使わない
そのままだと、一覧に表示される作成日付が「%p」というAM/PMをロケールに直した物が使われるが、utf-8なGTKにも関わらずWindowsなのでシフトJISが使われてしまう。設定画面で、AM/PM付き12時間表記「%I:%M %p」から24時間表記「%H:%M」に直す。
ちなみに、もしソースからコンパイルされたのであれば g_locale_to_utf8 という関数で utf-8 に直すようなpatchを書くといい。
入力モジュールを追加する
KeepNoteのWindows版にはimmoduleというGTK上での入力機構モジュールが同根されていない。入力出来ない訳ではないが OverTheSpot な入力は出来ない。そこで
ここからGTKのランタイムを持ってきて中にある
lib/gtk-2.0/2.10.0/immodules/
をKeepNoteにコピーする。これで完了。
この2つだけでもグンと使いやすくなった。
このKeepNote、ページ自身はHTMLで出力されるのでノート出力先フォルダをDropboxにしておけば、もうEvernoteですね。
Linux版もあるので、試してみよう。
2009/07/21
jjencode素晴らしいですね。記号だけでjavascriptが実行出来てしまいます。
何かに使えないかなーと考えて、apacheのmod_ext_filterを使ってjavascriptの難読サーブをやってみようかと思います。
javascriptエンジンとして使うのは、v8。本当は全部Cで書けば速いのでしょうがメンドクサイのでやりません。
以下フィルタのソース
crypt-js.cc
#include <v8.h>
#include <string>
#include <iostream>
static std::string&
replace_string(std::string& str, const std::string from, const std::string dest) {
std::string::size_type n, nb = 0;
while((n = str.find(from, nb)) != std::string::npos) {
str.replace(n, from.size(), dest);
nb = n + dest.size();
}
return str;
}
v8::Handle<v8::Value> Print(const v8::Arguments& args) {
bool first = true;
for (int i = 0; i < args.Length(); i++) {
v8::HandleScope handle_scope;
if (first) {
first = false;
} else {
printf(" ");
}
v8::String::Utf8Value str(args[i]);
const char* cstr = *str ? *str : "";
printf("%s", cstr);
}
printf("\n");
return v8::Undefined();
}
int main(int argc, char* argv[]) {
v8::V8::SetFlagsFromCommandLine(&argc, argv, true);
v8::HandleScope handle_scope;
v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
global->Set(v8::String::New("print"), v8::FunctionTemplate::New(Print));
v8::Handle<v8::Context> context = v8::Context::New(NULL, global);
v8::Context::Scope context_scope(context);
std::string line, content;
while(!std::cin.eof()) {
std::getline(std::cin, line);
content += line;
}
replace_string(content, "\\", "\\\\");
replace_string(content, "'", "\\'");
line = "var text = '";
line += content;
line += "';";
line +=
"var r=\"\";"
"var n;"
"var t;"
"var b=[ \"___\", \"__$\", \"_$_\", \"_$$\", \"$__\", \"$_$\", \"$$_\", \"$$$\", \"$___\", \"$__$\", \"$_$_\", \"$_$$\", \"$$__\", \"$$_$\", \"$$$_\", \"$$$$\", ];"
"for( var i = 0; i < text.length; i++ ){"
" n = text.charCodeAt( i );"
" if( n < 128 ){"
" r += \"\\\"\\\\\\\\\\\"+\" + n.toString( 8 ).replace( /[0-7]/g, function(c){ return \"$.\"+b[ c ]+\"+\" } );"
" }else{"
" r += \"\\\"\\\\\\\\\\\"+$._+\" + n.toString(16).replace( /[0-9a-f]/gi, function(c){ return \"$.\"+b[parseInt(c,16)]+\"+\"} );"
" }"
"}"
"print("
"\"$=~[];$={___:++$,$$$$:(![]+\\\"\\\")[$],__$:++$,$_$_:(![]+\\\"\\\")[$],_$_:++$,$_$$:({}+\\\"\\\")[$],$$_$:($[$]+\\\"\\\")[$],_$$:++$,$$$_:(!\\\"\\\"+\\\"\\\")[$],$__:++$,$_$:++$,$$__:({}+\\\"\\\")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};\"+"
"\"$.$_=\"+"
"\"($.$_=$+\\\"\\\")[$.$_$]+\"+"
"\"($._$=$.$_[$.__$])+\"+"
"\"($.$$=($.$+\\\"\\\")[$.__$])+\"+"
"\"((!$)+\\\"\\\")[$._$$]+\"+"
"\"($.__=$.$_[$.$$_])+\"+"
"\"($.$=(!\\\"\\\"+\\\"\\\")[$.__$])+\"+"
"\"($._=(!\\\"\\\"+\\\"\\\")[$._$_])+\"+"
"\"$.$_[$.$_$]+\"+"
"\"$.__+\"+"
"\"$._$+\"+"
"\"$.$;\"+"
"\"$.$$=\"+"
"\"$.$+\"+"
"\"(!\\\"\\\"+\\\"\\\")[$._$$]+\"+"
"\"$.__+\"+"
"\"$._+\"+"
"\"$.$+\"+"
"\"$.$$;\"+"
"\"$.$=($.___)[$.$_][$.$_];\"+"
"\"$.$($.$($.$$+\\\"\\\\\\\"\\\"+\" + r + \"\\\"\\\\\\\"\\\")())();\");";
v8::Handle<v8::String> source = v8::String::New(line.c_str(), line.size());
v8::TryCatch try_catch;
v8::Handle<v8::Script> script = v8::Script::Compile(source, v8::String::New(""));
v8::Handle<v8::Value> result = script->Run();
return 0;
}
エスケープして変数textに入れて、残りはjjencodeそのままです。
これをhttpd.confで以下の様に設定します。
LoadModule ext_filter_module modules/mod_ext_filter.so
ExtFilterDefine crypt_js mode=output intype=application/x-javascript cmd="/path/to/crypt-js"
出来ました。簡単ですね。
これで以下のjavascriptをダウンロードしてみます。
window.alert('hasegawa!');
コマンドラインから...
curl http://localhost:8080/filter/foo.js
$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+"\\"+$.__$+$.$$_+$.$$$+"\\"+$.__$+$.$_$+$.__$+"\\"+$.__$+$.$_$+$.$$_+"\\"+$.__$+$.$__+$.$__+"\\"+$.__$+$.$_$+$.$$$+"\\"+$.__$+$.$$_+$.$$$+"\\"+$.$_$+$.$$_+"\\"+$.__$+$.$__+$.__$+"\\"+$.__$+$.$_$+$.$__+"\\"+$.__$+$.$__+$.$_$+"\\"+$.__$+$.$$_+$._$_+"\\"+$.__$+$.$$_+$.$__+"\\"+$.$_$+$.___+"\\"+$.$__+$.$$$+"\\"+$.__$+$.$_$+$.___+"\\"+$.__$+$.$__+$.__$+"\\"+$.__$+$.$$_+$._$$+"\\"+$.__$+$.$__+$.$_$+"\\"+$.__$+$.$__+$.$$$+"\\"+$.__$+$.$__+$.__$+"\\"+$.__$+$.$$_+$.$$$+"\\"+$.__$+$.$__+$.__$+"\\"+$.$__+$.__$+"\\"+$.$__+$.$$$+"\\"+$.$_$+$.__$+"\\"+$.$$$+$._$$+"\"")())();
おぉ!
HTMLから実行してみました。
おぉ!
これいいね!とか思ってjQueryでやってみたら、30秒以上は戻って来ませんでした... orz
別にEditor's Warを起こそうって訳では無いです。
だたvimが弱々しく読める文章だったので補足させて頂きたい。
Emacs の人と Vim の人のキーストロークの数え方の違い。 - 日々、とんは語る。
Ctr キーは1ストロークには入りません!
普段 Emacs を使うとき、どう考えても「修飾キー + 文字キーの組み合わせは一発操作」という感覚でしかありません。
「Ctr + Alt + s」とか、修飾キーが複数であっても、同時押しであれば、僕の中では1ストロークです。
http://d.hatena.ne.jp/tomoya/20090715/1247590638
慣れればそうかもしれませんが、モディファイアキーを使うには両手、もしくは指が2本要ります。片手で済むならばもう片方の手で鼻をほじる事だって出来るんです。なんてすばらしい。
Emacs の人と Vim の人のキーストロークの数え方の違い。 - 日々、とんは語る。
一発で何んでもできるっていうところが重要で、とりあえず、気に入ったコマンドはキーバインドを割り当てます*1。これ常識。
http://d.hatena.ne.jp/tomoya/20090715/1247590638
vimもしますよ。人によってはストロークではなくてモディファイアキーを使った物にマップする人もいます。
Emacs の人と Vim の人のキーストロークの数え方の違い。 - 日々、とんは語る。
何ならキーバインド定義用のマイナーモードとか作って、オンオフを切り替えることで、キーバインドを自由に入れ替えて使う事もできます。
http://d.hatena.ne.jp/tomoya/20090715/1247590638
それもvimで出来ますし、無ければ作れます。
Emacs の人と Vim の人のキーストロークの数え方の違い。 - 日々、とんは語る。
そういった、キー操作カスタマイズの柔軟性が Emacs の利点であり、Vim の場合のアレコレする場合はコマンドを打ち込まないとアレコレできないところに、僕は不満を感じるのかもしれないなぁと、ふと思った。
http://d.hatena.ne.jp/tomoya/20090715/1247590638
vimも設定次第ではいくらでも出来ます。キー登録用のスクリプトもやろうと思えば出来ます。
emacsでも次回起動からも有効にしようと思えば .emacs に書くかと思いますが、間違ってますか?
ただしvimmerはしません。なぜなら...
コマンドを打ち込むのが好きだから。
自分だけのvimrcを作る為に、惜しげなく時間を費やす人達なのです。DropBoxなんかのストレージサービスやgithubなんかにvimrcを置いてしまう人達なんです。
ブログなんかでvimrcを晒しちゃう人達なんです。
えっそれemacsユーザも同じ?変態ですね!
mattn the vimmer!
2009/07/16
2009/07/16 追記
AWS認証制限に対応しました。本文中はそのままですが、デモには非公開キーを使用してアクセスするCGIに変更しています。
時代は便利になった物です。
MOONGIFT: » XMLをJSONにするXSLT「xml2json.xslt」:オープンソースを毎日紹介
xml2json.xsltを見ていたらma.la氏のAmazon最速検索を思い出した。あちらはAmazon AWS専用になるだろうが、xml2json.xsltはそれをもっと汎用的なものにしたと考えられるだろう。
http://www.moongift.jp/2009/04/xml2json-xslt/
javascriptとXSLTを使うならば、AWSサーバから見えるサーバを用意しないといけないのですが
Yahoo! YQLを使えばそれも要りません。
YQLといえばPipesに毛の生えた様な物だと思う方もいらっしゃるかもしれませんが、解析した結果をXMLやJSONで返す事ができJSONならばDOMツリーをJSONで表現してくれます。
例えば
githubで自分が公開したりforkしたりしているプロジェクトの一覧を作る場合、何を使って作りますか?Pipes?CGI?YQLならば簡単です。
続きを読む...
2009/07/15
amachangの記事で
glooxなんて物があるのを知った。
Gloox で XMPP を書いてみた - IT戦記
けっこうシンプルに書ける
http://d.hatena.ne.jp/amachang/20090601/1243852022
gloox - A portable high-level Jabber/XMPP library for C++
gloox is a rock-solid, full-featured Jabber/XMPP client library, written in C++. It makes writing spec-compliant clients easy and allows for hassle-free integration of Jabber/XMPP functionality into existing applications. gloox is released under the GNU GPL. Commercial licensing and support are available.
http://camaya.net/
確かに簡単そう。って事でWindowsだけれど試した。ソース見ると、なんと既にWindows対応出来ている。すばらしい。ただVisual Studio用のプロジェクトファイルが付いているだけだったので、mingw32からビルドする際には少し小細工が必要。
以下、その手順。
# wget http://camaya.net/download/gloox-1.0-beta7.tar.bz2
# tar xjvf gloox-1.0-beta7.tar.bz2
# cd gloox-1.0-beta7
# vim config.h.win
comment out HAVE_WINDNS_H
// #define HAVE_WINDNS_H 1
# cd src
# gcc -I. -c *.cpp
# ar cr libgloox.a *.o
これでmingw32用のlibgloox.aが出来上がる。そしてソース。glooxを使っておしゃべりするプログラムなので鶏っぽく"glookoo"(ぐるっくー)と名づけた。
必要な物はreadlineとpthread。readlineは出回っている物には
--enable-multibyte
付きでコンパイルされた物が見当たらなかったので、rubyのmingw32バイナリ等でも使われている
ココの
物から使わせて頂く。
またpthreadは
ココからダウンロードして
# make clean GCE-inlined
でビルドした物を使う。以下コード。
#include <gloox/client.h>
#include <gloox/connectionlistener.h>
#include <gloox/messagesessionhandler.h>
#include <gloox/messageeventhandler.h>
#include <gloox/messagehandler.h>
#include <gloox/message.h>
#include <gloox/messagesession.h>
#include <pthread.h>
#include <readline/readline.h>
#ifdef _WIN32
#include <windows.h>
#endif
static char* str_to_utf8_alloc(const char* str) {
#ifdef _WIN32
size_t in_len = strlen(str);
wchar_t* wcsdata;
char* mbsdata;
size_t mbssize, wcssize;
wcssize = MultiByteToWideChar(GetACP(), 0, str, in_len, NULL, 0);
wcsdata = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t));
wcssize = MultiByteToWideChar(GetACP(), 0, str, in_len, wcsdata, wcssize + 1);
wcsdata[wcssize] = 0;
mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsdata, -1, NULL, 0, NULL, NULL);
mbsdata = (char*) malloc((mbssize + 1));
mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsdata, -1, mbsdata, mbssize, NULL, NULL);
mbsdata[mbssize] = 0;
free(wcsdata);
return mbsdata;
#else
return strdup(str);
#endif
}
static char* utf8_to_str_alloc(const char* utf8) {
#ifdef _WIN32
size_t in_len = strlen(utf8);
wchar_t* wcsdata;
char* mbsdata;
size_t mbssize, wcssize;
wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, in_len, NULL, 0);
wcsdata = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t));
wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, in_len, wcsdata, wcssize + 1);
wcsdata[wcssize] = 0;
mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsdata, -1, NULL, 0, NULL, NULL);
mbsdata = (char*) malloc((mbssize + 1));
mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsdata, -1, mbsdata, mbssize, NULL, NULL);
mbsdata[mbssize] = 0;
free(wcsdata);
return mbsdata;
#else
return strdup(utf8);
#endif
}
class Glookoo : public gloox::ConnectionListener, gloox::MessageHandler
{
public:
Glookoo(const char* server, const char* jid, const char* passwd, const char* user)
: server_(server), jid_(jid), passwd_(passwd), user_(user) {
client_ = NULL;
session_ = NULL;
}
virtual ~Glookoo() {
delete client_;
}
virtual void onConnect() {
displayMessage("=== connected ===");
session_ = new gloox::MessageSession(client_, gloox::JID(user_));
session_->registerMessageHandler(this);
}
virtual void handleMessage(const gloox::Message& msg, gloox::MessageSession *session) {
char* message = utf8_to_str_alloc(msg.body().c_str());
displayMessage(message);
free(message);
}
virtual void onDisconnect(gloox::ConnectionError reason) {
displayMessage("=== disconnected ===");
delete session_;
session_ = NULL;
}
virtual bool onTLSConnect(const gloox::CertInfo &info) {
return true;
}
void run() {
client_ = new gloox::Client(gloox::JID(jid_), passwd_);
client_->registerConnectionListener(this);
client_->setServer(server_);
client_->connect();
}
void sendMessage(const char* message) {
if (session_) {
char* send_message = str_to_utf8_alloc(message);
session_->send(send_message, "");
free(send_message);
}
}
void displayMessage(const char* message) {
int point = rl_point;
rl_beg_of_line(0, 0);
rl_redisplay();
printf("%s\n", message);
rl_point = point;
rl_refresh_line(0, 0);
}
private:
gloox::Client* client_;
gloox::MessageSession* session_;
const char* server_;
const char* jid_;
const char* passwd_;
const char* user_;
};
Glookoo* glookoo = NULL;
void* input_func(void* arg) {
pthread_detach(pthread_self());
while (true) {
rl_callback_read_char();
}
return 0;
}
void* gloox_func(void* arg) {
glookoo->run();
}
void input_handler(void) {
char* message = rl_copy_text(0, rl_end);
if (glookoo) glookoo->sendMessage(message);
free(message);
}
int opterr = 1;
int optind = 1;
int optopt;
char *optarg;
int getopt(int argc, char** argv, char* opts) {
static int sp = 1;
register int c;
register char *cp;
if(sp == 1) {
if(optind >= argc ||
argv[optind][0] != '-' || argv[optind][1] == '\0')
return(EOF);
else if(strcmp(argv[optind], "--") == 0) {
optind++;
return(EOF);
}
}
optopt = c = argv[optind][sp];
if(c == ':' || (cp=strchr(opts, c)) == NULL) {
if(argv[optind][++sp] == '\0') {
optind++;
sp = 1;
}
return('?');
}
if(*++cp == ':') {
if(argv[optind][sp+1] != '\0')
optarg = &argv[optind++][sp+1];
else if(++optind >= argc) {
sp = 1;
return('?');
} else
optarg = argv[optind++];
sp = 1;
} else {
if(argv[optind][++sp] == '\0') {
sp = 1;
optind++;
}
optarg = NULL;
}
return(c);
}
int main(int argc, char* argv[]) {
pthread_t pt_input, pt_gloox;
char* server = "talk.google.com";
char* jid = NULL;
char* passwd = NULL;
char* user = "wassr-bot@wassr.jp";
int c;
opterr = 0;
while ((c = getopt(argc, argv, "j:p:s:u:") != -1)) {
switch (optopt) {
case 'j': jid = optarg; break;
case 'p': passwd = optarg; break;
case 's': server = optarg; break;
case 'u': user = optarg; break;
case '?': break;
default:
argc = 0;
break;
}
optarg = NULL;
}
if (!jid || !passwd) {
fprintf (stderr, "Usage: glookoo [-s server] [-j jid] [-p passwd] [-u user]\n");
exit (1);
}
rl_callback_handler_install("#> ", (void(*)(char*))input_handler);
glookoo = new Glookoo(server, jid, passwd, user);
pthread_create(&pt_input, NULL, &input_func, glookoo);
pthread_create(&pt_gloox, NULL, &gloox_func, glookoo);
pthread_join(pt_gloox, NULL);
pthread_join(pt_input, NULL);
rl_deprep_terminal();
return 0;
}
相変わらずコメントもなく適当ですね。
mingw32でのビルド手順は以下の通り。
gcc -Ic:/mingw/include/pthread -o glookoo.exe -I. glookoo.cxx libgloox.a c:/mingw/lib/readline.lib -lstdc++ -lws2_32 -lcrypt32 -lsecur32 -lpthreadGCE2
使い方はこんな感じ
Usage: glookoo [-s server] [-j jid] [-p passwd] [-u user]
デフォルトでサーバはGoogle Talk、ユーザは
ワッサーのボットになっています。
マルチスレッドで入力とGlooxを処理していて入力途中の文字列を消されないようにしてあります。
今回はバイナリも置いておきます。
glookoo-0.0.1.tar.gz
よろしければどうぞ。ソースは後でgithubにでも上げておきます。
javascript:(function(e){e[0].value='your-name';e[1].value='#room-1,#room-2,#room-3';e[5].click()})(document.getElementsByTagName('form')[0].elements);
javascriptで動的にform/input要素を作っているのでname属性もid属性も無い。なのでこんなjs。
これで
webhatを開いてブックマークレット一発でログイン。
your-name, room-1, room-2, room-3 等を修正してお使い下さい。
2009/07/11
またさらに動かなくなってしまいました。
(再)ブラウザを全く使わずにustream.tvを楽しむ方法
その後、ustream.tvで何か変更があり、そのままでは使えなくなってしまったのですが、もういっかいチャレンジしたら見れる事が分かりました。
http://mattn.kaoriya.net/web/ustream/20090622220622.htm
今度の変更は、paramに&(アンパサンド)が入ったのと、IE対応による重複したparam要素です。
でも抽出しなきゃ行けない値は変わりません。
FLASHPLAYER=/usr/bin/flashplayer
if [ "x$1" == "x" ]; then
echo "usage: `basename $0` [channel]"
exit
fi
TMP=`mktemp /tmp/ustplayer.XXXXXX`
curl -s "http://www.ustream.tv/channel/$1" |\
/bin/grep "<param name=\"\(flashvars\|movie\)\"" |\
/bin/sed -e 's/&/\&/g' -e 's/^\s*//g' |\
/usr/bin/sort |\
/usr/bin/uniq > $TMP
URL=`cat $TMP | sed -ne 's/^\s*<param name="movie" value=\"\([^\"]\+\)\".*$/\1/p'`
URL=$URL\&`cat $TMP | sed -ne 's/^\s*<param name="flashvars" value=\"\([^\"]\+\)\".*/\1/p'`
rm $TMP
if [ "x$URL" == "x" ]; then
echo "currently offline?"
exit
fi
echo playing $URL
$FLASHPLAYER "$URL"
負けません。
もうそろそろ面倒くさくなってきたので、githubに置いて更新して行きます。
mattn's ustplayer at master - GitHub
ustream.tv player using standalone flashplayer
http://github.com/mattn/ustplayer/tree/master
2009/07/09
XML::Simpleだと格納される結果が決まっており、例えば
<statuses>
<status>
<id>4773580</id>
<text>kazuhoさんがやってくれました!</text>
<user>
<screen_name>mattn</screen_name>
</user>
</status>
<status>
<id>4773581</id>
<text>今日のnickは○○提供です。</text>
<user>
<screen_name>kazuho</screen_name>
</user>
</status>
</statuses>
こんなXMLを以下の様な形にしたい場合がある場合に少し不便だったりします。
---
id: 4773580
text: kazuhoさんがやってくれました!
screen_name: mattn
---
id: 4773581
text: 今日のnickは○○提供です。
screen_name: kazuho
XML::Simpleを使うと、arrayノード一つにID要素があると勝手にノード扱いになったり、不必要なノードへのアクセスが必要になったりします。以下XML::Simpleのパース結果
---
status:
4773580:
text: kazuhoさんがやってくれました!
user:
screen_name: mattn
4773581:
text: 今日のnickは○○提供です。
user:
screen_name: kazuho
こんな場合にはXML::CuteQueriesを使うと便利です。
Paul Miller / XML-CuteQueries - search.cpan.org
A cute little query language for converting XML to Perl
http://search.cpan.org/dist/XML-CuteQueries/
上の例であれば以下のコードで望み通りの形式でパース出来てしまいます。
use strict;
use warnings;
use LWP::Simple;
use XML::CuteQueries;
my $cq = XML::CuteQueries->new;
$cq->parse(get "http://api.wassr.jp/statuses/public_timeline.xml");
my @statuses = $cq->cute_query("/statuses/*" => {'*' => '', 'user/*' => ''});
use YAML;
warn Dump @statuses;
PODを見ていただければ分かりますが、XPathでクエリ式を書きそれに対するデータシェイプを指定します。例であれば"/statuses/*"にあるノードすべて"*"はデータシェイプのルートに、また"/statuses/*"にある"user/*"(user内の全て)もデータシェイプ内のルートに置くという指定になります。
これ、XML版のWeb::Scraperって感じですかね。便利だわー。
2009/07/07
kazuhoさんがやってくれました。
ずいぶん前からjsonをC++でパース(SAXじゃなくてDOM)するのに小さいライブラリないかなーと思ってました。個人的には
json-cというのを使ってたのですが、幾らか気に入らない所があったりビルドが少し手間だったりしていました。STLしか使わなくてvectorとかmapで表現されるツリー構造な物が欲しいなぁって思ってたんです。
とあるIRCで昨日、kazuhoさんと「ほしいですよねー」という話から始まって、githubにある
jsonxxとかも物色しながら「いいのないねー」とか言ってたらkazuhoさんが「もすこし綺麗に書けそう」って言い出して朝から本格的に書き始めてついさっき出来上がりました。速いw
名前はpicojson
とても小さく、実装コードだと300数十ステップ程です。しかもヘッダファイルだけなので管理が楽です。
試しに
wassrのpublicタイムラインをパースしてみました。
コードはこんな感じ。
curlのコードではなく、jsonのパース部分を見てください。
#include <curl/curl.h>
#include "../picojson.h"
typedef struct {
char* data;
size_t size;
} MEMFILE;
MEMFILE*
memfopen() {
MEMFILE* mf = (MEMFILE*) malloc(sizeof(MEMFILE));
mf->data = NULL;
mf->size = 0;
return mf;
}
void
memfclose(MEMFILE* mf) {
if (mf->data) free(mf->data);
free(mf);
}
size_t
memfwrite(char* ptr, size_t size, size_t nmemb, void* stream) {
MEMFILE* mf = (MEMFILE*) stream;
int block = size * nmemb;
if (!mf->data)
mf->data = (char*) malloc(block);
else
mf->data = (char*) realloc(mf->data, mf->size + block);
if (mf->data) {
memcpy(mf->data + mf->size, ptr, block);
mf->size += block;
}
return block;
}
char*
memfstrdup(MEMFILE* mf) {
char* buf = (char*)malloc(mf->size + 1);
memcpy(buf, mf->data, mf->size);
buf[mf->size] = 0;
return buf;
}
using namespace std;
using namespace picojson;
int
main(int argc, char* argv[]) {
char error[256];
MEMFILE* mf = memfopen();
CURL* curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "http://api.wassr.jp/statuses/public_timeline.json");
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, &error);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, memfwrite);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, mf);
if (curl_easy_perform(curl) != CURLE_OK) {
cerr << error << endl;
} else {
value v;
string err;
parse(v, mf->data, mf->data + mf->size, &err);
if (err.empty()) {
array arr = v.get<array>();
array::iterator it;
for (it = arr.begin(); it != arr.end(); it++) {
object obj = it->get<object>();
cout << obj["user_login_id"].to_str() << ": " << obj["text"].to_str() << endl;
}
} else {
cerr << err << endl;
}
}
curl_easy_cleanup(curl);
memfclose(mf);
return 0;
}
こんなに短いコードでアプリが作れる!
STLに慣れた人ならイメージ沸くかと思います。すばらしい!
こういうのが欲しかったんです。
ただまだ出来上がったばっかりですしバグはあるかもしれません。また高機能にするつもりもないでしょうから使用目的を選ぶのが先決かと思います。
ライセンスはBSDとの事なので、バイナリ配布も可能です。
share - Revision 34226: /lang/cplusplus/picojson/trunk
picojson
http://svn.coderepos.org/share/lang/cplusplus/picojson/trunk/
ありがたや、ありがたや。
追記
kazuhoさんも記事を書いてますんでそちらも...
Kazuho@Cybozu Labs: 今更 C++ で JSON パーサ「picojson」を書いたわけ
http://developer.cybozu.co.jp/kazuho/2009/07/c-json-picojson.html
2009/07/05
最後までC言語で書き直そうかと迷いましたが、結局Perlのままになりました。
スクリーンショットは以下
しくみは、起動時にオートディスカバリでフィード一覧をコンボボックスに展開、コンボボックスが選ばれたらフィードを取得してエントリ一覧をリストに展開、リストがダブルクリックされたらHTMLをスクレイピングしてGtk2::Ex::MPlayerEmbedで再生という単純なもの。プレーヤ部分のツールチップにニュース本文が表示されます。
本当は、fltk2でmplayer埋め込みプレーヤを2年ほど前に作ってあったのでそれを使っても良かったのですが、いかんせんfltkがマイナー過ぎるのでやめました。
mattn's nhk-news-pl at master - GitHub
NHK News Movie Player
http://github.com/mattn/nhk-news-pl/tree/master
ライセンスとか難しい物はありません。ソースは上のリンクにあるのでパクるとか、お好み焼きに付けるとか、勝手にして下さい。
問題ありそうならキャプチャは取り下げる予定です。
2009/06/26
Luaでもやろうと思えば、色んな事が出来ます。先日書いた「
LuaでTwitterるわ」を良い例に、Luaでも拡張さえ利用すれば何でも出来るんです。
で、今日は何をしようかと考えて...
Luaでmigemoを使用して、本当に「Lua」で「るわ」が検索出来るかどうかやってみようと思います。
#もう結果が見えていますが、やるんです...
まず、Luaで高度な処理を行うには、拡張モジュールを作る必要があります。
MigemoはC/Migemoを基礎ライブラリとして、ラッパI/Fとして作成します。
まずC/Migemoを、
香り屋からsvnで取得して、ビルドします。
#今回は、Windowsでコンパイルしています。
C:¥temp> svn co http://cvs.kaoriya.net/svn/CMigemo/trunk cmigemo
C:¥temp> cd cmigemo
C:¥temp¥cmigemo> cp config.mak config.mk.orig
C:¥temp¥cmigemo> vim config.mak
C:¥temp¥cmigemo> diff config.mk.org config.mk
38,39c38,39
< FILTER_CP932 = qkc -q -u -s
< FILTER_EUCJP = qkc -q -u -e
---
> #FILTER_CP932 = qkc -q -u -s
> #FILTER_EUCJP = qkc -q -u -e
41,42c41,42
< #FILTER_CP932 = nkf -s
< #FILTER_EUCJP = nkf -e
---
> FILTER_CP932 = nkf -s
> FILTER_EUCJP = nkf -e
C:¥temp¥cmigemo> vcvars32
Setting environment for using Microsoft Visual C++ tools.
C:¥temp¥cmigemo> nmake msvc
C:¥temp¥cmigemo> nmake msvc-dict
次に、今回作成したluamigemoをビルドします。
C:¥Lua¥luamigemo> nmake
後は以下のコードを実行するまで
local migemo = require("migemo")
local rex_pcre = require("rex_pcre")
local mcx = migemo.open("C:/temp/cmigemo/dict/migemo-dict")
migemo.set(mcx, migemo.OPINDEX_OR, "|");
migemo.set(mcx, migemo.OPINDEX_NEST_IN, "\\(");
migemo.set(mcx, migemo.OPINDEX_NEST_OUT, "\\)");
function test(subject, query)
local pattern = migemo.query(mcx, query)
local match = rex_pcre.match(subject, pattern)
if match then
print(subject.." is matched with "..query)
else
print(subject.." is not matched with "..query)
end
end
test("まっつん", "mattun")
test("マッツン", "mattun")
test("マッツン", "mattn")
test("幹事", "kanji")
test("私はマッツンです", "watashiHaMattunDesu")
test("るわ", "lua")
今回は、migemoで取得した正規表現パタンを食わせる為にLrexlibという拡張を使用し、検証しています。
実行結果は
まっつん is matched with mattun
マッツン is matched with mattun
マッツン is not matched with mattn
幹事 is matched with kanji
私はマッツンです is matched with watashiHaMattunDesu
るわ is not matched with lua
となり、結果「Lua」では「るわ」は検索出来ない事が分かりました。
ダウンロード:
2009/06/24
これはgithubじゃなくても使えるかも
Gitはトランスポート層が選択出来るのは知っていたけど、まさかこんな書き方出来るとは思ってなかった。
以前、こんな記事書きましたが
github.comへのSSH接続にはホスト名"ssh.github.com"、ポート"443"に接続する様に設定します。※このssh.github.comが味噌です。
github.comへのSSH接続にはホスト名"ssh.github.com"、ポート"443"に接続する様に設定します。
※このssh.github.comが味噌です。
http://mattn.kaoriya.net/software/20081029172540.htm
これ以下の1行で出来る事が分かりました。
# git clone ssh://git@ssh.github.com:443/my-name/my-repo.git
ssh.github.comに443ポートでSSHプロトコルを喋るよ!っていう指定になります。これでcloneしたワーキングツリーでは、以前書いた記事の様にpullはpublic clone、pushはowner cloneというやり方ではなくowner cloneといてpullしている為 .ssh/config ファイルを修正したり
# git remote add origin git@github.com:my-name/my-repo.git
する事もなくいきなりpush出来るになります。
こりゃいいわ。
Freenodeのwebchatインタフェースを叩いて通信し、携帯電話からでもIRC出来るCGI書いた。
欲しい人なんかいるんかなーとか思いながら、作ったのでメッセージの送受信しか出来ません。
nick登録もなし、nickの自動割り当てもなしです。
私はこんだけあったら十分なので...。コードはこの辺にあります。
mattn's freenode-mobile-gateway at master - GitHub
IRC gateway for freenode writen in perl.
http://github.com/mattn/freenode-mobile-gateway/tree/master
SoftBank携帯ですが、一応動いてます。欲しい機能があればgithubでforkして下さい。
2009/06/23
以前こんな記事を書いた事がありました。
Big Sky :: ブラウザを全く使わずにustream.tvを楽しむ方法
映像/音声ですが、Linux版に用意されているスタンドアローン版flashplayerを使っています。ネット上にあるFLVもコマンドラインから起動して閲覧出来ます。
http://mattn.kaoriya.net/web/ustream/20071018005843.htm
その後、ustream.tvで何か変更があり、そのままでは使えなくなってしまったのですが、もういっかいチャレンジしたら見れる事が分かりました。
答えは簡単でobjectタグのパラメータ値を全て足せば再生出来るURLになるという事。
たとえばさっきまで見ていたVimMの動画。
<object
id="viewer"
name="viewer"
width="480"
height="386"
allowfullscreen="true"
codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
ここから
<param name="movie" value="http://cdn1.ustream.tv/swf/4/viewer.137.swf?varnish=true" />
<param name="flashvars" value="loc=/&cid=19118&channelid=19118&share=false&group=channel19118&imu=medrect&autoResize=false&localid=4398457344a3f82bac4140&varnish=true&vrsl=c.4.158&" />
ここまで
</object>
これを結合して出来上がったURLにflashplayerスタンドアローン版でアクセスすれば良いのです。
これをshellスクリプトにすると以下の様になりました。
GFLASHPLAYER=/usr/bin/flashplayer
if [ "x$1" == "x" ]; then
echo "usage: `basename $0` [channel]"
exit
fi
TMP=`mktemp /tmp/XXXXXX`
curl -s "http://www.ustream.tv/channel/$1" | grep "<param" > $TMP
URL=''
while read -r line; do
if [ ! -z $URL ]; then
URL="$URL&"
fi
URL="$URL`echo \"$line\" | sed 's/^.* value=\"\([^\"]\+\)\".*$/\1/'`"
done < $TMP
rm $TMP
if [ "x$URL" == "x" ]; then
echo "currently offline?"
exit
fi
echo playing $URL
$GFLASHPLAYER "$URL"
これでまたブラウザが楽になりました。
アナウンスしてなかったですが、先日書いた「
VimからFastLadderを扱えるスクリプトFastLadder.vim書いた。」ですが、
LivedoorReaderに対応させています。
let g:fastladder_server = 'http://reader.livedoor.com'
とvimrcに書いておくと
:FastLadder
でLivedoorReaderのフィードが見れます。
今後はFastLadderやLivedoorReaderの様にフォルダ単位の閲覧が出来る様に改良して行く予定です。
2009/06/22
VCだとビルド出来るらしいけど、strawberry perlとかだとコンパイル出来ない。
まずCoro側のpatch。
diff -ur Coro-5.132.orig/Coro/libcoro/coro.c Coro-5.132/Coro/libcoro/coro.c
--- Coro-5.132.orig/Coro/libcoro/coro.c 2008-11-19 11:50:13.000000000 +0900
+++ Coro-5.132/Coro/libcoro/coro.c 2009-06-19 15:01:16.140625000 +0900
@@ -228,6 +228,9 @@
#if __CYGWIN__
ctx->env[7] = (long)((char *)sptr + ssize) - sizeof (long);
ctx->env[8] = (long)coro_init;
+ #elif defined(__MINGW32__)
+ ctx->env[4] = (int)((unsigned char *)sptr + ssize);
+ ctx->env[5] = (long)coro_init;
#elif defined(_M_IX86)
((_JUMP_BUFFER *)&ctx->env)->Eip = (long)coro_init;
((_JUMP_BUFFER *)&ctx->env)->Esp = (long)STACK_ADJUST_PTR (sptr, ssize) - sizeof (long);
mingw32のjumpbufは4番目がEipで5番目がEspだったはず。
一応手元で動いてます。
あとこのままでも駄目で、ExtUtils::MakeMakerがgccの時に付けてしまう--image-baseオプションがまずい。
アドレスの作り方が Coro::Event であれば Event という文字列に対して上位4バイト、下位4バイトで分けてunpackとかやってござる。
if ($GCC) {
my $dllname = $self->{BASEEXT} . "." . $self->{DLEXT};
$dllname =~ /(....)(.{0,4})/;
my $baseaddr = unpack("n", $1 ^ $2);
$otherldflags .= sprintf("-Wl,--image-base,0x%x0000 ", $baseaddr);
}
なので、Coro::Event が使っている Event というモジュールと --image-base がバッティングして起動時にメモリロケーション不正参照のエラーが起きる。
こちらについては、ExtUtils::MakeMakerに含まれる MM_Win32.pm を修正する。最近の gcc は勝手に --image-base 作ってくれるので、そちらに任せる。
--- MM_Win32.pm.orig 2009-06-22 17:48:43.906250000 +0900
+++ MM_Win32.pm 2009-06-22 17:49:05.703125000 +0900
@@ -307,12 +307,12 @@
# we try to overcome non-relocateable-DLL problems by generating
# a (hopefully unique) image-base from the dll's name
# -- BKS, 10-19-1999
- if ($GCC) {
- my $dllname = $self->{BASEEXT} . "." . $self->{DLEXT};
- $dllname =~ /(....)(.{0,4})/;
- my $baseaddr = unpack("n", $1 ^ $2);
- $otherldflags .= sprintf("-Wl,--image-base,0x%x0000 ", $baseaddr);
- }
+# if ($GCC) {
+# my $dllname = $self->{BASEEXT} . "." . $self->{DLEXT};
+# $dllname =~ /(....)(.{0,4})/;
+# my $baseaddr = unpack("n", $1 ^ $2);
+# $otherldflags .= sprintf("-Wl,--image-base,0x%x0000 ", $baseaddr);
+# }
push(@m,'
# This section creates the dynamically loadable $(INST_DYNAMIC)
SYNOPSISの例でもちゃんと動きます。
あとはバグ報告から上手く修正されれば...
IRCのWebChatと言えば、
Mibbit.comが有名ですが、Freenode自身が
qwebircという、
Twistedと
Mootoolsを使ったIRCチャットサーバアプリを持っている事に気づきました。
freenode Web IRC (qwebirc)
Connect to freenode IRC
http://webchat.freenode.net/
Webページへの埋め込みもウィザード付きで簡単です。チャネル#Vim-users.jpであればこんな感じの埋め込みでいけます。
続きを読む...
2009/06/18
Windowsでは
Growl For Windowsとそれが使っているプロトコル
GNTPにより、Windowsでもアイコンを使ったGrowlアプリケーションの開発が可能になりました。その一つにmiyagawaさんが作った
github growlerのGNTP版でもある
yet another github growlerというのも作りました。
これでMac, Windowsでのgithub growlerがある事になるのですが、Linuxにありません。Linuxにはアイコンが表示できてGNTPプロトコルを喋るGrowlシステムがありません。
そこで以前から使っていた、Growlネットワークプロトコル(Growlネットワークプロトコルはアイコンが出せません)をサポートしているmumblesというGrowlシステムを調べて見たところ、内部ではpythonでDBusによるプロセス間通信を行っている事が分かりました。
mumbles-project.org
a plugin driven, modern notification system for Gnome
http://www.mumbles-project.org/
さらにそのDBusインタフェース上ではアイコン表示をサポートしていた為、これは!と思いGitHubのGrowlアプリケーションを作ってみました。
まず、DBusで通信する為のプラグインを作成します。
DBusでメソッドが呼ばれると、MumblesPluginクラスに渡されるMumblesNotifyオブジェクトのalertメソッドを呼び出します。
全体のソースは以下の様になります。
from MumblesPlugin import *
import dbus
import gnomevfs
import os
import urllib
class GithubMumbles(MumblesPlugin):
plugin_name = 'GithubMumbles'
dbus_interface = 'com.github.DBus'
dbus_path = '/com/github/DBus'
icons = {'github' : 'github.png'}
__url = None
def __init__(self, mumbles_notify, session_bus):
self.signal_config = {
'Notify': self.Notify,
'NotifyNum': self.NotifyNum
}
MumblesPlugin.__init__(self, mumbles_notify, session_bus)
self.add_click_handler(self.onClick)
def NotifyNum(self, num):
self.__url = 'http://github.com/'
icon = self.get_icon('github')
title = 'Github'
msg = str(num)+' new messages!'
self.mumbles_notify.alert(self.plugin_name, title, msg, icon)
def Notify(self, link, author, text):
self.__url = link
path = os.path.join(PLUGIN_DIR_USER, 'icons', 'github-%s' % author)
if os.path.exists(path):
self.icons[author] = 'github-%s' % author
icon = self.get_icon(author)
else:
icon = self.get_icon('github')
self.mumbles_notify.alert(self.plugin_name, author, text, icon)
def onClick(self, widget, event, plugin_name):
if event.button == 3:
self.mumbles_notify.close(widget.window)
else:
self.open_url(self.__url)
def open_url(self, url):
mime_type = gnomevfs.get_mime_type(url)
application = gnomevfs.mime_get_default_application(mime_type)
os.system(application[2] + ' "' + url + '" &')
インタフェースはリンク、作者、本文のみとしました。これをegg形式にビルドしてmumblesのpluginフォルダに置くと、上記のインタフェース呼び出しによりGrowlが表示されます。
MumblesPluginにはget_iconメソッドが用意されており、これにはアイコン名称を渡す事になります。実際にはplugin/iconsというフォルダにある名称のファイルが使用されるので、今回の仕組としてはgithubフィードのチェッカースクリプトでアイコンをplugin/iconsフォルダに格納させ、それを使用してプラグイン側が使用するという形になっています。プラグイン側でアイコンを取って来ても良いのですがアイコンをダウンロードしている最中はGrowlが固まってしまう為、今回の様な作りとなっています。
次にチェッカースクリプトですが以下の様なコードになります。
UPDATE_INTERVAL=1000
MAX_NOTIFICATIONS = 40
DEBUG = True
import os
import sys
import time
import getopt
import rfc822
import calendar
import urllib
import feedparser
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
import gobject
from BeautifulSoup import BeautifulSoup
from pit import Pit
GITHUB_DBUS_INTERFACE = 'com.github.DBus'
GITHUB_DBUS_PATH = '/com/github/DBus'
config = Pit.get('github.com', {'require': {
'user' : 'user id on github.com',
'token' : 'user token on github.com'
}})
class Usage(Exception):
def __init__(self, msg=None):
app = sys.argv[0]
if msg != 'help':
self.msg = app+': Invalid options. Try --help for usage details.'
else:
self.msg = app+": DBus notifications on new github messages.\n"
class GithubCheck(dbus.service.Object):
def __init__(self):
session_bus = dbus.SessionBus()
bus_name = dbus.service.BusName(GITHUB_DBUS_INTERFACE, bus=session_bus)
dbus.service.Object.__init__(self, bus_name, GITHUB_DBUS_PATH)
self.interval = UPDATE_INTERVAL
self.notifyLimit = MAX_NOTIFICATIONS
self.debug = DEBUG
self.lastCheck = None
self.minInterval = 60000
if self.interval < self.minInterval:
print "Warning: Cannot check github more often than once a minute! Using default of 1 minute."
self.interval = self.minInterval
self._check()
@dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='sss')
def Notify(self, link, author, text):
pass
@dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='i')
def NotifyNum(self, num):
pass
def _check(self):
if self.debug:
if self.lastCheck:
print "checking feed (newer than %s):" %(self.lastCheck)
else:
print "checking feed:"
try:
items = feedparser.parse("http://github.com/%s.private.atom/?token=%s" % (config['user'], config['token']))['entries']
except Exception, e:
items = []
if self.lastCheck:
lastCheck = calendar.timegm(time.localtime(calendar.timegm(rfc822.parsedate(self.lastCheck))))
for item in items:
if calendar.timegm(item.published_parsed) < lastCheck:
items.remove(item)
self.lastCheck = rfc822.formatdate()
num_notifications = len(items)
if num_notifications > MAX_NOTIFICATIONS:
if self.debug:
print "%s new entries\n" %(num_notifications)
self.NotifyNum(num_notifications)
elif num_notifications < 0:
if self.debug:
print "no new entries\n"
else:
for item in items:
path = os.path.join(os.path.expanduser('~'), '.mumbles', 'plugins', 'icons', 'github-%s' % item['author'])
if not os.path.exists(path):
html = urllib.urlopen('http://github.com/%s' % item['author']).read()
soup = BeautifulSoup(html)
img = soup.findAll('div', {'class':'identity'})[0].find('img')['src']
img = img.replace("?s=50&", "?s=30&");
urllib.urlretrieve(img, path)
self.Notify(item['link'], item['author'], item['title'])
time.sleep(6)
gobject.timeout_add(self.interval,self._check)
if __name__ == '__main__':
DBusGMainLoop(set_as_default=True)
try:
try:
opts, args = getopt.getopt(
sys.argv[1:], "hp", ["help"])
except getopt.GetoptError:
raise Usage()
for o, a in opts:
if o in ("-h", "--help"):
raise Usage('help')
else:
raise Usage()
except Usage, err:
print >> sys.stderr, err.msg
sys.exit(2)
t = GithubCheck()
try:
loop = gobject.MainLoop()
loop.run()
except KeyboardInterrupt:
print "githubcheck shut down..."
except Exception, ex:
print "Exception in githubcheck: %s" %(ex)
だらだらとしたコードですが、大体分かってもらえるかと思います。pitを使っているので初回起動のみユーザとトークンをエディタで入力する必要があります。トークンはGitHubのダッシュボードにあるRSSアイコンのリンク先URLに含まれています。
実行してしばらくすると以下の様な画面が表示されます。
これで快適になりました。
github上で全てのソースを公開しています。
mattn's mumbles-github-growler at master - GitHub
github growler using mumbles plugin and checker script.
http://github.com/mattn/mumbles-github-growler/tree/master
よろしければどうぞ。
require "rubygems"
require "dl/import"
module Lib_MSVCRT extend DL::Importable
LC_CTYPE = 2
dlload "msvcrt.dll"
extern "char* setlocale(int, char*)"
end
Lib_MSVCRT::setlocale(Lib_MSVCRT::LC_CTYPE, "")
2009/06/17
使っているreadline.dllは
ここから持って来ているんだけど、まずこれがマルチバイトありでビルドされていない。なのでマルチバイト文字を入力した後、バックスペース押すと1バイト単位でしか戻らない。
今日はそれを解決してみる。
readline-4.3-2-src.zipを持ってきて
# unzip readline-4.3-2-src.zip
# cd readline-4.3-2¥win32¥shlib
# copy config.h config.h.orig
# vim config.h
diff -u config.h.orig config.h
--- config.h.orig 2003-04-14 16:04:46.000000000 +0900
+++ config.h 2009-06-17 17:04:07.234375000 +0900
@@ -128,11 +128,9 @@
/* Define if you have the <varargs.h> header file. */
//#define HAVE_VARARGS_H 1
-/*
#define HAVE_WCTYPE_H 1
#define HAVE_WCHAR_H 1
#define HAVE_MBSRTOWCS 1
-*/
/* config.h.bot */
/* modify settings or make new ones based on what autoconf tells us. */
# copy config.h ..
# mingw32-make -f GNUmakefile
とするとreadline.dllが出来上がるのでそれをパスの通った場所に置く。古いreadline.dllをバックアップしておいて入れ替えるのもOK。
次に、
id:Constellationさんの記事にある以下のソースをlocale.cとして保存する。
#include <locale.h>
#include "ruby.h"
static VALUE mLocale;
static VALUE
locale_setlocale(obj)
{
setlocale(LC_CTYPE, "");
#ifdef LC_MESSAGES
setlocale(LC_MESSAGES, "");
#endif
return Qnil;
}
void
Init_locale()
{
mLocale = rb_define_module("Locale");
rb_define_module_function(mLocale, "setlocale", locale_setlocale, 0);
}
そしてコンパイル
# gcc -Ic:/ruby/lib/ruby/1.8/i386-mswin32 -shared -o locale.so locale.c libreadline.a c:/ruby/lib/msvcrt-ruby18.lib
ワーニングが出るけど気にしない
出来上がったlocale.soを
C:¥ruby¥lib¥ruby¥1.8¥locale.so
として配置し、ホームディレクトリ(HOME環境変数を設定していないならばUSERPROFILE変数の位置)に".irbrc"というファイルを作って以下の様に書く。
begin
require"locale"
Locale.setlocale
rescue
end
require 'rubygems'
require 'utility_belt'
require 'win32console'
"utility_belt"は
この記事を、win32consoleはエスケープシーケンスを色付けして表示して貰うために...
後はirbを起動すれば、マルチバイト文字でバックスペースしても正しく1文字消えてくれます。
Constellation++
これでようやく、
termtterでも正しく日本語が打てる。
なお、Constellationさんの記事にも書かれていますがtime.rb周りで弊害が出る可能性があるので、気を付けて。termtterの場合なら"~/termtter/config"にLocale.setlocaleを書いた方が良いかもしれない。
ついカッとなってやった。今も後悔してない。
昨日作った
GoogleReader.vimがだんだん安定して来て少し飽きたので
FastLadder版を作ってみました。
FastLadder.vim - vimscript for fastladder : vim online
This is vimscript for fastladder (http://fastladder.com/)
FastLadder.vim - vimscript for fastladder : vim online
開発はこの辺でやってます。
mattn's fastladder-vim at master - GitHub
This is vimscript for fastladder
http://github.com/mattn/fastladder-vim/tree/master
起動は
:FastLadder
で <c-n> と <c-p> で上下移動、リターンキーで閲覧、qで終了です。一応 <c-i> でブラウザ起動ですがキーは変えるかもしれません。
vimrcとかに g:fastladder_user と g:fastladder_passwd を設定してあれば curl だけで動作します。
画面はこんな感じ。
詳しくは"?"をタイプしてヘルプを見て下さい。
ちなみにg:fastladder_serverを"http://localhost"に設定してローカルでお楽しみ頂く事も出来ますが、
LivedoorReaderには対応していません。
mattn the vimmer!
2009/06/15
ついカッとなってやった。今も後悔してない。
Peepの改造としてはじめた
cpeepですが、今cpeepが実装している機能くらいならばvimで出来るやん...と思って勢いで書いてみました。
GoogleReader.vim - vimscript for googlereader : vim online
This is vimscript for googlereader (www.google.com/reader/)
http://www.vim.org/scripts/script.php?script_id=2678
開発はこの辺でやってます。
mattn's googlereader-vim at master - GitHub
This is vimscript for googlereader
http://github.com/mattn/googlereader-vim/tree/master
起動は
:GoogleReader
で <c-n> と <c-p> で上下移動、リターンキーで閲覧、qで終了です。一応 <c-i> でブラウザ起動ですがキーは変えるかもしれません。
vimrcとかに g:googlereader_email と g:googlereader_passwd を設定してあれば curl だけで動作します。
画面はこんな感じ。
作りかけなので、まだまだです。
えっ?cpeep?何でしたっけそれ...
正直言うと、これがキッカケだったりする。
はてなブックマーク - pekepekesamuraiのブックマーク
だれかEmacsのpeep-mode作ってくれないだろうか。と期待している
http://b.hatena.ne.jp/pekepekesamurai/20090615#bookmark-13902255
mattn the vimmer!
2009/06/14
先日書いた、「
コマンドラインからGoogle Readerが使えるPeepを試した」でLinuxでは快適になった。ただWindowsでは動かない。まずWindowsのpython2.6にはcursesのバイナリモジュールが含まれていなかった。これは「
Python2.6にはcursesのバイナリが含まれていないので作る」で解決したのだけど、元になっているpdcursesがWindows上ではまともに動かなくて、例えばマルチバイト文字を含んだ複数行をscrl(スクロール)すると、正しくマルチバイト文字の幅を取れていないのか下段の行が上段の行にゴミとなって重なったりした。
Peep自身を触っても良かったのだが修正非量も多い。常に残念だったのでCで書く事にした。APIの呼び出しまわりは
Peepを参考にさせて頂き、libxml2やcurl、pdcurses(linuxではncurses)で書き直す事にした。ただWindows上でcursesの動作が変なのは変わらない話なので、scrlを使わない事にした。どうやったかというとスクロールの度にclear()を呼んで全行書き直し。試してみた所、それ程操作感も悪く無かった。また色属性についてもACS_REVERSEを使うと幅計算が間違っているのか1行の幅で反転してくれなかったので、A_BOLDを使う事にした。現状、WindowsでもLinuxでも動作するようになっているけど、C++のstlをふんだんに使っててメモリ効率を何も考えてないソースになっています。
mattn's cpeep at master - GitHub
terminal front-end for Google Reader writen in C
http://github.com/mattn/cpeep/tree/master
まだまだ、全く作りかけなので閲覧しか出来ません。
j,kスクロールと、oで閲覧、qで閉じる、vでブラウザ(現状Windowsは通常使うブラウザ、それ意外はfirefox限定)です。
ちなみにPeepはHTMLをテキストに変換するのにw3mを内部で呼び出していたけど、cpeepでは自前でウンチャラカンチャラやってます。
まだまだ、これからです。
ちなみに今気付いたのですが、PeepのAuthorさんからPeepのcommit bitを付与して頂いている様です。ありがとうございます。何か協力出来る事があればcommitさせて頂きます。
追記
なんかの間違いだった様です。すみません。
2009/06/13
追記
パッチがいるのを忘れていました。
一番下にあります。
バッチファイル。実行すると直接バイナリ配置するので気をつけて。バッチの先頭にあるパスを環境に合わせ修正して下さい。
pythonのソースは
オフィシャルから「Python-2.6.2.tar.bz2」を、pdcursesも
オフィシャルから「pdcurs34.zip」をダウンロードし"C:¥TEMP¥"に展開しておきます。
@echo off
setlocal
set PYTHON26=c:\Python26
set PYSOURCE=c:\temp\Python-2.6.2
set PDSOURCE=c:\temp\pdcurses
if "%1" == "msvc" goto msvc
if "%1" == "gcc" goto gcc
goto usage
:msvc
set FLAGS=/nologo /LD -DHAVE_CURSES_RESIZE_TERM -DNCURSES_MOUSE_VERSION=2
echo building _curses.pyd
cl %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_cursesmodule.c /link %PYTHON26%\libs\python26.lib %PDSOURCE%\win32\pdcurses.lib %PDSOURCE%\win32\panel.lib /out:%PYTHON26%\lib\_curses.pyd
echo building _curses_panel.pyd
cl %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_curses_panel.c /link %PYTHON26%\libs\python26.lib %PDSOURCE%\win32\pdcurses.lib %PDSOURCE%\win32\panel.lib /out:%PYTHON26%\lib\curses\_curses_panel.pyd
goto end
:gcc
set FLAGS=-DHAVE_CURSES_RESIZE_TERM -DNCURSES_MOUSE_VERSION=2 -Wl,--enable-auto-import -Wl,--export-all -s -shared
echo building _curses.pyd
gcc %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_cursesmodule.c %PYTHON26%\libs\libpython26.a %PDSOURCE%\win32\pdcurses.a %PDSOURCE%\win32\panel.a -o %PYTHON26%\lib\curses\_curses.pyd
echo building _curses_panel.pyd
gcc %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_curses_panel.c %PYTHON26%\libs\libpython26.a %PDSOURCE%\win32\pdcurses.a %PDSOURCE%\win32\panel.a -o %PYTHON26%\lib\curses\_curses_panel.pyd
goto end
:usage
echo pycurses_build.bat [msvc or gcc]
:end
endlocal
以下ソースにあてるパッチです。Windows上のsetuptermは必ずエラーで返すコードになっているので呼ばないようにしています。
--- Modules/_cursesmodule.c.orig 2009-06-13 01:30:02.000000000 +0900
+++ Modules/_cursesmodule.c 2009-06-13 01:30:23.000000000 +0900
@@ -2036,6 +2036,7 @@
}
}
+#ifndef _WIN32
if (setupterm(termstr,fd,&err) == ERR) {
char* s = "setupterm: unknown error";
@@ -2048,6 +2049,7 @@
PyErr_SetString(PyCursesError,s);
return NULL;
}
+#endif
initialised_setupterm = TRUE;
2009/06/11
Tagtterをリリースして、初期は「CPU Quota」連発してしまい見苦しい状態でした。ごめんなさい。
さて、一時はどうなるかと思いましたが現在は「CPU Quota」も表示される事無く動いています。
今日はこの状況を乗り切る際に行った「Google App Engine」のパフォーマンスチューニングtipsを3つほどご紹介。
webapp.WSGIApplicationのdebugフラグはFalseにすべし
いきなり当たり前で申し訳ないですが、実はこのフラグがTrueかFalseかというだけで「CPU Quota」が出る頻度が極度に異なります。
実際に
同じ症状の方もいらっしゃる様なので、おそらく間違いありません。
また公開する様なシステムでは、パスワード等も管理される事になるかと思いますが、debug=Trueだとスタックトレースに変数の値が表示されてしまい無茶苦茶危険です。気を付けましょう。
なお、
Tagtterではユーザのパスワードは一切保存していません。
時間の掛かる既知な処理はキャッシュする
これはGoogle App Engineだけに言える話ではないですが、Tagtterではユーザの存在確認の為にtwitter.comへアクセスしています。
但し、タグが追加される度、voteされる度にこれを行ってしまうとtwitter.comにも負荷を掛ける事になります。
そこでTagtterではTagtter内に存在するユーザに対してはtwitter.comへのアクセスを行わないようになっています。
確かにGoogle App Engineの処理能力は良いのですが、いかんせん制限が厳しかったりします。余談になりますが現状Tagtterのデータ全件をXMLエクスポートする処理は「CPU Quota」で動きません。現在はデータの種別毎にバックアップしています。
Templateにはクラスオブジェクトは渡さない
Django Templateには、db.Modelのインスタンスを渡せて非常に便利なのですが、このDBインスタンスをTemplateの中から扱うと非常に遅くなります。
またdb.Modelに持ったgeneratorをTemplateから使うと更にパフォーマンスが落ちます。
例えばTagtterではユーザに対して付けられたタグをManyToManyで管理しており、モデルの一部を抜粋すると
class TagtterTag(db.Model):
name = db.StringProperty(required=True)
... snip ...
class TagtterMembership(db.Model):
user = db.ReferenceProperty(TagtterUser)
tag = db.ReferenceProperty(TagtterTag)
... ship ...
class TagtterUser(db.Model):
name = db.StringProperty(required=True)
... snip ...
def tags(self):
return (x.tag for x in self.tagttermembership_set)
この様なコードになるのですが、user.tagsをTemplate側から呼び出すとかなりパフォーマンスダウンになります。また、Google App EngineではCPU占有度により「CPU Quota」させる仕組みがあるのですが、実はこれはTemplate内の呼び出しにも適応される為
application → template → model procedure
という処理を行うと、1処理(1値参照)あたりのCPU使用回数が増える事になります。
つまり上記
Tagtterのtagsページでは全てのタグが表示されますがこれを
template_values = {
'tags' : TagtterTag.all(),
}
この様にしてしまうとTemplate側からModelのメソッドを呼び出す事になり、現状のデータ件数だと100%エラーが発生してしまいます。
Google App EngineではCPU占有状態が5秒程続くとエラーになる様で、このブリッジ状態だけでもパフォーマンスダウンに繋がるって事になります。
これを回避する為には、applicationにてTemplateで使う値を全て保持してしまう事が最も効果的でした。
つまり全てdictとして渡してあげるのです。変にTemplate側で「タグの数が0でない物を表示」とするのではなく
all_tags = []
for tag in TagtterTag.all():
if tag.size():
size = tag.size()
all_tags.append({
'name' : tag.name,
'title' : tag.title,
'size' : size,
'fontsize' : font_size(size),
})
... ship ...
template_values = {
... ship ...
'tags' : all_tags,
... ship ...
}
この様にTemplate側からdb.Modelメソッドを呼び出させないようにするのです。
これで、現状エラーも出なくなりました。要するに極力db.Modelインスタンスにアクセスしない様にするってのが味噌ですね。
こんな
品祖貧祖なパフォーマンスチューニングですが、皆さんの何かしらのお役に立てば幸いです。
他にもパフォーマンスチューニングの方法は幾らでもあるかと思います。Tagtterで言えばクライアント側の処理は殆どパフォーマンスチューニングされていないのが現状です。
不定期ですが時間を見付けてやって行きたいと思います。
何かご要望などあれば、
Feature Requestまで。
タイトルは半分釣りです。
はてなブックマーク - ブック・マークパンサー
prismがいいんじゃないかな
http://b.hatena.ne.jp/mattn/20090611#bookmark-13920618
Mozilla Prismでtumblr Dashboard専用ブラウザを作る - otsune's SnakeOil - subtech
(これは http://otsune.tumblr.com/post/121569443/mozilla-prism-tumblr-dashboard をはてな記法に修正した再録記事です)
http://subtech.g.hatena.ne.jp/otsune/20090611/prismTumblr
って事で作ってみた。
otsuneさんところに書いてある手順通りPrismをダウンロードして起動後にURL:「http://localhost:10010/」、Name:「Remedie」を入れただけ。
で、起動
Prism.exeが内部でxul-runnerを起動している部分のコードを見たけど、起動時に最大化する仕組みは無いみたい。でもナビゲーションなんかが無い分、可視領域が増えてウマー
2009/06/10
これは良いね。
MOONGIFT: » GoogleリーダーのCUIフロントエンド「Peep」:オープンソースを毎日紹介
Googleリーダーは出始めた頃はAjaxを使った高速な操作性が可能で、とても便利なRSSリーダーだった。だが今ではソーシャル的な機能も増えており「読む」という機能に特化していない。速度だってlivedoorリーダーの方が速いだろう。そんなGoogleリーダーを再度便利にしてくれる、それがPeepだ。
http://www.moongift.jp/2009/06/peep/
ryuji's peep at master - GitHub
This software is a TUI front-end for Google Reader
http://github.com/ryuji/peep/tree/master
待ってたよ。こんなの。
ブラウザを起動するとfirefoxになっちゃうので環境変数BROWSERにw3mを設定すれば端末内で暮らせてウマー!
2009/06/03
どっちかって言うと前向きな発言が好きかな。私は
Web進化論も読んでないし、私本人Web屋でもない。ただ後ろ向きな話が多くて滅入る。
実験的なサービスを出したり、模索したりするのは良い事。ただ新しくサービスを作ると既存ユーザから「あーだこーだ」言われる事もあるだろう。
そんな中最近感じているのは、はてなは今後既存ユーザを無視したサービスを作って行くべきなんじゃないかと。
物理的にも切り離して良いと思う。新しくサービス作って、はてなのアカウントは引き継がず。さらに言うなら、既存ユーザに告知もせず後から「へぇ、これはてなが作ってたんだ」なんてのも良いと思う。
ラボなんかで出てくるサービス見てても既存ユーザありきで盛り上がってるだけだし、おおよそ検討が付いてしまうサービスは数日で飽きてしまい、いつもの辛口ユーザに叩かれてブクマ炎上し、まだ使ってもなかったユーザに悪い印象垂れ流し状態になるまで半月も掛からないだろう。
じゃぁ何を作ればいいか。とても漠然として難しいお題だろうけど、コンテンツはサービスが作るんじゃなく、ユーザが作る物。それをどう引き出すかでサービスの良し悪しが決まるはず。
これはWebが進化したとしても変わらないよね?
例えば、馴れ合った既存ユーザを新しいサービスに持ち込んだとしても、新規サービス開始時に見られる「誰だこれ」「面白いじゃんこの人」とか全く無いだろうし、新規サービスにありがちな「サービス規約を無視したユーザと、自警団」みたいな物は既存ユーザは既にやってしまっていて、ある意味面白みがない。既存ユーザは新しいサービスが出たとしても、その中で一定の認知度を獲得してしまうと納得してしまって使わなくなってしまうんだよね。アカウントの早取り合戦や新規ユーザ同士でルール作って行ったりしてサービスの色が付いて行くんじゃないかな。新規のサービスで盛り上がるまでを体験した事のある人なら分かるよね?
別に自分の所で全部作らなくてもいいと思う。Googleだって色んなサービスは別の会社が作ってた物を買い取ってGoogleブランドにしてたりするし、「はてなだってやっちゃえ!」って思うな。なんていうかびっくり出来ないんだよな。
言ってしまうと既存ユーザに見きられる程度の行動範囲しか取れてないんじゃないかと。
Web屋でも無い私に言われる筋合いもないだろうし、内情知らない部外者が何を言ってるんだと言われるかもしれない。
だとしてもやっぱり思うのは、既存ユーザに見られながら作られる新規サービスには高揚感が無い。
んー。オチも結論も無い話になるけど、言って置きたいのは、はてないっそはてなアカウントを切り離したサービスを作ってみるのが面白いんじゃないかなと思うなー。
2009/06/01
まずは一言。
起動はえーーーーーーー!
気付いたらLinux版のバイナリ配布してるじゃないですか。
知らなかった。
Index of /buildbot/snapshots/chromium-rel-linux
http://build.chromium.org/buildbot/snapshots/chromium-rel-linux/
一番下にある最新のディレクトリからchrome-linux.zipをダウンロードして解凍。中にあるchromeを実行します。色々と入れていたせいか何も追加する事無く起動しました。
キター
まだ設定画面等はTODO contentだったりしますが、遊ぶには十分。
期待大ですね。これから遊びます。
2009/05/22
YQLを使うと色んなネットワークリソースをさもAPIを扱うかの様に操作でき、幾らでも新しい可能性が生まれて来ます。YQLには初期の状態でYahoo!で扱える色んなテーブル(flickrやdelicious等)が用意されており
show tables
と入力することでそのテーブル一覧が表示されます。
また右側のサイドバーにあるテーブル一覧で「Show Comminity Tables」をクリックするとユーザコミュニティが作成した便利なテーブルも扱う事が出来ます。
これらのComminity Tablesは
githubで開発されており、日々新しいデータテーブルが作成されています。
実はこのユーザテーブルは、ネットワーク上にXMLを配置する事が出来る人ならば誰でも作れます。
今日はこのユーザテーブルを自作する手順をご紹介します。
ユーザテーブルはユーザテーブル群を纏めるenvファイルと、実際のクエリを記述するファイルとで構成され、YQLからenvパラメータを使って参照する事が出来ます。例えばユーザコミュニティのテーブルであればenvファイルのURLを指定して以下の様にアクセスされます。
http://developer.yahoo.com/yql/console/?env=http://datatables.org/alltables.env
このenvパラメータで指定されたURLにアクセスしてみて頂けると分かると思いますが形式は以下の様なuse文の羅列になっています。
use 'http://www.datatables.org/amazon/amazon.ecs.xml' as amazon.ecs;
use 'http://www.datatables.org/auth/auth.basic.xml' as auth.basic;
use 'http://www.datatables.org/bitly/bit.ly.shorten.xml' as bit.ly.shorten;
use 'http://www.datatables.org/data/data.html.cssselect.xml' as data.html.cssselect;
このenvファイルと、実際のXMLファイルを用意すれば自分専用のAPIを作る事が出来ます。
今日はサンプルとして、
はてなのユーザプロフィールをスクレイピングしてXML(もしくはJSON)を返すAPIを作ってみたいと思います。
まず配置場所としては、
Google Page Creatorを使用しました。Google Page CreatorならばGoogleのアカウントさえ作れば誰でも無料でXMLを配置する事が出来ます。
はてなプロフィールを扱うAPIなのでファイル名は
hatena.profile.xml
とし、
alltables.envファイルで
use 'http://mattn.jp.googlepages.com/hatena.profile.xml' as hatena.profile;
としました。
次に実際のAPI部ですが、形式は以下の様になります。
xml version="1.0" encoding="UTF-8"
<table xmlns="http://query.yahooapis.com/v1/schema/table.xsd">
<meta>
<author>mattn</author>
</meta>
<bindings>
<select produces="XML" itemPath="">
<urls>
<url>http://www.hatena.ne.jp/{id}/profile</url>
</urls>
<inputs>
<key id="id" type="xs:string" paramType="variable" required="true"/>
</inputs>
<execute/>
</select>
</bindings>
</table>
YQLではXMLの中に書かれたexecuteノードにjavascriptを記述する事ができ、response.objectというオブジェクト変数に値を格納する事で独自のフォーマットを作り出す事が出来ます。
またそこで使うことが出来るjavascriptにはE4X(ECMAScript for XML)が採用されており、XMLを扱いやすくなっています。
今回のはてなプロフィールAPIでは
http://www.hatena.ne.jp/mattn/で表示されるページを組み込み関数
y.query
を使ってスクレイピングします。
以下javascript部のコード
var contents = y.query("select * from html where url=@url and xpath=@xpath", {
url: "http://www.hatena.ne.jp/" + id + "/",
xpath: "'//dl[@class=\"profile\"]/dd'"
});
var shortdesc = contents.results..div.(@['id']=="hatena-body")..dd[2];
var longdesc = contents.results..div.(@['id']=="hatena-body")..dd[3];
var profile =
<profile>
<user>{contents.results..div.(@['id']=="hatena-body")..dd[0].p.text().toString()}</user>
<nickname>{contents.results..div.(@['id']=="hatena-body")..dd[1].p.text().toString()}</nickname>
<shortdesc>{shortdesc ? shortdesc.*.toString() : ''}</shortdesc>
<longdesc>{longdesc ? longdesc.*.toString() : ''}</longdesc>
<medals/>
<addresses/>
<services/>
</profile>;
for each(var n in contents.results..div.(@['class']=="medals").img) {
profile.medals.medial += <medal>{n.@['title'].toString()}</medal>;
}
for each(var n in contents.results..table.(@['class']=="profile addresslist")..tr) {
profile.addresses.address +=
<address>
<name>{n.th.*.text().toString()}</name>
<value>{n.td.*.text().toString()}</value>
</address>;
}
for each(var n in contents.results..ul.(@['class']=="hatena-fotolife floatlist")[0]..a) {
profile.services.service +=
<service>
<name>{n.img.@['title'].toString()}</name>
<url>{n.@['href'].toString()}</url>
</service>
}
response.object = profile;
y.queryにはYQLから使うことが出来るselect文を使用する事ができ、xpathを使って絞り込む事も出来ます。ただ今回は一つのAPIで
- アカウントID
- ニックネーム
- 短い紹介
- 自己紹介
- アドレス情報
- 使っているサービス一覧
- 市民メダル情報
を一度に扱うので、個別にxpathでクエリを投げるのでは無く、一部を除いたページ全体を取得しておいて、E4XのDOM操作でxpathライクな処理を書いています。
例えばE4Xでは、DOMオブジェクトの階層スキップや条件指定検索を
contents.results..div.(@['id']=="hatena-body")..dd[2];
等といった書き方をする事が出来ます。つまり無理にxpathで絞り込む必要は無いという事です。
おそらくサーバ側ではrhinoあたりを使っているのかもしれませんね。
XML全体のコードは以下の様になりました。
xml version="1.0" encoding="UTF-8"
<table xmlns="http://query.yahooapis.com/v1/schema/table.xsd">
<meta>
<author>mattn</author>
</meta>
<bindings>
<select produces="XML" itemPath="">
<urls>
<url>http://www.hatena.ne.jp/{id}/profile</url>
</urls>
<inputs>
<key id="id" type="xs:string" paramType="variable" required="true"/>
</inputs>
<execute><![CDATA[
var contents = y.query("select * from html where url=@url and xpath=@xpath", {
url: "http://www.hatena.ne.jp/" + id + "/",
xpath: "'//dl[@class=\"profile\"]/dd'"
});
var shortdesc = contents.results..div.(@['id']=="hatena-body")..dd[2];
var longdesc = contents.results..div.(@['id']=="hatena-body")..dd[3];
var profile =
<profile>
<user>{contents.results..div.(@['id']=="hatena-body")..dd[0].p.text().toString()}</user>
<nickname>{contents.results..div.(@['id']=="hatena-body")..dd[1].p.text().toString()}</nickname>
<shortdesc>{shortdesc ? shortdesc.*.toString() : ''}</shortdesc>
<longdesc>{longdesc ? longdesc.*.toString() : ''}</longdesc>
<medals/>
<addresses/>
<services/>
</profile>;
for each(var n in contents.results..div.(@['class']=="medals").img) {
profile.medals.medial += <medal>{n.@['title'].toString()}</medal>;
}
for each(var n in contents.results..table.(@['class']=="profile addresslist")..tr) {
profile.addresses.address +=
<address>
<name>{n.th.*.text().toString()}</name>
<value>{n.td.*.text().toString()}</value>
</address>;
}
for each(var n in contents.results..ul.(@['class']=="hatena-fotolife floatlist")[0]..a) {
profile.services.service +=
<service>
<name>{n.img.@['title'].toString()}</name>
<url>{n.@['href'].toString()}</url>
</service>
}
response.object = profile;
]]></execute>
</select>
</bindings>
</table>
あとはこれをYQLから指定してやれば良い事になります。
実際には
ここで試す事が出来ます。
さてこれでAPIが出来上がったので、APIを使ったサンプルを書いてみましょう。
$(function() {
$('#hatena-profile').submit(function() {
$('#hatena-profile-ajaxloader').attr('src', 'http://mattn.kaoriya.net/images/ajax-loader.gif').show();
var query = "select * from hatena.profile where id = '" + $('#hatena-id').val() + "'";
var envurl = "http://mattn.jp.googlepages.com/alltables.env";
$.getJSON('http://query.yahooapis.com/v1/public/yql?q=' + encodeURIComponent(query) + '&format=json&env=' + encodeURIComponent(envurl) + '&callback=?', function(data) {
$('#hatena-profile-ajaxloader').hide();
$('#hatena-profile-user').text(data.query.results.profile.user);
$('#hatena-profile-nickname').text(data.query.results.profile.nickname);
$('#hatena-profile-medals').html(data.query.results.profile.medals.medal.join('<br />'));
$.each(data.query.results.profile.addresses.address, function() {
var html = $('#hatena-profile-addresses').html();
$('#hatena-profile-addresses').html(html + this.name + ':' + this.value + '<br />');
});
$.each(data.query.results.profile.services.service, function() {
var html = $('#hatena-profile-services').html();
$('#hatena-profile-services').html(html + this.name + ':' + this.url + '<br />');
});
$('#hatena-profile-info').show('slow');
});
return false;
});
});
簡単にプロフィール情報を表示するだけの物です。
以下実行例です。
続きを読む...
2009/04/16
はてなブックマークをdeliciousに同期する場合、どうやってますか?
- 同時ポストツール?
- Plagger?
- まさか、手作業?
同時ポストツールの場合、確かにその場で同時にポスト出来て便利ですね。でも携帯ではてなブックマークから登録した場合、同期されませんよね。
Plaggerだと、cronで動き続けるPCが要りますよね。家に24時間稼動可能なPC無いよ!なんて人いるかもしれません。
手作業?問題外!
先日、Google App Engineに
cronが導入されました。
Google App Engine Blog: Seriously this time, the new language on App Engine: Java™
Cron support: schedule tasks like report generation or DB clean-up at an interval of your choosing.
http://googleappengine.blogspot.com/2009/04/seriously-this-time-new-language-on-app.html
つまり...
- cronでイベント発生
- はてなブックマークからRSS取得
- 内部のデータベースから既にポスト済みでないか確認
- deliciousにポストする
- モテモテ
って事で作ってみました。
mattn's hatenabookmark-meets-delicious at master - GitHub
post hatenabookmark to delicious periodically using google app engine
http://github.com/mattn/hatenabookmark-meets-delicious/tree/master
Google App Engineのアカウントを作成後アプリケーションを作成します。
app.yamlの
application: your_app_name
version: 1
runtime: python
api_version: 1
handlers:
- url: /tasks/sbm-sync
script: sbm-sync.py
- url: /
script: sbm-sync.py
- url: /favicon.ico
static_files: static/images/favicon.ico
upload: static/images/favicon.ico
mime_type: image/x-icon
- url: /static
static_dir: static
your_app_nameの部分を作成したアプリケーションIDに書き換えて下さい。次にmy-config.yaml.exampleをmy-config.yamlにコピーし
hatena_user : your_hatena_user
delicious_user : your_delicious_user
delicious_pass : your_delicious_password
timezone: JST
timeoffset: 9
あなたの、はてなブックマークIDとdeliciousユーザID/パスワードに書き換えて下さい。
あとはサーバにアップロードすれば10分毎に、はてなブックマークのRSSを取得してdeliciousに同期されます。
よろしければ使ってみて下さい。ソースはgithubにあるのでpatch welcomeです。
SQLite便利!
SQLite3におけるREGEXP演算子 - anon_193の日記
SQLite では、load_extension 関数を用いて、外部の拡張モジュールをロードすることが出来る。拡張モジュールは、いわばユーザ関数ライブラリで、SQLite3 ODBC Driver には標準で BLOB二次元マッピング拡張(sqlite3_mod_blobtoxy.dll)、外部データ取込・出力拡張(sqlite3_mod_impexp.dll)、全文検索拡張(sqlite3_mod_fts3.dll) が付属している。これらと同様にして、正規表現マッチングを行う regexp ユーザ関数を持つ拡張モジュールを制作し、ロードすれば、お目当ての REGEXP 演算子が使えるわけだ。
http://d.hatena.ne.jp/anon_193/20090114/1231935112
sqlite3_mod_regexp.cxx
#include <boost/regex.hpp>
#include <sqlite3ext.h>
extern "C" {
SQLITE_EXTENSION_INIT1
static void regexp_func(sqlite3_context *context, int argc, sqlite3_value **argv) {
if (argc >= 2) {
const char *target = (const char *)sqlite3_value_text(argv[1]);
const char *pattern = (const char *)sqlite3_value_text(argv[0]);
try {
boost::regex ereg(pattern, boost::regex_constants::perl);
sqlite3_result_int(context, boost::regex_search(target, ereg));
} catch (boost::regex_error &e) {
sqlite3_result_error(context, e.what(), 0);
}
}
}
__declspec(dllexport) int sqlite3_extension_init(sqlite3 *db, char **errmsg, const sqlite3_api_routines *api) {
SQLITE_EXTENSION_INIT2(api);
return sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8, (void*)db, regexp_func, NULL, NULL);
}
}
VCでコンパイルしました。
# cl /EHsc -Isrc -I. -I "c:\boost_1_35_0" sqlite3_mod_regexp.cxx /LD "C:\boost_1_35_0\libs\regex\build\vc80\libboost_regex-vc80-mt-s-1_35.lib"
サンプルデータ
foo.sql
CREATE TABLE foo(id integer primary key, value text);
INSERT INTO "foo" VALUES(1,'abc');
INSERT INTO "foo" VALUES(2,'def');
INSERT INTO "foo" VALUES(3,'あいうえお');
INSERT INTO "foo" VALUES(4,'かきくけこ');
INSERT INTO "foo" VALUES(5,'さしすせそ');
utf-8で保存して下さい
# cat foo.sql | sqlite3 foo.db
そしてPerlのコード
use strict;
use warnings;
use utf8;
use YAML;
use DBIx::Simple;
my $db = DBIx::Simple->connect("dbi:SQLite:dbname=c:/foo.db", "", "")
or die DBIx::Simple->error;
$db->func(1, "enable_load_extension");
my $result = $db->query("select load_extension('/sqlite3_mod_regexp.dll')")
or die DBIx::Simple->error;
warn Dump $db->query("select * from foo where value regexp '^[あか]'")->hashes;
dbのパスとdllのパスは指定して下さい
実行すると...
---
id: 3
value: あいうえお
---
id: 4
value: かきくけこ
スゲーーーー便利!
2009/04/14
JSONって大体のサーバレスポンスだと、改行もなくエディタで開いてちまちま改行入れて内容確認...なんて事やってる人もいるかと思います。
結構前に作った物ですが、もしかしたら便利と思ってくれる人もいるんじゃないか...と思ったの晒しておきます。
mattn's gtkjsonviewer at master - GitHub
A simple json viewer written in GTK.
http://github.com/mattn/gtkjsonviewer/tree/master
早い話が、JSONをGUIで見ようという話です。中身は簡単なpythonのコードで、GTKを使っています。
# curl http://twitter.com/public_timeline/?format=json | gtkjsonview.py
の様に使います。Windowsの場合はpython.exeがちゃんと標準入力を読み取ってくれないので
# curl http://twitter.com/statuses/user_timeline/mattn_jp.json | python gtkjsonview.py
とする必要があります。起動すると以下の様な画面が表示されます。
不正なJSON(例えば最終カンマあり)等の場合はパースエラーが出ます。
2009/04/10
少し前からですがリポジトリをgoogle codeか
githubに移しています。
その際少しだけ触って、以前まではデータ更新時にフローティングウィンドウを出し、そこにローディングアニメーションを表示していましたが、ローディング中に親画面を移動されるとカッコ悪いのでボタンの横でクルクル回る様に修正しました。
気が向いたら機能アップするかもしれません。
以上、GtkTwitter近況でした。
jQueryでマウスホバーでリンクに影をつけるカッコいいプラグインを見つけました。
nakajima's jquery-glow at master - GitHub
Make your elements glow. Ooooh.
http://github.com/nakajima/jquery-glow/tree/master
以下実行例
続きを読む...
2009/04/08
きたー!
Google App Engine Blog: Seriously this time, the new language on App Engine: Java™
Today, we're very excited to announce the availability of a new programming language for Google App Engine. Please welcome the Java runtime!
http://googleappengine.blogspot.com/2009/04/seriously-this-time-new-language-on-app.html
で、最初はEarly Lockというサーバを更新出来ない状態だったけど何故かさっき「Lock解除したよ」のメールが来た。
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
Hello JSP!<br />
</body>
</html>
ここで動いてます。
ちなみに、上の記事によるとGAEでCronが
動かせる様になった。こっちの方が嬉しい人いるんじゃないかな?
2009/04/06
Growl For Windowsのコミットビットを貰ったので、国際化の為のresxファイルを書いた。
次のリリースあたりには降りてくるはず。
その他の活動としてはC言語で書いたGNTP送信プログラムgntp-sendを書いた。
mattn's gntp-send at master - GitHub
あと、perlモジュールGNTP::Growlは暗号化送信出来る様になった。現状AESとDESに対応しています。3DESはまだ未対応です。
mattn's perl-gntp-growl at master - GitHub
ちなみに、
Growlのソースコードにはブランチですが
GNTPなコードが入っているっぽいので、もしかしたら上記のツール類でMacにGrowl出来る日が来るのではないか...と期待しています。
追記
vimperatorrcに追加するスクリプトを少し修正する必要があります。詳しくは
ココを参照。
こうするといいよ。
add to your .vimperatorrc
javascript if (typeof hBookmark) liberator.loadScript('chrome://hatenabookmark/content/vimperator/plugin/hatenabookmark.js', {__proto__: this});
javascript if (typeof hBookmark != 'undefined') liberator.loadScript('chrome://hatenabookmark/content/vimperator/plugin/hatenabookmark.js', {__proto__: this});
and try
:hbsearch はてな
:hbtabsearch はてな
id:hazime2914さんに間違いを指摘して頂きました。正しくはhbtagsearchではなくhbtabsearchです。
はてなブックマーク Firefox 拡張のベータテストを開始します - はてなブックマーク日記 - 機能変更、お知らせなど
2009/04/03
こうすればいいのか...
デザイン設定画面のスタイルシートに
.hatena-star-star-image { background-image: url('http://mattn.kaoriya.net/images/unko.gif'); }
こんな感じになります。
2009/03/31
貧乏なので買えません。
カラースターショップ - はてな
ソース
(function() {
function $X (exp, context, type ) {
if (typeof context == "function") {
type = context;
context = null;
}
if (!context) context = document;
exp = (context.ownerDocument || context).createExpression(exp, function (prefix) {
return document.createNSResolver((context.ownerDocument === null ? context
: context.ownerDocument).documentElement)
.lookupNamespaceURI(prefix) ||
document.contentType === "application/xhtml+xml" ? "http://www.w3.org/1999/xhtml" : "";
});
switch (type) {
case String: return exp.evaluate(context, XPathResult.STRING_TYPE, null).stringValue;
case Number: return exp.evaluate(context, XPathResult.NUMBER_TYPE, null).numberValue;
case Boolean: return exp.evaluate(context, XPathResult.BOOLEAN_TYPE, null).booleanValue;
case Array:
var result = exp.evaluate(context, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (var ret = [], i = 0, len = result.snapshotLength; i < len; i++) {
ret.push(result.snapshotItem(i));
}
return ret;
case undefined:
var result = exp.evaluate(context, XPathResult.ANY_TYPE, null);
switch (result.resultType) {
case XPathResult.STRING_TYPE : return result.stringValue;
case XPathResult.NUMBER_TYPE : return result.numberValue;
case XPathResult.BOOLEAN_TYPE: return result.booleanValue;
case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
var ret = [], i = null;
while ((i = result.iterateNext())) ret.push(i);
return ret;
}
return null;
default: throw(TypeError("$X: specified type is not valid type."));
}
}
var stars = ['star-red.gif', 'star-green.gif', 'star.gif#', 'star-blue.gif'];
setInterval(function() {
$X('//img[@src="http://s.hatena.ne.jp/images/star.gif"]').forEach(function(n) {
n.src = "http://s.hatena.ne.jp/images/" + stars[parseInt(Math.random() * 4)];
});
}, 1000);
})();
インストール
poor-mans-hatena-color-star.user.js
2009/03/23
書いた。
use strict;
use warnings;
use lib qw/lib/;
use GNTP::Growl;
my $growl = GNTP::Growl->new(AppName => "my perl app");
$growl->register([
{ Name => "foo", },
{ Name => "bar", },
]);
$growl->notify(
Event => "foo",
Title => "おうっふー おうっふー",
Message => "大事な事なので\n2回言いました",
Icon => "http://mattn.kaoriya.net/images/logo.png",
);
こんなソースで
こんな物が動く。
開発はこの辺で...
mattn's perl-gntp-growl at master - GitHub
TopHatenarを
やってみた。
gigazineもdqnplusも遠いですなぁ。。。
ちなみにdankogaiさんも遥か彼方へ...
購読者数・ブックマーク数相関グラフ
購読者数推移グラフ
ブックマーク数推移グラフ
2009/03/18
twitterのOAuthベータが開始されたので、さっそく軽量Web Application Framework「MENTA」でOAuthする物を作ってみた。
構成も簡単で
│ .htaccess
│ menta.cgi
├─app
│ ├─controller
│ │ callback.pl
│ │ request.pl
│ ├─data
│ └─static
├─bin
├─lib
└─plugins
session.pl
こんな感じ。
トップページからrequest.plに飛び、OAuthのcallbackとしてcallback.plがキックされる。
要のrequest.plは以下の様な感じ
use MENTA::Controller;
use URI;
use OAuth::Lite::Consumer;
sub run {
my $consumer = OAuth::Lite::Consumer->new(
consumer_key => config()->{application}->{consumer_key},
consumer_secret => config()->{application}->{consumer_secret},
site => 'http://twitter.com/',
request_token_path => 'http://twitter.com/oauth/request_token',
access_token_path => 'http://twitter.com/oauth/access_token',
authorize_path => 'http://twitter.com/oauth/authorize',
);
my $request_token = $consumer->get_request_token();
my $uri = URI->new( $consumer->{authorize_path} );
$uri->query(
$consumer->gen_auth_query(
"GET", "http://twitter.com/", $request_token
)
);
redirect( $uri->as_string );
}
そしてcallback.plはこんな感じ
use utf8;
use MENTA::Controller;
use OAuth::Lite::Consumer;
use JSON;
sub run {
my $consumer = OAuth::Lite::Consumer->new(
consumer_key => config()->{application}->{consumer_key},
consumer_secret => config()->{application}->{consumer_secret},
site => 'http://twitter.com/',
request_token_path => 'http://twitter.com/oauth/request_token',
access_token_path => 'http://twitter.com/oauth/access_token',
authorize_path => 'http://twitter.com/oauth/authorize',
);
my $access_token = $consumer->get_access_token( token => param('oauth_token') );
my $res = $consumer->request(
method => 'POST',
url => q{http://twitter.com/statuses/update.json},
token => $access_token,
params =>
{ status => 'おうっふー', token => $access_token },
);
if ( $res->is_success ) {
my $status = from_json( $res->content );
redirect( "http://twitter.com/"
. $status->{user}->{screen_name}
. "/status/"
. $status->{id} );
}
else {
redirect("http://twitter.com/");
}
}
ちなみにこのアプリケーションを"Accept"にするとtwitteのステータスラインに「おうっふー」がポストされます。自分で作る方は
twitterのアプリケーション登録画面から得られるconsumer_keyとconsumer_secretをmenta.cgiのapplication項目に設定するのをお忘れなく。
簡単ですね!
vimはファイルを開く際、iconvを使用してfileencodingsの順に変換を試し、正しく変換出来た物をfileencodingとして使用する仕組みになっています。
つまり化けたファイルだと、どのfileencodingsにもマッチせずencodingで指定されたエンコーディングでファイルが開かれステータス行に「変換失敗」と表示される事になります。
しかしながらそのファイルのエンコーディングが分かっている場合もあります。vimでは++enc
オプションを指定する事で指定のfileencodingでファイルを開く事が出来ます。
:e ++enc=utf-8 foo.txt
この場合、vimはiconvで変換出来なかった文字を?
という文字で置き換えてしまいます。
# echo あいうえお | iconv -f char -t utf-8 > utf8.txt
# echo あいうえお | iconv -f char -t cp932 > sjis.txt
# cat utf8.txt sjis.txt > bad.txt
# vim
cp932の場合は文字集合の区画が広い為、vimではutf-8が構成するバイト列をcp932の範囲内として開いてしまいます。
:e ++enc=cp932 bad.txt
縺ゅ>縺?縺医♀
あいうえお
またutf-8の場合はcp932の区画は範囲外になります。つまり
:e ++enc=utf-8 bad.txt
あいうえお
??????????
この様に?
に変換されてしまう事になります。ステータス行には何行目で変換が失敗したかを警告するメッセージも表示されますが、元のテキストは失われます。
こういったファイルを開くのにフィルタを使用する人もいるかもしれません。
:%!nkf -Ws
これでも良いのですが、nkfが変換出来ない文字は削られてしまいます。
実はvimの:eコマンドには++bad
オプションという物があり、この?
の挙動を変更出来る様になっています。
*++bad*
The argument of "++bad=" specifies what happens with characters that can't be
converted and illegal bytes. It can be one of three things:
++bad=X A single-byte character that replaces each bad character.
++bad=keep Keep bad characters without conversion. Note that this may
result in illegal bytes in your text!
++bad=drop Remove the bad characters.
The default is like "++bad=?": Replace each bad character with a question
mark.
例えば++badオプションに%
を指定すると
:e ++enc=utf-8 bad.txt
あいうえお
%%%%%%%%%%
となるのです。現在の所1文字の場合はその文字で置き換える様になっています。また++badオプションにkeep
を指定すると、何も変換せずに開いてくれます。
:e ++enc=utf-8 ++bad=keep bad.txt
あいうえお
<82><a0><82><a2><82><a4><82><a8><82><a6>
さらにdrop
を指定する事で不正な文字を削除してファイルを開く事も出来ます。
あいうえお
vimでファイルが開けなくなってしまった方、一度++badオプションを試してみては如何でしょうか?
2009/03/13
2009/03/11
解説は...いらんでしょ。
母艦は「dan the shell」。
!変数宣言が必要
ルートメニューとはメインメニュー
ファイルメニューとはメニュー
実行とは、メニュー。実行のテキストは「実行」
区切り1とは、メニュー。区切り1のテキストは「-」
閉じるとは、メニュー。閉じるのテキストは「閉じる」
「実行
区切り1
閉じる」を反復
「ファイルメニューの追加({それ}を)」をナデシコする
「ファイルメニュー」を反復
「ルートメニューの追加({それ}を)」をナデシコする
入力メモとはメモ。
入力メモについて
X=0
Y=0
W=母艦のクライアントW
H=母艦のクライアントHを2で割る
結果メモとはメモ。
結果メモについて
X=0
Y=母艦のクライアントHを2で割る
W=母艦のクライアントW
H=母艦のクライアントHを2で割る
母艦のサイズ変更した時は~
入力メモについて
X=0
Y=0
W=母艦のクライアントW
H=母艦のクライアントHを2で割る
結果メモについて
X=0
Y=母艦のクライアントHを2で割る
W=母艦のクライアントW
H=母艦のクライアントHを2で割る
閉じるのクリックした時は~
「閉じますか?」と二択
もし、それがはいならば、終わり
実行のクリックした時は~
URLは「http://api.dan.co.jp/perleval.cgi?c=callback&s=」
それは入力メモのテキスト
それをURLエンコード
「{改行}」を「%0A」に置換
URLにそれを追加
URLをHTTPデータ取得して、結果に代入
結果を「^callback\((.*)\);$」で正規表現マッチ
結果は抽出文字列[0]
結果をJSONデコード
結果はそれ
結果メモのテキストは結果@「result」
参考文献:
404 Blog Not Found:Ajax - perlを実行するAPI
readline使ってコマンドライン提供、curlで通信、結果をjson-cでパースまで作った。燃え尽きた。
追記
shebangから使えるようにした。
#!/usr/bin/dansh
以下コード
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <io.h>
namespace json {
#include "json.h"
}
#define READLINE_STATIC
#include <readline/readline.h>
#include <curl/curl.h>
#define API_URL "http:
typedef struct {
char* data;
size_t size;
} MEMFILE;
MEMFILE*
memfopen() {
MEMFILE* mf = (MEMFILE*) malloc(sizeof(MEMFILE));
mf->data = NULL;
mf->size = 0;
return mf;
}
void
memfclose(MEMFILE* mf) {
if (mf->data) free(mf->data);
free(mf);
}
size_t
memfwrite(char* ptr, size_t size, size_t nmemb, void* stream) {
MEMFILE* mf = (MEMFILE*) stream;
int block = size * nmemb;
if (!mf->data)
mf->data = (char*) malloc(block);
else
mf->data = (char*) realloc(mf->data, mf->size + block);
if (mf->data) {
memcpy(mf->data + mf->size, ptr, block);
mf->size += block;
}
return block;
}
char*
memfstrdup(MEMFILE* mf) {
char* buf = (char*) malloc(mf->size + 1);
memcpy(buf, mf->data, mf->size);
buf[mf->size] = 0;
return buf;
}
char*
url_encode_alloc(const char* str, int force_encode) {
const char* hex = "0123456789abcdef";
char* buf = NULL;
unsigned char* pbuf = NULL;
int len = 0;
if (!str) return NULL;
len = strlen(str)*3;
buf = (char*) malloc(len+1);
memset(buf, 0, len+1);
pbuf = (unsigned char*)buf;
while(*str) {
unsigned char c = (unsigned char)*str;
if (c == ' ')
*pbuf++ = '+';
else if (c & 0x80 || force_encode) {
*pbuf++ = '%';
*pbuf++ = hex[c >> 4];
*pbuf++ = hex[c & 0x0f];
} else
*pbuf++ = c;
str++;
}
return buf;
}
void do_dan(const char* line) {
char* source = url_encode_alloc(line, TRUE);
char* url = (char*) malloc(strlen(API_URL) + strlen(source) + 1);
strcpy(url, API_URL);
strcat(url, source);
MEMFILE* mf = memfopen();
CURL* curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, memfwrite);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, mf);
CURLcode res = curl_easy_perform(curl);
free(url);
if (res == CURLE_OK) {
char* data = memfstrdup(mf);
memfclose(mf);
char* ptr;
ptr = strrchr(data, ')');
if (ptr) *ptr = 0;
ptr = strchr(data, '(');
if (ptr) *ptr++ = 0;
else ptr = data;
json::json_object *obj = json::json_tokener_parse(ptr);
if (!is_error(obj)) {
json::json_object *result = json::json_object_object_get(obj, "result");
if (!is_error(result)) {
printf("%s\n", json::json_object_to_json_string(result));
json_object_put(result);
}
}
free(data);
}
curl_easy_cleanup(curl);
}
int main(int argc, char **argv)
{
char* line = NULL;
if (argc == 2) {
FILE* fp = fopen(argv[1], "rb");
if (!fp) {
perror("can't open file");
exit(-1);
}
char buf[BUFSIZ];
while (fgets(buf, sizeof(buf), fp)) {
if (!line) {
if (strncmp(buf, "#!", 2))
line = strdup(buf);
} else {
line = (char*) realloc(line, strlen(line) + strlen(buf) + 1);
strcat(line, buf);
}
}
fclose(fp);
do_dan(line);
free(line);
}
if (!isatty(fileno(stdin))) {
char buf[BUFSIZ];
while (fgets(buf, sizeof(buf), stdin)) {
if (!line) {
if (strncmp(buf, "#!", 2))
line = strdup(buf);
} else {
line = (char*) realloc(line, strlen(line) + strlen(buf) + 1);
strcat(line, buf);
}
}
do_dan(line);
free(line);
} else {
while (line = readline("dan> ")) {
do_dan(line);
free(line);
}
}
return 0;
}
参考文献:
404 Blog Not Found:Ajax - perlを実行するAPI
2009/03/05
結果から言うと実に使いにくい!
PyAWS - A Python wrapper for Amazon Web Service
PyAWS is a Python wrapper for the latest Amazon Web Service. It is designed to pave the way for Python developers to interactivate AWS. This project is forked from the code base of pyamazon. The Amazone E-Commerce Services is supported.
http://pyaws.sourceforge.net/
配列なら配列で、存在したり存在しない場合があるプロパティならばそれ用のアクセサを作ってほしい...
Djangoテンプレートで配列かどうかを判断してループで回してってのはつらいです。
あと、Django Template Engineの最新版でないと
{% for key, value in values %}
こういう書き方が出来ないので、結局明示的な名前の付いたdictに作り変える必要があった。まぁこれはpyawsのせいではないけれど。
aws.py
import os
import cgi
import wsgiref.handlers
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from google.appengine.api import urlfetch
import logging
import yaml
from pyaws import ecs
def to_dict_array(array):
ret = []
for item in array:
ret.append({'key': item.keys()[0], 'value':item.values()[0] })
return ret
class MainPage(webapp.RequestHandler):
config = yaml.safe_load(open(os.path.join(os.path.dirname(__file__), 'aws.yaml'), 'r'))
def get(self):
kind = self.request.get('kind')
keyword = self.request.get('keyword')
template_values = {}
template_values = {
'asoid' : self.config['asoid'],
'kinds' : to_dict_array(self.config['kinds']),
'kind' : kind,
'keyword' : keyword,
'books' : [],
}
if keyword:
asoid = self.config['asoid']
devkey = self.config['devkey']
kinds = self.config['kinds']
ecs.setLicenseKey(devkey)
ecs.setLocale(self.config['locale'])
books = ecs.ItemSearch(keyword, SearchIndex=kind, ResponseGroup='Medium,Offers')
count = 0
max = 5
for book in books:
if 'Author' in book.__dict__ and not isinstance(book.Author, list):
book.Author = [book.Author]
if 'Offer' in book.Offers.__dict__:
if isinstance(book.Offers.Offer, list):
book.Availability = book.Offers.Offer[0].OfferListing.Availability
elif 'Availability' in book.Offers.Offer.OfferListing.__dict__:
book.Availability = book.Offers.Offer.OfferListing.Availability
template_values['books'].append(book)
count += 1
if count > max: break
path = os.path.join(os.path.dirname(__file__), 'aws.html')
self.response.out.write(template.render(path, template_values))
def main():
application = webapp.WSGIApplication([('/pyaws/', MainPage)], debug=True)
wsgiref.handlers.CGIHandler().run(application)
if __name__ == '__main__':
main()
aws.html
<html>
<head>
<title>AWS商品検索</title>
<style tyle="text/css">
body {
font-family: 'メイリオ', 'Osaka'
}
#content {
margin-left: 50px;
}
#error {
color: red;
}
.awsxom {
background: #eeeeee;
padding: 0.5em;
}
</style>
</head>
<body>
<h1>AWS商品検索</h1><img src="http://b.hatena.ne.jp/entry/image/http://mattn.appspot.com/pyaws/" title="はてなブックマーク" />
<div id="content">
<p align="right"><a href="/">目次</a></p>
<form method="get">
<label for="kind">種類</label>
<select id="kind" name="kind" value="{{ kind }}">
{% for item in kinds %}<option value="{{ item.key }}"{% ifequal item.key kind %} selected="selected"{% endifequal %}>{{ item.value }}</option>
{% endfor %}
</select>
<label for="keyword">キーワード</label><input id="keyword" name="keyword" type="text" value="{{ keyword|escape }}" />
<input type="submit" />
</form>
<hr />
{% for book in books %}
<div class="awsxom">
<a href="http://www.amazon.co.jp/exec/obidos/ASIN/{{ book.ASIN }}/ref=nosim/{{ asoid }}">
<img src="{{ book.SmallImage.URL }}" align="left" hspace="5" border="0" alt="{{ book.Title }}" class="image" />
<strong>{{ book.Title }}</strong></a><br />
{{ book.Author|join:", " }}<br />
{{ book.Manufacturer}} / {{ book.ListPrice.FormattedPrice }} ({% if book.PublicationDate %}{{ book.PublicationDate }}{% else %}{{ book.ReleaseDate }}{% endif %})<br />
<br />
発送可能時間:{{ book.Availability }}<br />
<br clear="all" />
</div><br />
{% endfor %}
</div>
</body>
</html>
あと、設定用のyaml
aws.yaml
asoid : xxxxxxxxx
devkey : 1XXXXXXXXXXXXXXXXXXX
locale: jp
kinds:
- Blended: Blended すべての商品
- Books: 本
- Classical: クラシック音楽
- DVD: DVD
- Electronics: エレクトロニクス
- ForeignBooks: 洋書
- Hobbies: ホビー
- Kitchen: ホーム&キッチン
- Music: 音楽
- MusicTracks: 曲名
- Software: ソフトウェア
- SportingGoods: スポーツ
- Toys: おもちゃ
- VHS: VHSビデオ
- Video: DVD&ビデオ
- VideoGames: ゲーム
- HealthPersonalCare: ヘルス&ビューティー
これ、AuthorとかAvailabilityなんかのアクセサが最初から決まった型で扱えればコードは2/3くらいになりそう。
いっそBeautifulSoupとかで作った方が作り手側としては納得が行くのかも。
動いてる物は以下
AWS商品検索
追記
パッチを付けるのを忘れてました。
pyaws.diff
--- pyaws/ecs.py.orig Mon Apr 09 07:38:57 2007
+++ pyaws/ecs.py Wed May 07 18:12:21 2008
@@ -19,7 +19,8 @@
import os, urllib, string, inspect
-from xml.dom import minidom
+from google.appengine.api import urlfetch
+import pxdom
__author__ = "Kun Xi < kunxi@kunxi.org >"
__version__ = "0.2.0"
@@ -164,10 +165,7 @@
"""Send the query url and return the DOM
Exception is raised if there is errors"""
- u = urllib.FancyURLopener(HTTP_PROXY)
- usock = u.open(url)
- dom = minidom.parse(usock)
- usock.close()
+ dom = pxdom.parseString(urlfetch.fetch(url).content)
errors = dom.getElementsByTagName('Error')
if errors:
@@ -282,7 +280,7 @@
if(plugins == None):
plugins = {}
- childElements = [e for e in element.childNodes if isinstance(e, minidom.Element)]
+ childElements = [e for e in element.childNodes if isinstance(e, pxdom.Element)]
if childElements:
for child in childElements:
@@ -291,7 +289,7 @@
if type(getattr(rc, key)) <> type([]):
setattr(rc, key, [getattr(rc, key)])
setattr(rc, key, getattr(rc, key) + [unmarshal(child, plugins)])
- elif isinstance(child, minidom.Element):
+ elif isinstance(child, pxdom.Element):
if plugins.has_key('isPivoted') and plugins['isPivoted'](child.tagName):
unmarshal(child, plugins, rc)
elif plugins.has_key('isBypassed') and plugins['isBypassed'](child.tagName):
@@ -303,7 +301,7 @@
else:
setattr(rc, key, unmarshal(child, plugins))
else:
- rc = "".join([e.data for e in element.childNodes if isinstance(e, minidom.Text)])
+ rc = "".join([e.data for e in element.childNodes if isinstance(e, pxdom.Text)])
return rc
pxdomはDOM Level 3をpythonで実装しているすばらしいライブラリです。
ココから取得して下さい。
2009/03/04
Perlでジョブキューサーバと言えばTheSchwartzかGearmanといった所ですが、TheSchwartzみたいに不揮発なデータを扱わないのであればGearmanも有用かと思います。
Gearmanのクライアントライブラリ実装には
Gearman - Language Support (Client APIs)
http://www.danga.com/gearman/
Gearman - Client/Worker APIs
The C client/worker API can be found in the same package as the C server (BSD license):
The Perl API can be found as the Gearman::Client and Gearman::Worker modules in CPAN.
The PHP API can be found as Net_Gearman on PEAR.
The Python API can be found on PyPI as “gearman”, and it can be installed with "easy_install gearman".
- Python Gearman on PyPI
Python Gearman SVN
These are a set of MySQL UDFs that depend on the Gearman server and library in C.
http://www.gearman.org/doku.php?id=download#client_worker_apis
と数種類ありますが、サーバ実装としては
Gearman Job Server (gearmand)
- Gearman - Gearman server and library (0.3)
- Gearwoman - Another server written in C
- Gearman::Server - Gearman::Server - function call "router" and load balancer
http://www.gearman.org/doku.php?id=download#job_server_gearmand
の様に、CによるものとPerlによるものしかありません。Perl版でも良いのですがC版の方が少しでも速いのではないか...という事でgearmandのC版をWindowsにポーティングする事にしました。
ただ、
オリジナルのソースは結構UNIX臭く作ってあり差分も大きくなりそうだったので、別の方が実装した
GearwomanをベースにWindowsポーティングしてみました。
以下パッチ
diff --git a/client.c b/client.c
index ba21f1d..e4154b9 100644
--- a/client.c
+++ b/client.c
@@ -10,7 +10,11 @@ See LICENSE and COPYING for license details.
#include <stdio.h>
#include <string.h>
#include <errno.h>
+#ifndef _WIN32
#include <sys/socket.h>
+#else
+#include <winsock2.h>
+#endif
#include <assert.h>
diff --git a/common.h b/common.h
index 0a56ad5..204a9d2 100644
--- a/common.h
+++ b/common.h
@@ -12,6 +12,10 @@ See LICENSE and COPYING for license details.
#define MSG_NOSIGNAL 0
#endif
+#if defined(_WIN32)
+#define MSG_NOSIGNAL 0
+#endif
+
#ifndef max
#define max(a,b) (a<b?a:b)
#define min(a,b) (a<b?a:b)
diff --git a/gearmand.c b/gearmand.c
index 16e21ac..edfe2b0 100644
--- a/gearmand.c
+++ b/gearmand.c
@@ -13,6 +13,7 @@ See LICENSE and COPYING for license details.
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
+#ifndef _WIN32
#include <netinet/in.h>
#include <getopt.h>
#include <arpa/inet.h>
@@ -21,6 +22,12 @@ See LICENSE and COPYING for license details.
#include <sys/signal.h>
#include <sys/types.h>
#include <sys/stat.h>
+#else
+#include <getopt.h>
+#include <signal.h>
+#include <ws2tcpip.h>
+#define in_addr_t unsigned long
+#endif
#include <event.h>
#include <glib.h>
@@ -48,7 +55,8 @@ GHashTable *g_workers = NULL; /* maps functions -> list of worker clients (GPt
int g_foreground = 1;
char *g_logfilename = "gearmand.log";
char *g_bind = "0.0.0.0";
-int g_port = 4730;
+/* int g_port = 4730; */
+int g_port = 7003;
char g_handle_base[MAX_HANDLE_LEN];
void work_fail(Job *job);
@@ -72,7 +80,14 @@ int listen_on(in_addr_t addr, int port)
int i = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&i, sizeof(i));
// setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&i, sizeof(i));
+#ifndef _WIN32
fcntl(sock, F_SETFL, O_NONBLOCK);
+#else
+ {
+ unsigned long flags = 1;
+ ioctlsocket(sock, FIONBIO, &flags);
+ }
+#endif
sin.sin_addr.s_addr = addr;
sin.sin_port = htons(port);
@@ -822,7 +837,14 @@ void listener_cb(int fd, short events, void *arg)
int s = accept(fd, (struct sockaddr *)&sin, &addrlen);
+#ifndef _WIN32
fcntl(s, F_SETFL, O_NONBLOCK);
+#else
+ {
+ unsigned long flags = 1;
+ ioctlsocket(s, FIONBIO, &flags);
+ }
+#endif
Client *cli = client_new();
cli->state = CLIENT_STATE_CONNECTED;
@@ -871,8 +893,12 @@ void logger(const gchar *domain, GLogLevelFlags level, const gchar *message, gpo
{
struct tm dt;
time_t tme = time(NULL);
- localtime_r(&tme, &dt);
char str[64], *lvl = "OTHER";
+#ifndef _WIN32
+ localtime_r(&tme, &dt);
+#else
+ memcpy(&dt, localtime(&tme), sizeof(dt));
+#endif
strftime(str, 64, "%F %T", &dt);
switch(level) {
@@ -954,6 +980,7 @@ void signal_cb(int fd, short event, void *arg)
void detach()
{
+#ifndef _WIN32
if (fork() != 0)
exit(0);
@@ -977,6 +1004,10 @@ void detach()
open("/dev/null", O_RDWR, 0); /* 0 stdin */
dup2(0, 1); /* 1 stdout */
dup2(0, 2); /* 2 stderr */
+#else
+ perror("daemon mode is disabled on win32 ...");
+ exit(0);
+#endif
}
/****************************************************************************
@@ -986,6 +1017,11 @@ int main(int argc, char *argv[])
int nsockets = 0;
struct event listeners[10];
+#ifdef _WIN32
+ WSADATA wsaData;
+ WSAStartup(MAKEWORD(2, 0), &wsaData);
+#endif
+
parseargs(argc, argv);
if (g_foreground == 0) {
@@ -1009,8 +1045,11 @@ int main(int argc, char *argv[])
event_init();
//printf("%s %s\n", event_get_version(), event_get_method());
+#ifndef _WIN32
signal(SIGPIPE, SIG_IGN);
+#endif
+#ifndef _WIN32
struct event sig_int, sig_hup, sig_term;/*, sig_pipe;*/
if (g_foreground) {
event_set(&sig_int, SIGINT, EV_SIGNAL|EV_PERSIST, signal_cb, &sig_int);
@@ -1025,6 +1064,17 @@ int main(int argc, char *argv[])
event_add(&sig_term, NULL);
/*event_set(&sig_pipe, SIGPIPE, EV_SIGNAL|EV_PERSIST, signal_cb, &sig_pipe);
event_add(&sig_pipe, NULL);*/
+#else
+ struct event sig_int, sig_term;/*, sig_pipe;*/
+ if (g_foreground) {
+ event_set(&sig_int, SIGINT, EV_SIGNAL|EV_PERSIST, signal_cb, &sig_int);
+ event_add(&sig_int, NULL);
+ } else {
+ signal(SIGINT, SIG_IGN);
+ }
+ event_set(&sig_term, SIGTERM, EV_SIGNAL|EV_PERSIST, signal_cb, &sig_term);
+ event_add(&sig_term, NULL);
+#endif
int s = listen_on(inet_addr(g_bind), g_port);
if (s == -1) {
diff --git a/job.c b/job.c
index 812226b..9a28043 100644
--- a/job.c
+++ b/job.c
@@ -9,7 +9,11 @@ See LICENSE and COPYING for license details.
#include <stdio.h>
#include <string.h>
#include <errno.h>
+#ifndef _WIN32
#include <sys/socket.h>
+#else
+#include <winsock2.h>
+#endif
#include <assert.h>
diff --git a/util.c b/util.c
index a482011..6135c9a 100644
--- a/util.c
+++ b/util.c
@@ -9,7 +9,11 @@ See LICENSE and COPYING for license details.
#include <stdio.h>
#include <string.h>
#include <errno.h>
+#ifndef _WIN32
#include <sys/socket.h>
+#else
+#include <winsock2.h>
+#endif
#include <assert.h>
ここでひとつ問題が起こりました。libeventです。libeventのWindowsポーティングとしては
memcached for Win32が有名で、その配布物にlibeventのソースとバイナリがあります。これをmingw32からリンクしようかと思ったのですが、どうやらWindowsのバッファオーバーフローチェックライブラリbufferoverflow.libをリンクしているらしく__security_cookieが無いよとエラーが出ました。
しかしながら諦めず、
libeventのオフィシャルからlibevent-1.4.9-stableをダウンロードし、以下の様にlibeventをWindowsに移植しました。
% cat > config.h
#define VERSION "1.4.9"
#define HAVE_STRTOLL 1
^D
% gcc -c -DHAVE_CONFIG_H -Icompat -Iwin32-code -I. event.c log.c signal.c evutil.c WIN32-Code/win32.c
% ar cr libevent.a event.o log.o signal.o evutil.o win32.o
% cp libevent.a c:/mingw/lib/.
% mkdir c:/mingw/include/libevent
% cp ev*.h c:/mingw/include/libevent/.
あとはgearwomanのMakefileです。
--- Makefile 2009-03-04 09:42:20.000000000 +0900
+++ Makefile.w32 2009-03-04 15:52:29.765625000 +0900
@@ -3,7 +3,7 @@
CC = gcc
LD = gcc -o
AR = ar
-LDFLAGS = `pkg-config --libs gthread-2.0` -levent
+LDFLAGS = `pkg-config --libs gthread-2.0` -levent -lws2_32
CFLAGS = -Wall
# # Debug
@@ -12,10 +12,10 @@
# OPTIMIZATIONS =
# Production (NDEBUG = NO DEBUG / remove asserts)
-CPPFLAGS += -Wall `pkg-config --cflags glib-2.0` -DNDEBUG
+CPPFLAGS += -Wall `pkg-config --cflags glib-2.0` -DNDEBUG -Ic:/mingw/include/libevent
OPTIMIZATIONS = -O2 -funroll-loops -finline-functions
-BIN = gearmand
+BIN = gearmand.exe
OBJ = gearmand.o \
client.o \
拡張子と、libeventへのパス指定だけです。
この状態でmingw32-makeすると、Windowsネイティブ(cygwin未使用)なgearmand.exeが出来上がります。
さて実際にはどれくらい速いのか...
Perl版をポート番号7003で、Native版をポート番号7004で動作させ
use strict;
use Gearman::Worker;
my $gw = Gearman::Worker->new;
$gw->job_servers('127.0.0.1:7004');
$gw->register_function(
'sum' => sub {
my ( $lhs, $rhs ) = split /,/, shift->arg;
$lhs + $rhs;
}
);
$gw->work while 1;
ワーカーとしては数値を足すだけの物をそれぞれのポートで起動しました。
クライアントは接続、ジョブ登録500回、処理実行という流れを30回行うベンチマークで検証してみました。
use strict;
use Benchmark;
use Gearman::Client;
timethese(
30,
{
'perl gearmand' => '&test("127.0.0.1:7003");',
'native gearmand' => '&test("127.0.0.1:7004");',
}
);
sub test {
my $server = shift;
my $gc = Gearman::Client->new;
$gc->job_servers($server);
my $ts = $gc->new_task_set;
for my $i ( 1 .. 500 ) {
$ts->add_task(
"sum" => "3,4",
{
on_complete => sub {
}
}
);
}
$ts->wait;
}
実行結果は
Benchmark: timing 30 iterations of native gearmand, perl gearmand...
native gearmand: 19 wallclock secs (11.88 usr + 1.08 sys = 12.95 CPU) @ 2.32/s (n=30)
perl gearmand: 30 wallclock secs (13.25 usr + 1.17 sys = 14.42 CPU) @ 2.08/s (n=30)
の様になり、若干ですがネイティブ版の方が速い様です。
クライアント・ワーカーライブラリにもC言語の物を使うのであれば、クライアント・ワーカー・サーバ全てでPerlを必要とする事なくGearmanが使える様になりますね。
まぁWindowsでネイティブなGearman使いたいなんて変態は私しかいないかもしれませんが、ご参考になれば...
2009/02/26
IO::Lambdaを見てて、おーいいねー、と思ってサンプル動かしたらエラー出た。
追ってくと、IOなんて根底のモジュールに原因がある事が分かった。原因っていうかエラーが出るように仕込んであった。
以下パッチ作ってオフィシャルにメールした。
--- IO.xs.orig 2006-03-26 11:27:13.000000000 +0900
+++ IO.xs 2009-02-24 20:16:34.921875000 +0900
@@ -121,7 +121,12 @@
}
return RETVAL;
#else
+# ifdef WIN32
+ unsigned long flags = block;
+ return ioctl(PerlIO_fileno(f), FIONBIO, &flags);
+# else
return -1;
+# endif
#endif
}
もしかしたらFAQなpatchで蹴られるだろうけど...
追記 2009/02/26
patchが
マージされました。
2009/02/24
memcachedサーバへのアクセスモジュールは数ありますが、一般的にはlibmemcachedが使われる事が多いと思います。
Perlにおいても
- Cache::Memcached
- Cache::Memcached::Fast
- Cache::Memcached::libmemcached
- Memcached::libmemcached
と数種類存在し、一般的に使用されるlibmemcachedのラッパインタフェースであるCache::Memcached::libmemcachedが使われる事が多い様に思います。
libmemcachedに関しては、
以前Windowsへのポーティングを行い、
オフィシャルへのパッチ送付も行いました。
成果物としては
codereposに置いてあります。
オフィシャルからもリンクを張って頂けるようになりました。
さらにCache::Memcached::Fastについても、
以前Windowsへのポーティングを行い、Cache::Memcached::Fast version 0.13に取り込まれました。
つまり特殊な事をせずにWindowsから利用出来る高速なPerlのmemcachedクライアントライブラリとしてはCache::Memcached::Fastになります。
ちなみこのCache::Memcached::Fast、実はlibmemcachedと比較しても格段に速く、
tokuhiromさんが取ったベンチマークでも素晴らしい結果を叩き出してくれています。
さて今日は、このCache::Memcached::Fastが内部で使用しているXSクライアントモジュールを使用して、高速にmemcachedにアクセスする物を作ってみたいと思います。
このCache::Memcached::FastのXSコードは、Perlに依存した部分とPerlに依存していない部分で分けられており、その後者は一般的なC言語のソースから呼び出しが可能になっています。
なぜこのCache::Memcached::Fastが速いかと言うと、libmemcachedの様に逐次送信を行っているのではなくwritev(2)を使った一括送信を行っているからです。またCache::Memcached::Fastはselect(2)ではなくpoll(2)を使っている為、FD_SETの設定を毎回行わなくて良いというのも微量ではありますが影響しているのではないかと思っています。
このクライアントモジュールは、libmemcachedの様に単一取得(memcached_get)や複数取得(memcached_mget)のAPI呼び出し時に逐次送受信されるのではなく、client_prepare_get/client_prepare_setを使った前準備方式を使っています。これにより複数の問い合わせに対しても内部では一括送信してくれ、一括で受信してくれます。libmemcachedは複数の問い合わせに対してそれぞれ結果待ちをし、全ての問い合わせが完了した時点で制御が戻ります。これについてはMEMCACHED_BEHAVIOR_NO_BLOCKを使用する事でよく似た動作をする事が出来ます。
このクライアントモジュールを使用した実際のサンプルコードは以下の様になります。
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "client.h"
static void* alloc_value(value_size_type value_size, void **opaque) {
*opaque = (char *) malloc(value_size + 1);
memset(*opaque, 0, value_size + 1);
return (void *) *opaque;
}
static void free_value(void *opaque) {
free(opaque);
}
static void result_store(void *arg, void *opaque, int key_index, void *meta) {
char* res = (char*) opaque;
if (res) *(char**)arg = strdup(res);
else *(char**)arg = strdup("");
}
int main(int argc, char* argv[]) {
struct client* c = NULL;
const char* host = "127.0.0.1";
const char* port = "11211";
const char* key = "foo";
const char* value = "bar";
struct result_object object;
char* result = NULL;
int ret;
c = client_init();
client_add_server(c, host, strlen(host), port, strlen(port), 1.0, 1);
object.alloc = NULL;
object.store = result_store;
object.free = NULL;
object.arg = NULL;
client_reset(c, &object, 1);
printf("setting value of '%s' as '%s'\n", key, value);
client_prepare_set(c, CMD_SET, 0, key, strlen(key), 0, 0, value, strlen(value));
client_execute(c);
object.alloc = alloc_value;
object.store = result_store;
object.free = free_value;
object.arg = &result;
client_reset(c, &object, 0);
printf("getting value of '%s'\n", key);
client_prepare_get(c, CMD_GET, 0, key, strlen(key));
client_execute(c);
printf("result value of '%s' is '%s'\n", key, result);
free(result);
return 0;
}
少し変わったコードになりますが、メモリの確保から開放まで自分でハンドリングでき、自前の構造を使った処理も行えるかと思います。
なお、libmemcachedとCache::Memcached::Fastのクライアントモジュールでget/setを繰り返すベンチマークを取ってみました。
まずはlibmemcachedのコード
#include <winsock2.h>
#include <memcached.h>
#include <stdio.h>
#define SERVER_NAME "127.0.0.1"
#define SERVER_PORT 11211
#define KEY "foo"
#define VALUE "bar"
int main(void) {
memcached_return rc;
memcached_st *memc;
char* value;
int value_length = 0;
int flags = 0;
int n;
memc = memcached_create(NULL);
memcached_server_add(memc, SERVER_NAME, SERVER_PORT);
for (n = 0; n < 30000; n++) {
memcached_set(memc, KEY, strlen(KEY), VALUE, strlen(VALUE), 0, 0);
value = memcached_get(memc, KEY, strlen(KEY), &value_length, &flags, &rc);
}
memcached_free(memc);
return 0;
}
次にCache::Memcached::Fast(CMF)のクライアントモジュールのコード
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "client.h"
#define SERVER_NAME "127.0.0.1"
#define SERVER_PORT "11211"
#define KEY "foo"
#define VALUE "bar"
static void* alloc_value(value_size_type value_size, void **opaque) {
*opaque = (char *) malloc(value_size + 1);
memset(*opaque, 0, value_size + 1);
return (void *) *opaque;
}
static void free_value(void *opaque) {
free(opaque);
}
static void result_store(void *arg, void *opaque, int key_index, void *meta) {
char* res = (char*) opaque;
if (res) *(char**)arg = strdup(res);
else *(char**)arg = strdup("");
}
int main(int argc, char* argv[]) {
struct client* c = NULL;
struct result_object object_set, object_get;
char* result = NULL;
int n;
c = client_init();
client_add_server(c, SERVER_NAME, strlen(SERVER_NAME), SERVER_PORT, strlen(SERVER_PORT), 1.0, 1);
object_set.alloc = NULL;
object_set.store = result_store;
object_set.free = NULL;
object_set.arg = NULL;
object_get.alloc = alloc_value;
object_get.store = result_store;
object_get.free = free_value;
object_get.arg = &result;
for (n = 0; n < 30000; n++) {
client_reset(c, &object_set, 1);
client_prepare_set(c, CMD_SET, 0, KEY, strlen(KEY), 0, 0, VALUE, strlen(VALUE));
client_execute(c);
client_reset(c, &object_get, 0);
client_prepare_get(c, CMD_GET, 0, KEY, strlen(KEY));
client_execute(c);
}
free(result);
return 0;
}
計測結果は
libmemcached: 6.953125
CMF: 5.984375
となりました。get/setのループだけなのにCMFは速いですね。
Perlに依存していないので、通常アプリケーションでも問題なく使えるかと思います。
memcachedと通信するC言語で作ったプログラムのパフォーマンスが悪いと思われたならば、一度試して見られてはどうでしょうか?
なんかlibmemcachedはMLにパッチ送っても反応無いし、
Cache::Memcached::Fastの方が速いという噂なので、Cache::Memcached::FastをWindowsに移植してみた。
http://svn.coderepos.org/share/lang/perl/Cache-Memcached-Fast-0.12/
オリジナルからの差分は
svn diff -r19958
で取得して下さい。
リポジトリを作成し直しました。最新取得しなおして下さい。
チェックアウトしてそのままビルドして頂いてもいいです。libmemcachedはgccとVisual Studioをサポートしましたが、今回はgcc限定にしました。
よろしければ、どうぞ。
2009/02/23
WassrのAPIには、imageパラメータによる画像アップロードがあるのですが、これに困ってる人がいたので、人助け。
Wassr [お気軽メッセージングハブ・ワッサー]
質問です。Firefox の Live HTTP Headers で確認すると Web のフォームから画像をアップロードすると素のバイナリデータが送信されています。API を利用した POST でも素のバイナリデータで送信しなければならないのでしょうか?
http://wassr.jp/channel/wassr_api/messages/hNHcDCCtja
試しにvimperatorから出来ないか挑戦してたらハマってハマって...色々試してたんですが、どうも画像だけがアップロードされない。
送信データを以下の様な関数で保存して目で確認してみたけど、あってる。
function writeToFile (filePath, content) {
try {
netscape.security.PrivilegeManager.enablePrivilege ('UniversalXPConnect');
var file = Components.classes["@mozilla.org/file/local;1"].createInstance (Components.interfaces.nsILocalFile);
file.initWithPath (filePath);
if (! file.exists ()) file.create (0, 0664);
var out = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance (Components.interfaces.nsIFileOutputStream);
out.init (file, 0x20 | 0x02, 00004, null);
out.write(content, content.length);
out.close();
} catch (e) {
throw e;
}
}
で、最後の最後は自前でCGIを立ててデータを確認してみた所、なんとデータが欠けてる!!
もしかしてsendじゃ送れないんじゃないか?と思ったらXMLHttpRequestにsendAsBinaryなんてメソッドがあった。
XMLHttpRequest - MDC
This method was added in Firefox 3.
A variant of the send() method that sends binary data.
void sendAsBinary(
in DOMString body
);
Parameters
body
The request body as a DOM string. This data is converted to a string of single-byte characters by truncation (removing the high-order byte of each character).
https://developer.mozilla.org/en/XMLHttpRequest#sendAsBinary%28%29
はよ言えや!
見事送信する事が出来ました。
function createMultipartData(data, boundary) {
var ret = [], boundary = "BOUNDARY-" + (boundary||'')+Math.floor(0x1000000 + Math.random() * 0xffffff).toString(16);
for (var item in data) {
if (!data.hasOwnProperty(item)) continue;
var obj = data[item];
if (obj.filename) {
ret.push(
'--' + boundary,
'Content-Type: '+obj.contentType,
'Content-Length: '+obj.content.length,
'Content-Disposition: form-data; name="'+item+'"; filename="'+obj.filename+'"',
'Content-Transfer-Encoding: binary',
'',
obj.content
);
} else {
ret.push(
'--' + boundary,
'Content-Disposition: form-data; name="'+item+'"',
'',
String(obj)
);
}
}
ret.push('--' + boundary + '--', '');
return {
contentType : "multipart/form-data; boundary=" + boundary,
content: ret.join("\n")
};
}
function getFileContents(aFilePath) {
var file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile);
file.initWithPath(aFilePath);
var fstream = Components.classes['@mozilla.org/network/file-input-stream;1'].createInstance(Components.interfaces.nsIFileInputStream);
fstream.init(file, 1, 1, Components.interfaces.nsIFileInputStream.CLOSE_ON_EOF);
var bstream = Components.classes['@mozilla.org/network/buffered-input-stream;1'].createInstance(Components.interfaces.nsIBufferedInputStream);
bstream.init(fstream, 4096);
var binary = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream);
binary.setInputStream(fstream);
var data = "", size = 0;
while ((size = binary.available()) > 0) {
data += binary.readBytes(size);
}
return data;
}
var sendData = createMultipartData({
status: "image upload from vimperator!",
source: "vimperator/wassr.js",
image: {
filename: "example.jpg",
contentType: "application/octet-stream",
content: getFileContents("C:\\example.png")
}
});
var req = new XMLHttpRequest();
req.open("POST", "http://api.wassr.jp/statuses/update.json");
req.setRequestHeader("Content-Type", sendData.contentType);
req.setRequestHeader("Content-Length", sendData.content.length);
req.sendAsBinary(sendData.content);
こんなんでどうでしょうか?
てかFirefox2の時、どうしてたの?苦笑
2009/02/18
先ほど試していた
tw2ですが、少しやる気になってoutputzの設定もすませて動かしてみました。
ただ、中で使っているreadlineはimportした瞬間にbuilt-inのraw_input()を乗っ取ってしまい、かつ完全にマルチバイトに対応しきれていない様で、入力後の文字が化け化けになってしまいました。
おそらく、入力をutf-8で想定した作りになっているんだと思いますが、これはおそらく解決までに時間が掛かりそうな気がしています。
なのでpatchを書いてa2cさんにpull requestしました。
まれにutf-8しかない文字をtwitter/wassrに書き込んでくれる人がいるので、良いテストになりました。
raw_input()の変わりに、sys.stdin.readline()を使うようにしてあります。
diff --git a/tw2.py b/tw2.py
index fb04179..98fe90a 100755
--- a/tw2.py
+++ b/tw2.py
@@ -73,6 +73,9 @@ if __name__ == "__main__":
friends = set()
friendsTimeLine = []
+ def _T(text):
+ return text.encode('mbcs', 'ignore')
+
def complete(text, status):
results = [x for x in friends if x.startswith(text)] + [None]
return results[status]
@@ -90,13 +93,13 @@ if __name__ == "__main__":
# get twitter friends Replies
print '\twassr replies\t'
for data in reversed(wassr.getReplies()):
- print '%-12s : %s' % (data['user_login_id'] ,data['text'])
+ print _T('%-12s : %s' % (data['user_login_id'] ,data['text']))
friends.add(data['user_login_id'])
# get twitter friends Replies
print '\n\twassr replies\t'
for data in reversed(twitter.GetReplies()):
- print '%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText())
+ print _T('%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText()))
friends.add(data.GetUser().GetScreenName())
return 0
@@ -105,7 +108,7 @@ if __name__ == "__main__":
# Get FriendsTimeLine
print ' ----- wassr Friends Time Line -----'
for data in reversed(wassr. getTimeline()):
- print "%-12s: %s" % (data['user_login_id'] ,data['text'])
+ print _T("%-12s: %s" % (data['user_login_id'] ,data['text']))
twit = "wr[]%-12s: %s" % (data['user_login_id'] ,data['text'])
if twit in friendsTimeLine:
pass
@@ -114,7 +117,7 @@ if __name__ == "__main__":
friendsTimeLine.append(twit)
print '\n ----- twitter Friends Time Line -----'
for data in reversed(twitter.GetFriendsTimeline()):
- print '%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText())
+ print _T('%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText()))
# append log
twit = "tw[%s]%-12s: %s" % (data.GetCreatedAt(), data.GetUser().GetScreenName(), data.GetText())
if twit in friendsTimeLine:
@@ -127,7 +130,8 @@ if __name__ == "__main__":
prompt = '\n cmd: Friendstimeline[f] eXit[xx] \n> '
while True:
- input = raw_input(prompt).split(" ")
+ print prompt,
+ input = sys.stdin.readline().decode('mbcs').encode('utf-8').strip().split(" ")
if input[0] != '':
if len(input) == 1:
if len(input[0]) > 2:
Macで動かなかったら御免なさい。
a2cさんが作ったtwitter client「
tw2」を動かして見ようと思ったのですが、エラーも出るしpitが起動するエディタに何もテンプレートが出力されないのでコードを追ってみた。
調べた結果tw2ではなくpit側に原因がある事が分かった。
Bug #325139 in web.py: “web.profiler does not work on Windows”
tempfile.NamedTemporaryFile() to generate a temp file. On Windows the file cannot be opened a second time. Using tempfile.mkstemp() instead works but you lose the guaranteed cleanup of the temp file.
https://bugs.edge.launchpad.net/webpy/+bug/325139
どうやらNamedTemporaryFile()を使っていて、かつNamedTemporaryFile()はclose()時にファイルを削除してくれるのだけれど、内部でmkstempを使いfdを貰っているのにそのfdをclose()してくれていないものだからWindowsのハンドルが残りっぱなしになりwrite()もflush()もremove()出来ないと言う物だった。
Windowsでpit.pyを使ったプログラムを起動された事がある方は、一度テンポラリディレクトリの中を綺麗にした方が良いかもしれません。
(fd, name) = _mkstemp_inner(dir, prefix, suffix, flags)
file = _os.fdopen(fd, mode, bufsize)
return _TemporaryFileWrapper(file, name)
つまり
import os, yaml, tempfile
t = tempfile.NamedTemporaryFile()
t.write("foo")
t.flush()
path = os.path.abspath(t.name)
result = open(path).read()
t.close()
print result
の様にclose()前だと、fdが開放されていないので"Permission Denited"、また
import os, yaml, tempfile
t = tempfile.NamedTemporaryFile()
t.write("foo")
t.flush()
path = os.path.abspath(t.name)
t.close()
result = open(path).read()
print result
の様にclose()後だと、ファイルは消えてしまっているのでopen()出来ない...という事。なにそれ苦笑
上記リンクにある通り、NamedTemporaryFile()の変わりにmkstempを使い、かつremove()前にclose()してやれば動いた。
--- pit.py.orig 2008-06-28 16:58:42.000000000 +0900
+++ pit.py 2009-02-18 17:22:30.734375000 +0900
@@ -18,13 +18,21 @@
else:
if not os.environ.has_key('EDITOR'):
return {}
- t = tempfile.NamedTemporaryFile()
c = yaml.dump(opts['config'] if opts.has_key('config') else Pit.get(name) ,default_flow_style=False)
+
+ fd, path = tempfile.mkstemp()
+ os.close(fd)
+ t = open(path, "w+b")
t.write(c)
t.flush()
- path = os.path.abspath(t.name)
+
Popen([os.environ['EDITOR'],path]).communicate()
- result = open(path).read()
+
+ t = open(path)
+ result = t.read()
+ t.close()
+ os.remove(path)
+
if result == c:
print 'No Changes'
return profile[name]
@@ -44,7 +52,7 @@
if opts.has_key('require'):
flg = False
keys = set(opts['require'].keys()) - set(ret.keys())
- if keys:
+ if len(keys):
for key in keys:
ret[key] = opts['require'][key]
ret = Pit.set(name,{'config' : ret})
で、tw2については、outputzのIDがいるらしく面倒臭くなったのでヤメ。また今度試します。
ところでこのパッチ、NamedTemporaryFile()を修正するパッチにすべきか、はたまたpit.pyを修正してyoshioriさんに送ればよいのか...どうしましょうねぇ
2009/02/16
さらに朗報は続く
Google App Engine Blog: The sky's (almost) the limit! "High CPU" is no more.
We're very excited today to announce that we've raised limits on several App Engine operations:
- No more "High CPU Requests"! App Engine Apps were once allowed no more than 2 CPU-intensive requests per minute. We've made some adjustments to the way we handle requests, and have eliminated this limitation altogether. To learn more about how this works and the implications for your app, see our documentation.
- Response deadline raised to 30 seconds. The amount of time an App Engine app can take to respond to an incoming request has been raised from 10 to 30 seconds! There are limits on the number of simultaneous active requests an application can process at any given moment--see our docs to learn more.
- Size limits on code files, static files, and requests/responses raised to 10MB! App Engine apps can now receive requests and send responses of up to 10MB in size, and users can upload 10MB code and static files as well. Note that API requests (e.g. memcache.set(), db.put()) are still limited to 1MB in size.
http://googleappengine.blogspot.com/2009/02/skys-almost-limit-high-cpu-is-no-more.html
Google App Engineに1分あたり2CPU以上負荷を掛ける様なリクエストが許される事になりました。
またレスポンスのデッドラインが10秒から30秒にまで引き上げられました。
さらにコード、静的ファイル、および要求/応答におけるサイズ制限が10MBに引き上げられました(但しmemcache.set()、db.put()はまだ1MBに制限されています)。
これで例えば、デカいクエリを投げる様な処理でもエラーで落ちるって事が少なくなるんじゃないですかね。
もうすぐすると、Google App Engineでタイマ発動処理をサポートするという噂もあるので、よりインタラクティブな処理が作れるんじゃないですかね。
2009/02/14
Luaも動的にメンバが追加出来る言語ですので、データ構造を動的に作り上げる事が出来ます。今日はなんとなく、「LuaでWebScraper作ったら、どんなんになるんだろう...」と思いつきで...
用意するのは
あくまでサンプルですので、CSSセレクタも使えなければ最新のWebScraperの様に相対/絶対URL展開や、フィルタ等はサポートしていません。
またresultも動作させていない為、結果が全て戻ります。
さらにltxmlが内部で使っているTinyXML/TinyXPathの仕様からか、XPathの途中に「//」をめり込ます事が出来ませんでした。
まずLua版WebScraperのソース。
luascraper.lua
local http = require("socket.http")
local xml = require("xml")
function process(t)
return {name="process",process={xpath=t[1], name=t[2], scraper=t[3]}}
end
function result(p)
return p
end
function scraper(self)
self.name = "scraper"
function self.scrape(url, ctx)
if ctx == nil then
local chunk = {}
local b, c = http.request {
method = "GET",
url = url,
sink = ltn12.sink.table(chunk)
}
if not c == 200 then
return nil
end
local html = table.concat(chunk)
ctx = {op = self, doc = xml.parse(html)}
end
self.res = {}
if not ctx.doc then
return nil
end
for k,v in pairs(self) do
if (type(v) == "table" and v.name == "process") then
if (type(v.process.scraper) == "table") then
for k1,v1 in pairs(ctx.doc:select(v.process.xpath)) do
local newctx = {top=ctx.top, doc=v1}
self.res[#(self.res)+1] = v.process.scraper.scrape(url, newctx)
end
else
local node = ctx.doc:select(v.process.xpath)
if node then
local attr = v.process.scraper
if attr == "TEXT" then
self.res[v.process.name] = node[1]:text()
elseif string.sub(attr, 1, 1) == "@" then
attr = string.sub(attr, 2)
self.res[v.process.name] = node[1]:attribute(attr)
end
end
end
end
end
return self.res
end
return self
end
案外ボリューム無く書けました。
このモジュールを使って私のtwitter/with_friendsの発言リストをスクレイピングするLuaスクリプトがコレ
twitter_scraper.lua
require "luascraper"
local s = scraper {
process {'//tr[@class="hentry"]',
'status', scraper {
process {'//td[@class="content"]/strong/a', 'nick', 'TEXT'},
process {'//td[contains(@class,"author")]/a/img', 'name', '@alt'},
process {'//td[contains(@class,"author")]/a/img', 'image', '@src'},
process {'//span[contains(@class,"entry-title")]', 'description', 'TEXT'},
process {'//a[@rel="bookmark"]', 'url', '@href'},
}},
result = 'friends';
}
function dump(r)
print('-')
for k,v in pairs(r) do
if type(v) == "table" then
dump(v)
else
print(k,v)
end
end
end
r = s.scrape('http://twitter.com/mattn_jp/with_friends/')
dump(r)
そして結果がコレ
image http://s3.amazonaws.com/twitter_production/profile_images/24072332/negipo_normal.png
name Yoshiteru Negishi
url http://twitter.com/negipo/statuses/371889282
description fooo.nameのおかげで自分が入り忘れてるウェブサービスがわかるようになった
nick negipo
-
image http://s3.amazonaws.com/twitter_production/profile_images/34076402/crestock-221345-1024x768_normal.jpg
name ハラヘ(´・ω・`)
url http://twitter.com/mobcov/statuses/371888702
description 今見るとシローアマダはかっこ悪いけどアイナサハリンは(ry
nick mobcov
-
image http://s3.amazonaws.com/twitter_production/profile_images/33598712/wt_normal.png
name 暴君
url http://twitter.com/VoQn/statuses/371888452
description 授業終わった。やっとご飯食べられる
nick VoQn
-
image http://s3.amazonaws.com/twitter_production/profile_images/24072332/negipo_normal.png
name Yoshiteru Negishi
url http://twitter.com/negipo/statuses/371887582
description はてなperlグループに入った
nick negipo
-
image http://s3.amazonaws.com/twitter_production/profile_images/34078402/cat_888_normal.gif
name (c)yabkoji
url http://twitter.com/yabkoji/statuses/371887022
description 激スイマー襲来中。
nick yabkoji
-
image http://s3.amazonaws.com/twitter_production/profile_images/22560082/nobita_normal.jpg
name kimidora
url http://twitter.com/kimidora/statuses/371886072
description ねむいねむいねむいねむいねむいねむいねむいねむいねむいねむい。。。。。。。。。。ぐぅ *Tw*
nick kimidora
-
image http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name highness
url http://twitter.com/smokeymonkey/statuses/371885732
description うぅ、ムラサメで部長とカブった。
nick smokeymonkey
-
image http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name highness
url http://twitter.com/smokeymonkey/statuses/371885022
description はてなグループって排他制御あるのかしら。
nick smokeymonkey
-
image http://s3.amazonaws.com/twitter_production/profile_images/34076402/crestock-221345-1024x768_normal.jpg
name ハラヘ(´・ω・`)
url http://twitter.com/mobcov/statuses/371884382
description TextMateアップデートキターーーーー!!
nick mobcov
-
image http://s3.amazonaws.com/twitter_production/profile_images/32257122/basara_normal.jpg
name japo
url http://twitter.com/japo/statuses/371884052
description ハマーン様万歳だろJK
nick japo
-
image http://s3.amazonaws.com/twitter_production/profile_images/19383642/ore_normal.jpg
name Sugano Yoshihisa(E)
url http://twitter.com/koshian/statuses/371883862
description 「あのPerlモジュールなんてったっけ?」とぐぐったら、自分のはてなブックマークがひっかかって見事に発見
nick koshian
-
image http://s3.amazonaws.com/twitter_production/profile_images/22022432/plagger_logo_purple_normal.png
name plagger.org
url http://twitter.com/plagger/statuses/371883772
description Safariの『Webクリップ』を試してみる。 - sta la sta
nick plagger
-
image http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name highness
url http://twitter.com/smokeymonkey/statuses/371882682
description うわ、 GFF でペーネロペー出てるのか。超欲しい。
nick smokeymonkey
-
image http://s3.amazonaws.com/twitter_production/profile_images/33078022/icon_normal.gif
name Miki@7500
url http://twitter.com/7500/statuses/371881092
description 午後の部にとりかかりまっしゅ!夕方までしゃいなら?ノシ
nick 7500
-
image http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name highness
url http://twitter.com/smokeymonkey/statuses/371879692
description 初代からν→ F91 まで + アレックスと 08 小隊とデンドロビウムは把握してる。その後はさっぱりだな。
nick smokeymonkey
-
image http://s3.amazonaws.com/twitter_production/profile_images/15059862/al3x_normal.jpg
name Alex Payne
url http://twitter.com/al3x/statuses/371879502
description Oh, how I wanted to be rid of MacPorts. Curse you, Image/Rmagick.
nick al3x
-
image http://s3.amazonaws.com/twitter_production/profile_images/34297902/kamon_normal.png
name wrongbee
url http://twitter.com/wrongbee/statuses/371878872
description ペーネロペーの画像検索結果
nick wrongbee
-
image http://s3.amazonaws.com/twitter_production/profile_images/33078022/icon_normal.gif
name Miki@7500
url http://twitter.com/7500/statuses/371878362
description @
nick 7500
-
image http://s3.amazonaws.com/twitter_production/profile_images/15927972/otsune_hanaji_purple_normal.jpg
name ??uns?o ??n??s??
url http://twitter.com/otsune/statuses/371877452
description Pukkaのすばらしい所はdel.icio.usのアカウント切り替えが一瞬でプルダウンメニューで選べる所だ。bookmarkletなんかでソーシャルブックマークしてたらログアウト・ログインの時間分だけ人生を無駄にする lang:ja
nick otsune
-
image http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name highness
url http://twitter.com/smokeymonkey/statuses/371876992
description 閃光のハサウェイはガンダムサイドストーリーでは一番好きだ。
nick smokeymonkey
-
-
Luaでもやれない事はない!
追記
result処理、相対/絶対URL展開処理を加えて、CodeReposに入れておきました。
2009/02/12
hexdigestでなくdigestですね。:-)
mod_access_tokenを入れてみた - まめ畑
ここまでは良かったのですが、Signatureを生成するスクリプトを作ろうと思ったのですが、Rubyで上手くいかない・・・。
http://d.hatena.ne.jp/con_mame/20090209
あとはbase64に付く改行とターミネータをカットすればOKですね。
require 'openssl'
require 'base64'
require 'uri'
require 'time'
access_key = 'foo'
secret = 'bar'
url = 'http://localhost:8080/access_token/example.jpg'
exp = (Time.new+300).to_i.to_s
uri = URI.parse(url)
plain = "GET#{uri.path}#{exp}#{access_key}"
hmac = OpenSSL::HMAC::digest(OpenSSL::Digest::SHA1.new, secret, plain)
sig = Base64.encode64(hmac).chomp.sub(/=*$/, '')
puts "#{url}?Signature=#{URI.escape(sig)}&AccessKey=#{URI.escape(access_key)}&Expires=#{URI.escape(exp.to_s)}"
結果はこんな感じ。
http://localhost:8080/access_token/example.jpg?Signature=Rt2cKnnOYuDiE/XYKTLTaOuY4Wg&AccessKey=foo&Expires=1234417692
ちなみに
Base64.b64encode
はエンコード結果を表示してしまうのでencode64が良さそうですね。
def b64encode(bin, len = 60)
encode64(bin).scan(/.{1,#{len}}/) do
print $&, "\n"
end
end
2009/02/10
これは朗報。
Google App Engine Blog: SDK version 1.1.9 Released
- You can now use the Python standard libraries urllib, urllib2 or httplib to make HTTP requests. This has been a frequent request on our issue tracker.
- We've been working on a set of tools that will make the process of uploading and downloading data from App Engine applications easier. Today we're excited to announce an early release of our new bulk uploading client. You can try it out here. Let us know what you think in our Google Group!
- Several updates to our datastore, including the automatic generation of single property indexes and the addition of IN and != operators to db.Query. See the Datastore API docs for more details.
- A bunch of additional bugfixes and enhancements, listed in our Release Notes.
http://googleappengine.blogspot.com/2009/02/sdk-version-119-released.html
試しに以下の様なコードを書いて
import urllib2
f = urllib2.urlopen('http://www.google.com/')
print "Content-Type: text/plain;"
print
print f.read()
実行させてみた。
おー動いてる。これで今までGoogle App Engineのurlfetch API様にパッチを当ててきた物が要らなくなる。
2009/02/09
livedoor 製品で mod_access_token というのが出たみたいです。
livedoor ラボ「EDGE」 開発日誌 : 「mod_access_token」の配布開始と「EDGE src」公開のお知らせ - livedoor Blog(ブログ)
ウェブサイト上の画像やファイルに有効期限を指定して、ユーザーに一時的なダウンロードを許可する、ライブドアで独自開発したApacheモジュールです。このモジュールをApache Webサーバに組み込むことにより、画像やファイルをウェブ上で公開するときに有効期限をつけることができるようになり、Webアプリケーションと組み合わせる事で公開範囲の制御を行なう事が可能になります。
http://blog.livedoor.jp/edge_labs/archives/717201.html
modaccesstoken - Google Code
mod_access_token provides access token based secure downloading.
http://code.google.com/p/modaccesstoken/
ソースコード見たら依存が浅かったのでWindowsでビルドしてみた。
Makefile.w32
APACHE_ROOT=C:\Program Files\Apache Software Foundation\Apache2.2
CFLAGS=/I"$(APACHE_ROOT)\include" /DWIN32 /nologo
LDFLAGS=/LIBPATH:"$(APACHE_ROOT)\lib"
LIBS= libapr-1.lib libaprutil-1.lib libhttpd.lib
all : mod_access_token.so
mod_access_token.so : mod_access_token.obj
link /nologo /DLL /OUT:$@ /EXPORT:access_token_module mod_access_token.obj $(LDFLAGS) $(LIBS)
mod_access_token.obj : mod_access_token.c
cl -c $(CFLAGS) mod_access_token.c
なぜかmingw32では実行時にエラーが出たのであきらめました。VC6では可変長マクロが使えないので最終的にはVC8でしか試せませんでした。
ビルドしたモジュールを
C:\Program Files\Apache Software Foundation\Apache2.2\modules\
に置き、httpd.confへLoadModuleを追加。対象のフォルダに以下の様に設定(.htaccess)します。
.htaccess
AccessTokenCheck On
AccessTokenAccessKey foo
AccessTokenSecret bar
このAccessTokenAccessKey(foo)が公開鍵、AccessTokenSecret(bar)が秘密鍵になります。
認証はREADMEに書かれている通り
download.pl
use strict;
use URI;
use Digest::HMAC_SHA1;
my $access_key = 'foo';
my $secret = shift || die('specify secret key!');
my $exp = time + 300;
my $url = 'http://localhost:8080/access_token/example.jpg';
my $uri = URI->new( $url );
my $plain = sprintf '%s%s%s%s', 'GET', $uri->path, $exp, $access_key;
my $hmac = Digest::HMAC_SHA1->new( $secret );
$hmac->add( $plain );
my $sig = $hmac->b64digest;
$uri->query_form({
Signature => $sig,
AccessKey => $access_key,
Expires => $exp,
});
printf "%s\n", $uri->as_string;
といった感じ。キーが間違ってるとDECLINED(Forbidden)になります。
内部はSHA1による認証処理になってます。
誰ですか!「WindowsなんてマイナーなOSのことは知りません」とか言ってるの!(謎)
追記
パッチを当てないと動かなかったのを忘れてました。
Index: mod_access_token.c
===================================================================
--- mod_access_token.c (revision 3)
+++ mod_access_token.c (working copy)
@@ -2,10 +2,12 @@
#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
+#include "http_request.h"
#include "http_log.h"
#include "ap_config.h"
#include "apr_sha1.h"
#include "apr_strings.h"
+#include "apr_base64.h"
#include "apr_lib.h"
#define ACCESS_KEY_NAME "AccessKey"
2009/01/27
よく検索エンジンなんかでtwitterの発言が引っかかった時、@付きの会話があり話の前後を辿ってみたりする事があります。
非常にめんどくさいですね。
例えば先日あった
kuさんとの会話の一部が見つかったとしましょう。
http://twitter.com/ku/status/1136652052

http://twitter.com/ku/status/1136652052
この発言から会話の前後を辿るには、時刻が記述されたリンクをポチポチとクリックして行くしかなく、とても大変な手間になります。
これを簡単に知る方法はないか...あります。
twitterは検索機能を持っているのですが、この結果に会話の前後を辿る機能があるのです。試しに「mattn_jp ku」という検索語で検索し、結果にある「Show Conversation」という部分をクリックしてみましょう。
綺麗に会話のツリーが出ますね。この「Show Conversation」は、search.twitter.com の http://search.twitter.com/search/thread/{ステータスID} からのHTML出力結果をインライン表示している処理になっています。
つまり、上記の
http://twitter.com/ku/status/1136652052
から会話のツリーを得るには
http://search.twitter.com/search/thread/1136652052
にアクセスすれば良い事が分かります。
但しインライン用に用意されたHTMLなのでCSSも付きませんし、ドメインが twitter.com ではなく、search.twitter.com なので twitter が使っている jQuery で get メソッド呼び出しする事も出来ません。
ここはいたし方無くGrasemonkeyと行きましょう。twitterのタイムラインのHTMLにはmicroformatと共に返信があるかどうかを示すクラス属性値「reply」が付いています。これが付いているステータスのみ「Show Conversation」を付け、クリックしたらインラインで会話を表示する物を作ってみました。
タイムラインに「Show Conversation」と付いているリンクをクリックすると、前後の会話が表示されます。何気に便利かもしれません。
ソースは
この辺においておきます。
宜しければどうぞ。
2009/01/21
このサイトでは、jQuery Lightbox Plugin は"
balupton edition"を入れているのですが、jQueryのバージョンを1.3に上げた途端、エラーが出る様になってしまいました。
調べた所、jQuery1.3ではSizzleという新しいセレクタが採用されており、以下の様な属性フィルタが動かない事が分かりました。
$('[@rel*=foo]')
さらに調べた所、どうやら"@"を付けている事自体が間違っているらしく"@"を取って
$('[rel*=foo]')
としてやれば動く様になりました。なぜオリジナルの"@"付きのまま動いていたのかは分かりませんが...
本体への反映は以下
--- jquery.lightbox.js.orig 2008-09-12 02:46:50.000000000 +0900
+++ jquery.lightbox.js 2009-01-21 21:15:37.312500000 +0900
@@ -827,7 +827,7 @@
var groups_n = 0;
var orig_rel = this.rel;
// Create the groups
- $.each($('[@rel*='+orig_rel+']'), function(index, obj){
+ $.each($('[rel*='+orig_rel+']'), function(index, obj){
// Get the group
var rel = $(obj).attr('rel');
// Are we really a group
ま、balupton edition使ってる人少ないかも知れませんが...
2009/01/20
Greasemonkeyから扱いやすくなった。
Gist for Greasemonkey - GitHub
We’re now appending the gist name at the end of its raw url. That means it’s dead-simple to serve greasemonkey (or greasekit) scripts directly from gist.github.com. I was able to write my first script and install it in less than five minutes: ...
http://github.com/blog/302-gist-for-greasemonkey
今までのraw形式URLの後に、ファイル名が付与される様になった。
これにより
gist の GM を簡単にインストールする方法 - 8時40分が超えられない - subtech
あー、いちいち raw format 叩かなくても
http://gist.github.com/00000
なスクリプトなら
http://gist.github.com/00000.txt?.user.js
とするだけでおkなのか。
http://subtech.g.hatena.ne.jp/secondlife/20090116/1232083225
といったhackを使わずGreasemonkeyスクリプトを直接インストール出来る様になる。
embedならばview rawから直接インストール出来る様になった。
gist++
2009/01/16
今日、とある場所で
kanaさんからvimに
"syn-include"
なんて物があるのを教えて貰いました。
:he syn-include
9. Including syntax files *:syn-include* *E397*
It is often useful for one language's syntax file to include a syntax file for
a related language. Depending on the exact relationship, this can be done in
two different ways:
- If top-level syntax items in the included syntax file are to be
allowed at the top level in the including syntax, you can simply use
the |:runtime| command: >
< - If top-level syntax items in the included syntax file are to be
contained within a region in the including syntax, you can use the
":syntax include" command:
:sy[ntax] include [@{grouplist-name}] {file-name}
All syntax items declared in the included file will have the
"contained" flag added. In addition, if a group list is specified,
all top-level syntax items in the included file will be added to
that list. >
<
When {file-name} is an absolute path (starts with "/", "c:", "$VAR"
or "<sfile>") that file is sourced. When it is a relative path
(e.g., "syntax/pod.vim") the file is searched for in 'runtimepath'.
All matching files are loaded. Using a relative path is
recommended, because it allows a user to replace the included file
with his own version, without replacing the file that does the ":syn
include".
知らんかった...
これを使えばPerlのPodだけ別のsyntaxファイルから適用出来るといった物。
これは凄い!
つまりはregionだけ決められれば、その部分に言語毎のsyntaxが適用出来る事になる。試しに、はてなのスーパーPre記法に色を付けられる様にしてみた。
ベースはmotemenさんのhatena-vim
motemen's hatena-vim at master - GitHub
Vim scripts for posting/updating hatena diary/group
http://github.com/motemen/hatena-vim/tree/master
この
syntax/hatena.vim
の最下行に以下のコードを貼り付ける。
function SyntaxSuperPre()
let lnum = 1
let lmax = line("$")
let mx = '^>|\(.*\)|$'
while lnum <= lmax
let curline = getline(lnum)
if curline =~ mx
let lang = substitute(curline, mx, '\1', '')
exec 'runtime! syntax/'.lang.'.vim'
unlet b:current_syntax
let syntaxfile = fnameescape(substitute(globpath(&rtp, 'syntax/'.lang.'.vim'), '[\r\n].*$', '', ''))
if len(syntaxfile)
exec 'syntax include @inline_'.lang.' '.syntaxfile
exec 'syn region hatenaSuperPre matchgroup=hatenaBlockDelimiter start=+^>|'.lang.'|$+ end=+^||<$+ contains=@inline_'.lang
endif
end
let lnum = lnum + 1
endwhile
syn cluster inline_perl remove=perlFunctionName
endfunction
call SyntaxSuperPre()
適当なので使わないで下さい
perlFunctionNameはregionが広すぎるので無効にしてます
すると...
おーーー!出ました。
続きは
この辺でやっていきます。出来上がったらmotemenさんにmergeして貰うのも良いかも。
ちなみに、filetype適用時にロードしているので、pre記法の言語を編集途中で変更したり、新しくスーパーPre記法を追加したりするとsyntaxが適用されなくなります。ま、これからですな。
syn-include++
2009/01/14
追記
事情が変わった!(髭男爵)
変わってなかった...orz
詳細は下記。
おもしろい。
static - 素人がプログラミングを勉強するブログ
var counter = function () {
var static = /(^o^)/;
return ('i' in static)? ++static.i:
static.i = 0;
};
console.log(counter());
console.log(counter());
console.log(counter());
console.log(counter());
http://d.hatena.ne.jp/javascripter/20090113/1231863436
正規表現リテラルがコンパイル時に生成され、静的保持される特性を利用したカウンタ。
でも使わない。気持ちワルイ!苦笑
皆、思いつくだろうけど私はやっぱりこう書く。
var counter = (function() {
var static = 0;
return function() {return static++};
})();
ちなみにベンチを取ってみた。
if (typeof console == 'undefined') console = {log:print};
var counter1 = function () {
var static = /(^o^)/;
return ('i' in static)? ++static.i:
static.i = 0;
};
var counter2 = (function() {
var static = 0;
return function() {return static++};
})();
var start;
start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter1();
console.log(new Date().getTime() - start)
start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter2();
console.log(new Date().getTime() - start)
Windows XP、P4 3GHz CPU、1G Mem。
tracemonkey : JavaScript-C 1.8.0 pre-release 1 2007-10-03
6515
5500
v8 : V8 version 0.3.4 (internal)
1558
383
ま、やっぱりね...というところ。
結論
V8はえーーー!
追記
os0xさんから
var counter1 = function () { var static = /(^o^)/; return ++static.i || (static.i=0); }; こうすれば差は縮まる http://la.ma.la/blog/diary_200705301141.htm
とブックマークコメントを貰った。
counter3としてベンチに追加した
所、結果が変わった。
if (typeof console == 'undefined') {
if (typeof print == 'function') console = {log:print};
else if (typeof WScript != 'undefined') console = {log:function(s){WScript.StdOut.WriteLine(s)}};
else console = {log:alert};
}
var counter1 = function () {
var static = /(^o^)/;
return ('i' in static)? ++static.i:
static.i = 0;
};
var counter2 = (function() {
var static = 0;
return function() {return static++};
})();
var counter3 = function () {
var static = /(^o^)/; return ++static.i || (static.i=0);
};
var start;
start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter1();
console.log("counter1:" + (new Date().getTime() - start))
start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter2();
console.log("counter2:" + (new Date().getTime() - start))
start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter3();
console.log("counter3:" + (new Date().getTime() - start))
さらにベンチにwindows scripting hostも足してみた。
tracemonkey : JavaScript-C 1.8.0 pre-release 1 2007-10-03
counter1:1624
counter2:381
counter3:529
v8 : V8 version 0.3.4 (internal)
counter1:1610
counter2:403
counter3:665
cscript : Microsoft (R) Windows Script Host Version 5.6
counter1:107282
counter3:44016
counter2:51297
おぉぉぉぉぉぉぉぉぉぉ!!!!なんと
正規表現オブジェクトの方が速いではないか!!!
詳細はos0xさんか何方かが書いてくれるとして...
失礼しました!!!
上の結果が正しいです。
結論
windows scripting hostおせーーー!
追記
POSIX では明確にソケットの最大値とはうたってはいないものの、Linux の実装を見ても最大値と扱う方が良い様です。また Winsock では select(2)
の第一引数は無視されるようです。
C言語でソケットを使うプログラミングを行う際、ソケットディスクリプタがシグナル状態かを調べる方法として
select(2)
があります。
使い方は
int r;
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sock, &rfds);
r = select(1, &rfds, NULL, NULL, NULL);
といった感じ。ここで
select(2)
の第一引数に渡している値は、ディスクリプタ集合
rfds
の内、いくつ検証するかを指す値。つまり
rfds
に対して
FD_ZERO/FD_CLR
してから
FD_SET
した回数となります。
ちなみに戻り値は、ディスクリプタ集合の内どれだけシグナル状態かの数が返ります。つまり
r = select(num_fds, &rfds, &wfds, &efds, NULL);
の場合、
rfds/wfds/efds
の内、シグナル状態であるディスクリプタの総数が返ります。
昔のC言語で書かれたソースを見ると、よく
int r;
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sock, &rfds);
r = select(FD_SETSIZE, &rfds, NULL, NULL, NULL);
と
FD_SETSIZE
を使って書かれた記述を見ます。これは
rfds
が構造体であり、そのメンバに持つディスクリプタ格納配列
fd_array
が
FD_SETSIZE
でサイズ定義されている事を利用している為です。
これを無駄と考える人がいた為か、こういう記述も未だに良く見かけます。
int r;
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sock1, &rfds);
FD_SET(sock2, &rfds);
int check_sock = max(sock1, sock2);
r = select(max(sock1, sock2), &rfds, NULL, NULL, NULL);
確かに
FD_SETSIZE
の理屈から言えば納得が行く話かも知れませんが、
FD_SETSIZE
の定義とは
fd_array
の個数でありディスクリプタの値が取り得る値の最大値ではありません。ディスクリプタが順列で生成されるなんて仕様もありません。
つまり、間違いです。
にも関わらず、ディスクリプタの最大値は
FD_SETSIZE
だと思わせる記述が出回ってしまったのだと思います。
Manpage of SELECT_TUT
nfds: 全ての集合に含まれるファイルディスクリプタのうち、値が最大のものに 1 を足した整数である。すなわち、ファイルディスクリプタを集合に加える作業の途中で、全てのファイルディスクリプタを見て最大値を求め、それに 1 を加えて nfds として select に渡さないといけない、ということだ。
http://www.linux.or.jp/JM/html/LDP_man-pages/man2/select_tut.2.html
おそらく初期のディスクリプタの実装が配列の添え字であった為、こういう記述となり残っていったのだと思う。
例えばWindowsで言えば、
socket(7)
関数で返るディスクリプタの値は
FD_SETSIZE
に収まらない値で返ります。またMinGW(Minimalist GNU for Windows)のwinsock.hで定義されている
FD_SETSIZE
は64と中途半端な値になっています。
それはなぜか...
適当だからです。
通常、1プロセスが扱えるディスクリプタの数が設定されるべきですが、それが明確に定義すべきでない環境では意味のない値になったのだと思います。
しかしながら、多数のディスクリプタ(ファイルもソケットも)を扱うプログラムならば64個使い切ってしまう事はあり得りえるでしょうね。
ではどうすれば良いか。
FD_SETSIZE
は
fd_array
の個数を定義するマクロであり、
FD_ZERO/FD_CLR/FD_SET
でそれを操作する再に用いられる閾値であり、ループ回数なのです。さらに
FD_ZERO/FD_CLR/FD_SET
は
FD_SETSIZE
と同じくマクロなのです。
#define FD_CLR(fd,set) do { u_int __i;\
for (__i = 0; __i < ((fd_set *)(set))->fd_count ; __i++) {\
if (((fd_set *)(set))->fd_array[__i] == (fd)) {\
while (__i < ((fd_set *)(set))->fd_count-1) {\
((fd_set*)(set))->fd_array[__i] = ((fd_set*)(set))->fd_array[__i+1];\
__i++;\
}\
((fd_set*)(set))->fd_count--;\
break;\
}\
}\
} while (0)
しかも
FD_SETSIZE
の定義は#ifdefにより使い手側が変更出来る様になっています。もし通常よりも多くディスクリプタを扱いたいならばコンパイル時に
FD_SETSIZE
を定義してやれば良いのです。
この事は、MSDNにも書いてあります。
select Function (Windows)
http://msdn.microsoft.com/en-us/library/ms740141.aspx
Four macros are defined in the header file Winsock2.h for manipulating and checking the descriptor sets. The variable FD_SETSIZE determines the maximum number of descriptors in a set. (The default value of FD_SETSIZE is 64, which can be modified by defining FD_SETSIZE to another value before including Winsock2.h.)
ディスクリプタ集合をを操作/チェックするためにヘッダーファイルWinsock2.hに4つのマクロが定義されています。FD_SETSIZEはディスクリプタ集合の最大個数を記述子の最大数を決定します。 (FD_SETSIZEの初期値は64です。これはwinsock2.hをインクルードする前に別の値で変更する事が出来ます。)
但し、この
FD_SETSIZE
が少ない値のままコンパイルされたライブラリと、多く設定した値のライブラリを併用すると場合によっては誤動作する可能性があるので注意が必要です。まぁこれはUNIXでも同じ話ですね。
だらだら書きましたが、何を言いたいかというと
UNIXで開発していて、将来的にWindowsにも移植するかもしれないソフトウェアならば、ディスクリプタの値がディスクリプタ集合の最大個数である...といった様なコーディングは辞めましょう。
という事です。
2009/01/07
つい勢いでやった。今は反省している。
やる夫で学ぶCooking&AAstory 第0章
http://d.hatena.ne.jp/lionfan/20090105#1231170925
import sys
import os
import gtk
import pango
import urllib
from BeautifulSoup import BeautifulSoup
win = gtk.Window()
win.connect('destroy', gtk.main_quit)
win.set_default_size(800, 600)
win.set_title('やる夫で学ぶCooking&AAstory 第0章')
table = gtk.Table(1, 2, False)
win.add(table)
swin = gtk.ScrolledWindow()
swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
l = gtk.Label('')
l.set_alignment(0.5, 0.5)
fonts = ['Mona 10', 'MS UI Gothic 10']
for fn in fonts:
try:
font = pango.FontDescription(fn)
if font:
l.modify_font(font)
break
except:
pass
swin.add_with_viewport(l)
table.attach(swin,
0, 1, 0, 1,
gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL,
0, 0)
b = gtk.Button('次へ')
def button_clicked(widget):
if len(ascii_arts) == 0:
gtk.main_quit()
return
elif len(ascii_arts) == 1:
b.set_label('閉じる')
text = ''.join(ascii_arts.pop(0).findAll(text=True))
l.set_text(text)
b.connect('clicked', button_clicked)
table.attach(b,
0, 1, 1, 2,
gtk.EXPAND | gtk.FILL, 0,
0, 0)
html = urllib.urlopen('http://d.hatena.ne.jp/lionfan/20090105#1231170925').read().decode('euc-jp', 'ignore')
soup = BeautifulSoup(html, convertEntities='html')
ascii_arts = soup.findAll('div', { 'class' : 'ascii-art'})
b.emit('clicked')
win.show_all()
try:
gtk.main()
except KeyboardInterrupt:
gtk.main_quit()
追記
第1章も作った...というかタイトルとURL変えただけだが...
import sys
import os
import gtk
import pango
import urllib
from BeautifulSoup import BeautifulSoup
win = gtk.Window()
win.connect('destroy', gtk.main_quit)
win.set_default_size(800, 600)
win.set_title('やる夫で学ぶCooking&AAstory 第1章')
table = gtk.Table(1, 2, False)
win.add(table)
swin = gtk.ScrolledWindow()
swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
l = gtk.Label('')
l.set_alignment(0.5, 0.5)
fonts = ['Mona 10', 'MS UI Gothic 10']
for fn in fonts:
try:
font = pango.FontDescription(fn)
if font:
l.modify_font(font)
break
except:
pass
swin.add_with_viewport(l)
table.attach(swin,
0, 1, 0, 1,
gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL,
0, 0)
b = gtk.Button('次へ')
def button_clicked(widget):
if len(ascii_arts) == 0:
gtk.main_quit()
return
elif len(ascii_arts) == 1:
b.set_label('閉じる')
text = ''.join(ascii_arts.pop(0).findAll(text=True))
l.set_text(text)
b.connect('clicked', button_clicked)
table.attach(b,
0, 1, 1, 2,
gtk.EXPAND | gtk.FILL, 0,
0, 0)
html = urllib.urlopen('http://d.hatena.ne.jp/lionfan/20090106#1231249409').read().decode('euc-jp', 'ignore')
soup = BeautifulSoup(html, convertEntities='html')
ascii_arts = soup.findAll('div', { 'class' : 'ascii-art'})
b.emit('clicked')
win.show_all()
try:
gtk.main()
except KeyboardInterrupt:
gtk.main_quit()
im.kayac.comに送信するperlスクリプト。
コードだけ。
#!/usr/bin/perl
use strict;
use warnings;
use Encode;
use LWP::UserAgent;
use HTTP::Request::Common qw(POST);
use Config::Pit;
use Digest::SHA1 qw(sha1_hex);
use JSON;
if ($^O eq 'MSWin32') {
eval {
require Win32::API;
Win32::API->Import('kernel32', 'UINT GetACP()');
Encode::from_to($ARGV[0], 'cp'.GetACP(), 'utf-8');
};
}
my $config = pit_get("im.kayac.com", require => {
"username" => "your username on im.kayac.com",
"password" => "your password(or secretkey) on im.kayac.com",
"authtype" => "auth type on im.kayac.com: none/password/secretkey",
});
my %data = ( message => shift );
die "should be specify message" unless $data{message};
$data{password} = $config->{password} if $config->{authtype} eq "password";
$data{sig} = sha1_hex($data{message} . $config->{password}) if $config->{authtype} eq "secretkey";
my $ua = LWP::UserAgent->new;
my $res = from_json($ua->post(
"http://im.kayac.com/api/post/$config->{username}", \%data,
"Content-Type" => "application/x-www-form-urlencoded"
)->decoded_content);
print $res->{result}.$res->{error};
2009/01/06
WindowsでGitを使う場合、
msysGitというMSYS(Minimal SYStem)ポーティングされたGitを使うのですが、これにはssh-askpassが付いてきません。以前書いた「
Big Sky :: Windowsでもssh-agentとssh-addを使ってパスフレーズ入力を省略する。」でご紹介した方法は、開いたコマンドプロンプト内でしか有効になりません。これは新しく起動したコマンドプロンプトに、ssh-agentから引き渡されるSSH_AGENT_PID/SSH_AUTH_SOCKといった環境変数が引き継がれていないのが原因です。これを解決する方法のひとつとして、win-ssh-askpassという物があります。
win-ssh-askpass 1.05 (GANAware)
GUI ssh-agent for cygwin.
http://www.ganaware.jp/archives/2006/04/winsshaskpass_1.html
win-ssh-askpass バージョンアップについてあれこれ (GANAware)
ずいぶん久しぶりに win-ssh-askpass をバージョンアップしました。
http://www.ganaware.jp/archives/2006/04/winsshaskpass_2.html
このwin-ssh-askpassは、ssh-agent起動時に得た環境変数情報をWindowsのユーザ環境変数に登録してくれ、次回起動するコマンドプロンプトでもパスフレーズを聞かれなくするという物です。UNIXだとx11-ssh-askpassとか、ssh-askpass-gnomeというGUIバージョンも存在しますが、これのWindows版という所ですね。
但し、win-ssh-agentはcygwinを使っており、cygwin配下でunix domain socketを使った通信を行っています。unix domain socketを使わず実装されているmsysGitでは動かないのです。他に、puttyのpagent/plinkを使った方法もありますが私が使いたい条件
- OpenSSH for windows
- msysGit付属のbashではなく、コマンドプロンプト
- cygwinは使いたくない
には適応出来なかったりします。
という事で、win-ssh-askpass-1.05のcygwin依存を無くすパッチを書いてみました。
気を付ける所は
- setenv/putenvの置き換え
- fork/execからsystemへ
だけです。
以下パッチ
diff -u win-ssh-askpass-1.05.orig/agent.cpp win-ssh-askpass-1.05/agent.cpp
--- win-ssh-askpass-1.05.orig/agent.cpp 2006-04-02 10:42:24.000000000 +0900
+++ win-ssh-askpass-1.05/agent.cpp 2009-01-06 17:21:35.953125000 +0900
@@ -2,8 +2,10 @@
#include <stdlib.h>
#include <string.h>
#include <string>
+#ifdef __CYGWIN__
#include <sys/cygwin.h>
#include <sys/wait.h>
+#endif
#include <unistd.h>
#include <list>
#include <stdarg.h>
@@ -56,6 +58,7 @@
// run ssh-add
int run_ssh_add(const char *i_identityFile)
{
+#ifdef __CYGWIN__
pid_t childPID = fork();
setenv("SSH_ASKPASS", getAskpassPath().c_str(), 1);
verbose("export SSH_ASKPASS=%s\n", getAskpassPath().c_str());
@@ -95,6 +98,25 @@
else
return -1;
}
+#else
+ std::string str;
+
+ str = "SSH_ASKPASS=";
+ str += getAskpassPath().c_str();
+ putenv(str.c_str());
+ verbose("export SSH_ASKPASS=%s\n", getAskpassPath().c_str());
+
+ str = "DISPLAY=localhost:0";
+ putenv(str.c_str());
+ verbose("exec ssh-add %s\n", i_identityFile ? i_identityFile : "");
+ if (i_identityFile && !i_identityFile[0])
+ i_identityFile = NULL;
+ str = "ssh-add ";
+ if (i_identityFile) str += i_identityFile;
+ str += " < NUL 2> NUL";
+ verbose(str.c_str());
+ system(str.c_str());
+#endif
}
// http://sourceware.org/ml/cygwin/2006-02/msg00289.html
@@ -118,6 +140,7 @@
/* Convert POSIX to Win32 where necessary */
if (!strcmp(var, "PATH") ||
!strcmp(var, "LD_LIBRARY_PATH")) {
+#ifdef __CYGWIN__
winpathlist = (char *)
malloc(cygwin_posix_to_win32_path_list_buf_size(val) + 1);
if (winpathlist) {
@@ -125,12 +148,19 @@
SetEnvironmentVariable(var, winpathlist);
free(winpathlist);
}
+#else
+ SetEnvironmentVariable(var, val);
+#endif
} else if (!strcmp(var, "HOME") ||
!strcmp(var, "TMPDIR") ||
!strcmp(var, "TMP") ||
!strcmp(var, "TEMP")) {
+#ifdef __CYGWIN__
cygwin_conv_to_win32_path(val, winpath);
SetEnvironmentVariable(var, winpath);
+#else
+ SetEnvironmentVariable(val, winpath);
+#endif
} else {
SetEnvironmentVariable(var, val);
}
@@ -144,7 +174,11 @@
void run_program(char **i_argv)
{
char win32_pathname[1024];
+#ifdef __CYGWIN__
cygwin_conv_to_win32_path(i_argv[0], win32_pathname);
+#else
+ strcpy(win32_pathname, i_argv[0]);
+#endif
setup_win_environ();
if (i_argv[1])
@@ -386,7 +420,7 @@
NULL, NULL, g_hInst, this);
// show tasktray icon
- std::memset(&m_ni, 0, sizeof(m_ni));
+ memset(&m_ni, 0, sizeof(m_ni));
m_ni.cbSize = sizeof(m_ni);
m_ni.uID = ID_TaskTrayIcon;
m_ni.hWnd = m_hwndTaskTray;
@@ -513,7 +547,13 @@
verbose("DISPLAY=%s\n", getenv("DISPLAY"));
std::string askpassPath(getAskpassPath());
+#ifdef __CYGWIN__
setenv("SSH_ASKPASS", askpassPath.c_str(), 1);
+#else
+ std::string str("SSH_ASKPASS=");
+ str += askpassPath;
+ putenv(str.c_str());
+#endif
writeRegistry(HKEY_CURRENT_USER, "Environment", "SSH_AGENT_PID",
getenv("SSH_AGENT_PID"));
verbose("export SSH_AGENT_PID=%s\n", getenv("SSH_AGENT_PID"));
diff -u win-ssh-askpass-1.05.orig/misc.cpp win-ssh-askpass-1.05/misc.cpp
--- win-ssh-askpass-1.05.orig/misc.cpp 2002-09-23 03:32:39.000000000 +0900
+++ win-ssh-askpass-1.05/misc.cpp 2009-01-06 17:18:43.656250000 +0900
@@ -1,5 +1,7 @@
#include "misc.h"
+#ifdef __CYGWIN__
#include <sys/cygwin.h>
+#endif
// instance
HINSTANCE g_hInst = NULL;
@@ -74,9 +76,16 @@
// get the path of win-ssh-askpass.exe
std::string getSelfPath()
{
+#ifdef __CYGWIN__
char win32_pathname[1024];
GetModuleFileName(NULL, win32_pathname, NUMBER_OF(win32_pathname));
char posix_pathname[1024];
cygwin_conv_to_posix_path(win32_pathname, posix_pathname);
return posix_pathname;
+#else
+ char win32_pathname[1024], *ptr = win32_pathname;
+ GetModuleFileName(NULL, win32_pathname, NUMBER_OF(win32_pathname));
+ while (*ptr++) if (*ptr == '\\') *ptr = '/';
+ return win32_pathname;
+#endif
}
これを適応すると、mingw32だけでビルド出来る様になります(mingw32-make)。なお、cygwinでビルドすればこれまで通りのモジュールが出来上がります。
ビルド出来たら、win-ssh-agentを起動します。すると
の様に入力ダイアログが表示されます。ここにパスフレーズを入力すれば、後は次回から起動するコマンドプロンプトで
と環境変数が引き継がれる事になります。
「
Big Sky :: Windowsでもssh-agentとssh-addを使ってパスフレーズ入力を省略する。」で書いた、setxがあればwin-ssh-agentを使わなくても美味く行くのですが、コマンドプロンプトが出ないので少しカッコよくなりました。
これをWindowsのスタートアップに入れておけば初回にパスフレーズを入力した後は、sshパスフレーズを聞かれる事無くsshが行える様になります。
win-ssh-agent++
よろしければどうぞ。
というか私くらいしか、必要ないか...