2008/12/22


scaffoldで客寄せするのも悪くない...私はそうは思うんです。
scaffoldがあるから何秒でwebアプリケーションができました風の自慢はやめた方がいいと思う - val it : α → α = fun

この観点からいうと、scaffoldというのはつまりHQ9+である。

http://www.jmuk.org/diary/2008/12/20/1
確かに釣りな記事を書いた事には間違いないです。が、私が思うscaffoldの認識と少し違うかなと思ったので...

scaffoldはそもそも足場という意味の単語で、アプリケーションフレームワーク上では何かのベースとなる部品を提供する機能を一般的に指しているのだと思う。確かに「××分で出来る」ってのはbuzzword的に広まっており、「rails 分で出来る」なんてググると、わんさか出てきたりします。それだけインパクトがあり、需要があってこそなんだと思います。
mukaiさんの仰っておられるのは、「scaffoldで出来上がったデモがその後のユーザにとって本当に意味のある物になるのか...無駄じゃないのか」という事だと私は読み取ったのですが、巷のIDEの様に幾らか雛を作ってくれる物は初級者にとって敷居を下げてくれ、マニュアルやフレームワーク自身のコード解析からフレームワークの使い方を学ぶ手間と比べると、改造ベースからコーディングを始められる良さってのはあると信じています。
これは使い手側の意識と、先人が思う考察の違いの問題であって、例えばブログを書くのにWYSIWYGなツールを使う事は、HTMLを学べないから良くないとか、HTMLを知らなくてもHTMLを書けるWYSIWYGエディタの自慢をする事は良くないとか、そんな事はユーザにとってどうでもいい事の様な気がするんです。HTMLなんて知りたくないけどWYSIWYGは便利だよって人もいるだろうし、Google App Engineのマスタになりたいなんて思わないけどGoogle App Engineを使ってみたいって人はいると思う。
ユーザが開発者で、Google App Engineを学びたい人であって、Google App Engine Oilを知らなくて、何から手っ取り早くGoogle App Engine Oilで何が出来るかを知るには、多少特化したエッセンスを用いて表すのもアリなんじゃないかと。

例えば「10分で出来る、かんたん餃子レシピ」なんて物があって「その後本当に餃子の皮から作りたい人が、果たしてこのレシピを有用と見るだろうか」なんて考えるのはナンセンスな気がするんです。
調べる人は自分で調べるだろうし、「かんたん餃子レシピ」の役目は「餃子を自分で作ってみたいけど、皮から作るなんて難しい」と思ってる人への道しるべになれたのならそれで十分だと思う。そして本当に10分で美味しい餃子が出来たのなら、私はそのレシピを評価してあげて良いと思う。
Google App Engine Oilは、素のGoogle App Engineでは痒い所に手が届かない部分に色々と機能を追加していってくれていますし、良い意味でも悪い意味でもキャッチーです。
WSGIってどう書けば良いの?モデルを作ってそのモデルを編集する画面ってどう作ればいいの?って人は、まずサンプル触るのが良いと思うんですよね。
「そのフレームワークを学ぶ」が目的じゃなくて、「○○みたいなの作るにはどうしたら良いの?」が目的な人多いと思うんですよね。

これはあくまで私の持論であって、「自慢がいけない」のか「scaffoldがいけない」のかの違いも本筋でもない気がしています。

私は餃子を作りたいと思った時に「餃子のプロが見せる超絶技巧100」よりは「10分で出来る、かんたん餃子レシピ」を見たい人もいると思うんですよね。

ちなみに、お店で出すメニューに「10分で出来る、かんたん餃子レシピ」で作った餃子を出して自慢する...なんて事はもちろん、私も(その店にとって)良くない事だと思っていますよ。:-)

2008/12/18


Perlerと呼ばれる程のスキルは無いのですが...
Perlプログラマーの皆さん - Perl入門~サンプルコードによるPerl入門~

Perlをやっている人のブログをRSSリーダーに登録しておくと最新の事情がわかったりします。

http://d.hatena.ne.jp/perlcodesample/20081217/1229522517
Plaggerで上記リンクからOPML作るYAML書きました。
perler-in-japan.yaml
plugins:
  - module: Subscription::XPath
    config:
      url: http://d.hatena.ne.jp/perlcodesample/20081217/1229522517
      xpath: //div[@class="body"]/div[@class="section"]/blockquote/p/a

  - module: Publish::OPML
    config:
      filename: perler-in-japan.opml
出来上がったOPMLも置いておきます。2008/12/17 14:30:00時点のOPMLです。
Plagger Subscriptions - Perler in Japan
よろしければどうぞ。

2008/12/17


いつも面倒臭いなぁと思いながらブラウザでログインしてバージョン書いたりしてたんですが、勢い余ってスクリプトを書いてしまいました。
使い方は
# vim-release.pl -sv=<スクリプトのバージョン> -vv=<vimのバージョン> -msg=<メッセージ> <ファイル>
です。-vvを省略すると7.0が、-msgを省略すると入力プロンプトが表示されます。ユーザおよびパスワードはConfig::Pitで管理します。
実際には、このスクリプトの-svを省略する為の補助が欲しい所ですが、前のバージョンから+1では不味いし、スクリプトから調べるのは無理があるし...で引数にしました。
おそらく私くらいしか使わないかと思いますが、よろしければどうぞ。
gist: 36989 — GitHub
#!perl
use strict;
use warnings;
use Config::Pit;
use Getopt::Long;
use WWW::Mechanize;
use Perl6::Say;

my $conf = pit_get("vim.com", require => {
  "username" => "your username on vim.org", # NOTE: do not edit this line
  "password" => "your password on vim.org", # NOTE: do not edit this line
});

my %args = ( id => '', sv => '', vv => '7.0', msg => '' );
die "invalid args"
  unless GetOptions(\%args, 'id=i', 'sv=s', 'vv=f', 'msg=s');
unless ($args{msg}) {
  print "message: ";
  $args{msg} = <STDIN>;
}

my $file = shift;
die "script version not specified" unless $args{id};
die "message not specified" unless $args{msg};
die "file not specified" unless $file;

my $mech = WWW::Mechanize->new;
$mech->get('http://www.vim.org/login.php');
$mech->submit_form(
    form_name => 'login',
    fields    => {
        userName => $conf->{username},
        password => $conf->{password},
    },
);

$mech->get("http://www.vim.org/scripts/add_script_version.php?script_id=$args{id}");
$mech->form_name('script');
$mech->field(script_file => $file);
$mech->select('vim_version', $args{vv});
$mech->field(script_version => $args{sv});
$mech->field(version_comment => $args{msg});
my $res = $mech->click('add_script');
say $res->is_success ? "uploaded" : $res->status_line;
Gist.vim version 1.1 への更新で上手く動いてそげです。


最近Githubをよく使っているのですが、どうしてもGithubといえばプロジェクト管理なイメージがあり、プロジェクトを持ってない人から見ると少し遠い存在にみえがちです。ですが、ちょっとしたスクリプトを書きたい人、しかもリモートにもリポジトリを置きたい人いますよね。
そんな時に便利なのがGistです。
「Gistというと、単なるコードスニペットサービスでしょ?」と思われるかもしれませんが、Gistの凄いところはちょっとしたスクリプトをソース管理出来る所です。
以下、簡単なperlスクリプトをソース管理しながら完成まで仕上げる過程を例を持って示してみます。

ただし、ここで言っておきたい事が一つ。Gistを便利に使う為のツール、Gistyを入れるべきです。

GistyはGistにポストすると同時に所定の場所にgit cloneしてくれ、初期ポストからコーディング開始までをスムーズに促してくれます。

ファイル名を決める

まぁ、やりたい事が見つかっているならば、既にファイル名は決まっているでしょうね。
% vi cookpad.pl
% cat cookpad.pl
#!perl
use strict;
use warnings;
出来てなくても良いです。とりあえずpostしましょう。
なお、Gistyでポストする前には、cloneするディレクトリを環境変数GISTY_DIRに設定しておきましょう。
% gisty post cookpad.pl
恥ずかしいならばprivate postでもいいでしょう。
% gisty private_post cookpad.pl
Initialized empty Git repository in /home/mattn/gisty/aea6b5797ba2b83a6dc8/.git/
Enter passphrase for key '/home/mattn/.ssh/id_rsa':
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3/3), done.

% ls /home/mattn/gisty
118f0658700dce0c48df

%
このポストが成功した時点で、環境変数GISTY_DIRで設定したディレクトリに「aea6b5797ba2b83a6dc8」というディレクトリが出来ています。作ったファイルは実はもう要りません。出来たディレクトリに移動して作業を開始します。
なお、先日ご紹介した「Big Sky :: SSHポートが通らなくてもgithub.comにpushする方法」に付け足して"~/.ssh/config"に
Host gist.github.com
    User git
    Hostname ssh.github.com
    Port 443
    IdentityFile /home/mattn/.ssh/id_rsa
    #IdentityFile /home/mattn/.ssh/id_rsa.ppk
    TCPKeepAlive yes
    IdentitiesOnly yes
としておけば、SSHポートが通らなくてもGistをclone/push出来ます。

コーディングする

ま、ここは人によりいろいろですね。私の場合は
#!perl
use strict;
use warnings;
use utf8;
use URI;
use Encode;
use Perl6::Say;
use Web::Scraper;

if ( $^O eq "MSWin32" ) {
    binmode STDOUT, ":encoding(cp932)";
    @ARGV = map { Encode::decode( "cp932", $_ ) } @ARGV;
}
my $menu = shift || '味噌汁';

my $recipes = scraper {
    process 'div.recipe-text', 'recipes[]' => scraper {
        process 'a.recipe-title', title => 'TEXT',
        process 'a.recipe-title', url => '@href',
    };
    result 'recipes';
}->scrape( URI->new("http://cookpad.com/レシピ/$menu") );

say $_->{title}, "\n\t", $_->{url} for ( @{$recipes} );
こんな感じに仕上げました。
その都度、何度か
# git commit -a
してcommitしています。

リモートにも反映させる

# git push origin master
Enter passphrase for key '/home/mattn/.ssh/id_rsa':
Counting objects: 5, done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 617 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@gist.github.com:aea6b5797ba2b83a6dc8.git
   af1ff17..24c4fd9  master -> master
private postじゃない場合なら、誰かがforkして改良してくれるかもしれませんね。

Github/Gist、Gistyって便利

コーディングの時間を除くと、Gistへの登録やGitでの作業に費やした時間はほぼ十数秒しかありませんでした。
個人のスクリプトをgitでソース管理してくれるGithub、そしてGistを便利にしてくれるGistyすばらしいですね。

皆さんもちょっとしたスクリプトの作成をGist/Gistyから始めてみませんか。

2008/12/16


GAEOにScaffoldのジェネレータが付いた。どれだけ速くアプリケーションを作れるか。

アプリケーションを作る

# gaeo.py gaeotter
The "gaeotter" project has been created.

# cd gaeotter

Scaffoldを作る

# gaeogen.py scaffold posts index new show user:StringProperty(required=True) comment:StringProperty(required=True)
Creating Model posts ...
Creating .../gaeotter/application/model/posts.py ...
Creating .../gaeotter/application/templates/posts ...
Creating .../gaeotter/application/templates/posts/index.html ...
Creating .../gaeotter/application/templates/posts/new.html ...
Creating .../gaeotter/application/templates/posts/show.html ...
Creating Controller posts ...
Creating .../gaeotter/application/controller/posts.py ...

テンプレートを少しだけ編集する

# vim application/templates/posts/index.html
# cat !$
<h1>PostsController#index</h1>
<a href="/posts/new">New</a>
<ul>
{% for rec in result %}
    <li><a href="/posts/show?key={{ rec.key }}">{{ rec.comment|escape }}</a> by {{ rec.user|escape }}</li>
{% endfor %}
</ul>

# vim application/templates/posts/show.html
# cat !$
<h1>PostsController#show</h1>
<p>user: {{ user|escape }}</p>
<p>comment: {{ comment|escape }}</p>


起動する

# dev_appserver.py gaeotter
INFO     2008-12-16 05:03:03,546 appcfg.py] Server: appengine.google.com
INFO     2008-12-16 05:03:03,937 appcfg.py] Checking for updates to the SDK.
INFO     2008-12-16 05:03:04,437 appcfg.py] The SDK is up to date.
INFO     2008-12-16 05:03:05,062 dev_appserver_main.py] Running application gaeo
tter on port 8080: http://localhost:8080
# firefox http://localhost:8080/posts/
gaeo-example001

gaeo-example002

gaeo-example003


結論

Google App Engine Oilすばらしい。

2008/12/05


久々Plaggerネタ。めんどくさいので説明なしで...
スクリプト
hatena-news.pl
use strict;
use warnings;
use utf8;
use Web::Scraper;
use URI;

my $uri = URI->new( "http://news.hatelabo.jp/" );
my $entry_scanner = scraper {
    process 'h1.article', summary => 'TEXT';
    process 'div.section', body => 'RAW';
};

my $scanner = scraper {
    process '//td[.//span[text()="主なニュース"]]//ul/li',
        'entries[]' => scraper {
            process 'a',
                title => 'TEXT',
                link => '@href',
                info => sub {
                  $entry_scanner->scrape(
                      URI->new_abs( $_->attr('href'), $uri )
                  );
                }
        };
   result 'entries';
};

my $feed = {
    title => 'はてなニュース',
    link  => $uri->as_string,
};

for my $entry (@{ $scanner->scrape( $uri ) }) {
    push @{$feed->{entries}}, {
        title   => $entry->{title},
        link    => $entry->{link},
        summary => $entry->{info}->{summary},
        body    => $entry->{info}->{body},
    };
}

use YAML;
binmode STDOUT, ":utf8";
print Dump $feed;

hatena-news.yaml
global:
  log:
    level: error
plugins:
  - module: Subscription::Config
    config:
      feed:
        - script:///path/to/hatena-news.pl
  - module: CustomFeed::Script
  - module: Publish::Feed
    config:
      dir: /path/to/hatena-news
      filename: hatena-news.rss
      format: RSS
※Windowsで動かす人は環境変数PATHEXTを以下の様にしておく必要あり
set PATHEXT=%PATHEXT%;.PL

あと、出力先はご自由にPublish::なんちゃらで...
ま、その内フィード出来るだろけど。

2008/12/03


毎回良い物作るなぁ。
汎用ダウンローダっぽいのが欲しい - 冬通りに消え行く制服ガールは、夢物語にリアルを求めない。 - subtech http://subtech.g.hatena.ne.jp/cho45/20081202/1228220280
私がやったのはopen3の実装をwin32/open3に変えただけの簡単なお仕事です。
ただ、今非win32な環境が無いので元の動作が確認出来てません。動作に違いがあるのかもしれません。
両方試して動作が違っていればご報告下さい。
mattn's middown at master — GitHub http://github.com/mattn/middown/tree/master
安定すれば作者のcho45さんにpull requestしたいと思います。

ところで今回知ったのですが、rubyってshebangが"#perl"でもちゃんと処理してくれるんですね。

2008/11/21


追記2
動いた。どうやらjQuery lightboxと併用するとCPU100%になったり、lightboxのオーバーレイが変になったりするみたい。
調べた所、lightboxのdomReadyよりも先にTypePad Connectのjavascriptが読み込まれるとまずい様なので
$(function () {
    $.getScript("http://profile.typepad.com/services/embed/tpc/{your-id}/embed.js", function() {
        $.getScript("http://static.typepad.com/.shared/js/profile/blogside.js");
    });
});
この様にlightboxのdomReadyよりも後に読み込むように修正した。とりあえず動いている。
コメント歓迎です!

ただIEだとエラー出るなぁ...
unknown-yahoo
って事でIE以外の人、コメントよろしく!

さらに追記。Google ChromeやSafariでも重くなる。よってFirefoxとOperaだけ!
追記1
なぜだか、コメントが0件場合にCPUが100%になる現象が見られたので、一時的に外しました。
日を改めて調査し、出来れば導入したいと思います。

blosxomなので、flavourにそのまま貼り付け。
コードの生成はこちらから
Connect a blog - TypePad

Choose a blog to have TypePad Connect installed on

http://www.typepad.com/connect/site/blogs/create
↓の方にコメント欄ぽいのがあるので、よかったらコメント付けて見て下さい。

関連URL: YappoLogs: TypePad Connect Betaつけた

2008/11/20


追記

svn/trunkに取り込まれました。


ちょっとしたパッチですが...
まずエビデンス
remedie-ie6

そしてパッチ
Index: root/static/js/remedie.js
===================================================================
--- root/static/js/remedie.js   (revision 207)
+++ root/static/js/remedie.js   (working copy)
@@ -12,7 +12,7 @@
   current_id: null,
 
   initialize: function() {
-    if (!jQuery.browser.safari && !jQuery.browser.mozilla) {
+    if (!jQuery.browser.safari && !jQuery.browser.mozilla && !jQuery.browser.msie) {
       alert("Your browser " + navigator.userAgent + " is not supported.");
       return;
     }
@@ -170,7 +170,7 @@
           } else {
             alert(r.error);
           }
-        },
+        }
       });
     }
 
@@ -217,7 +217,7 @@
         dataType: 'json',
         success: function(r) {
           item.props.embed = { code: r.code };
-        },
+        }
       });
     }
 
@@ -377,20 +377,20 @@
         } else {
           alert(r.error);
         }
-      },
+      }
     });
   },
 
   newChannelDialog: function() {
     $.blockUI({
-      message: $("#new-channel-dialog"),
+      message: $("#new-channel-dialog")
     });
     return false;
   },
 
   importDialog: function() {
     $.blockUI({
-      message: $("#import-opml-dialog"),
+      message: $("#import-opml-dialog")
     });
     return false;
   },
@@ -443,7 +443,7 @@
         } else {
           alert(r.error);
         }
-      },
+      }
     });
     return false;
   },
@@ -484,7 +484,7 @@
           'div', { id: 'channel-items', className: "clear" }, null
         );
 
-        r.items.forEach(function(item) {
+        $.each(r.items, function(index, item) {
           remedie.items[item.id] = item;
           $("#channel-items").createAppend(
            'div', { className: 'channel-item channel-item-selectable', id: 'channel-item-' + item.id  }, [
@@ -504,7 +504,7 @@
                ],
                'h3', { id: 'channel-item-title-' + item.id,
                        className: item.is_unwatched ? 'channel-item-unwatched' : '' }, item.name,
-               'p', { className: 'item-infobox-description' }, item.props.description
+               'div', { className: 'item-infobox-description' }, item.props.description
              ],
              'div', { className: "clear" }, null
            ]
@@ -629,7 +629,7 @@
           $.event.trigger('remedieChannelUpdated', channel); // Fake updated Event to cancel animation
           alert(r.error);
         }
-      },
+      }
     });
   },
 
@@ -653,7 +653,7 @@
         } else {
           alert(r.error);
         }
-      },
+      }
     });
   },
 
@@ -665,7 +665,7 @@
       dataType: 'json',
       success: function(r) {
         $("#collection").children().remove();
-        r.channels.forEach(function(channel) {
+        $.each(r.channels, function(index, channel) {
           remedie.channels[channel.id] = channel;
           remedie.renderChannelList(channel, $("#collection"));
           remedie.redrawUnwatchedCount(channel);
@@ -769,5 +769,5 @@
       message.children("a.command-unblock").click($.unblockUI);
       $.blockUI({ message: message });
       return false;
-  },
+  }
 };
Index: Makefile.PL
===================================================================
--- Makefile.PL (revision 207)
+++ Makefile.PL (working copy)
@@ -21,6 +21,7 @@
 requires 'String::CamelCase';
 requires 'URI';
 requires 'XML::RSS::LibXML';
+requires 'Image::Info';
 
 build_requires 'Test::More';
 use_test_base;
単なるカンマ取りと、一部タグをpからdivに変えないと動かなかったのを修正したくらい。あとImage::Infoが無いと言われたのでMakefile.PLのrequiresに足しただけ。

今の所、私んところのIE7でもご機嫌良く動いてる。

2008/11/13


Gist GitHubが提供するコードスニペットサービス(という言い方で良いのかな?)、「Gist」をvimから扱えるvimscript「Gist.vim」を書いた。
内部はcurlコマンドを使っており
  • 一覧
  • 閲覧
  • ポスト
  • プライベートポスト
が出来る様になっています。導入にはcurlコマンドとgitコマンドが必要です(コマンドライン版gistコマンドやrubyは必要ありません)。ただし、グローバル変数に"github_user"と"github_token"さえ設定していればgitコマンドも必要ありません。
使い方は、適当にバッファを開いて
:Gist
とすればポストされ、公開URLがコマンドライン上に表示されます。
またビジュアル選択して
:'<,'>Gist
で部分的にポストする事も出来ます。なお引数として
:Gist -p
もしくは--private
を指定するとprivateモードでポストします。 GistのIDが分かっている場合には
:Gist XXXXX
とID指定でgistを開く事も出来ます。
さらに
:Gist -la
もしくは--listall
で全ユーザの最新gist一覧
:Gist -l mattn
もしくは--list mattn
でユーザ指定一覧、といった使い方も出来ます。

現在、github上で開発を進めておりvim.orgでも公開しています。
よかったら使ってみて下さい。



id:tokuhiromが良い物作ってくれたので、それを使ったブログエンジン書いてみた。
MENTA というウェブアプリケーションフレームワークをかいてみた - TokuLog 改めB日記

「CGI 用のウェブアプリケーションフレームワークにはどういうものが最適か」という問いに対する自分なりの解答。

http://d.hatena.ne.jp/tokuhirom/20081111/1226418572

名前は、「MENTOS(メントス)」。
mentos-weblog-engine
実質コードは以下の量くらい。

sub read_entry {
    my $file = shift;
    my $pubdate = strftime("%y-%m-%d %H:%M:%S", localtime((stat $file)[9])),
    my $content = read_file($file);
    $file =~ s!.*?([^/]+)\.txt$!$1!;
    $content =~ /^([^\n]+)\n\n(.*)/ms;
    return {
        id => $file,
        title => $1,
        pubdate => $pubdate,
        description => $2,
    }
}

# あなたのプログラム
sub do_index {
    my $id = param('id') || '';
    my $data_dir = config()->{application}->{data_dir};

    if ($id =~ /^(\d+)$/) {
        render('entry.html', read_entry("${data_dir}/${id}.txt"));
    } else {
        my @entries;
        for my $file (glob("${data_dir}/*.txt")) {
            push @entries, read_entry($file);
        }
        render('entries.html', \@entries);
    }
}

sub do_edit {
    my $id = param('id') || '';
    my $data_dir = config()->{application}->{data_dir};
    my $msg = '';
    my $entry = {};

    if ($ENV{'REQUEST_METHOD'} eq 'POST') {
        $entry->{id} = param('id') || '';
        $entry->{title} = param('title') || '';
        $entry->{description} = param('description') || '';
        $entry->{title} =~ s!\r!!g;
        $entry->{description} =~ s!\r!!g;
        my $password = param('password') || '';
        my $admin_password = config()->{application}->{password};
        if ($entry->{id} =~ /^(\d+)$/ && $password eq $admin_password) {
            $entry->{id} = time unless $entry->{id};
            utf8::decode($entry->{title});
            utf8::decode($entry->{description});
            write_file("${data_dir}/${id}.txt", $entry->{title}."\n\n".$entry->{description});
            redirect(config()->{application}->{blog_url});
            return;
        } else {
            $msg = 'パスワードが違います';
        }
    } else {
        $entry = read_entry("${data_dir}/${id}.txt") if -f "${data_dir}/${id}.txt";
    }

    render('edit.html', $entry, $msg);
}
MENTAを使ってどれだけ小さなコード量でブログエンジンが出来上がるかを試して見たかっただけなので、ブログと言いながらカテゴリやコメント、トラックバック等はありません。一応編集機能は持ち合わせています。
コードはcodereposのこの辺に置いときますので、適当に弄って遊んで下さい。
暇があれば、id:yappoのYacafiや、id:kazuhoのNanoAでも作ってみたいなぁ。

2008/11/10


さらにちょこっと弄ってset-branch/commitが使える様にした。
Ditz の Mercurial プラグイン - ursmの日記

例によって Mercurial のはないみたいだったので、Git のものをちょちょいと手直しして作ってみました。

http://d.hatena.ne.jp/ursm/20080729/1217345141
これまたgit.rbからパクっただけですが...
--- mercurial.rb.orig   2008-11-10 12:30:03.015625000 +0900
+++ mercurial.rb    2008-11-10 12:30:04.921875000 +0900
@@ -2,6 +2,7 @@
  
 module Ditz
   class Issue
+    field :hg_branch, :ask => false
     def hg_commits
       return @hg_commits if @hg_commits
       output = `hg log --template '{date|rfc822date}\t{author}\t{node|short}\t{desc|firstline}\n' #{pathname}`
@@ -45,5 +46,51 @@
 EOS
     end
   end
+  class Operator
+    operation :set_branch, "Set the hg feature branch of an issue", :issue, :maybe_string
+    def set_branch project, config, issue, maybe_string
+      puts "Issue #{issue.name} currently " + if issue.hg_branch
+        "assigned to hg branch #{issue.hg_branch.inspect}."
+      else
+        "not assigned to any hg branch."
 end
 
+      branch = maybe_string || ask("Mercurial feature branch name:")
+      return unless branch
+  
+      if branch == issue.hg_branch
+        raise Error, "issue #{issue.name} already assigned to branch #{issue.hg_branch.inspect}"
+      end
+  
+      puts "Assigning to branch #{branch.inspect}."
+      issue.hg_branch = branch
+    end
+  
+    operation :commit, "Runs hg-commit and auto-fills the issue name in the commit message", :issue do
+      opt :all, "commit all changed files", :short => "-a", :default => false
+      opt :verbose, "show diff between HEAD and what would be committed", \
+        :short => "-v", :default => false
+      opt :message, "Use the given <s> as the commit message.", \
+        :short => "-m", :type => :string
+    end
+  
+    def commit project, config, opts, issue
+  
+      args = {
+        :verbose => "--verbose",
+        :all => "--all",
+      }.map { |k, v| opts[k] ? v : "" }.join(" ")
+  
+      comment = "# #{issue.name}: #{issue.title}"
+      tag = "Ditz-issue: #{issue.id}"
+      message = if opts[:message]
+        "#{opts[:message]}\n\n#{tag}"
+      else
+        "#{comment}\n#{tag}"
+      end
+  
+      message = message.gsub("\"", "\\\"")
+      exec "hg commit #{args} --message=\"#{message}\""
+    end
+  end
+end
ちなみにmercurialとditzを使った開発方法はおおよそ以下の様な感じ。
# cd /path/to/my/repos/example
まず初期設定
# ditz init
# echo '- mercurial' >> .ditz-plugins
# cat > .ditz-config
--- !ditz.rubyforge.org,2008-03-06/config
mercurial_commit_url_prefix: .
^D
# ditz reconfigure
issueを追加
# ditz add
Title: 'foo' is not found in README.
Description (ctrl-d, ., or /stop to stop, /edit to edit, /reset to reset):
> need to add 'foo'
> .
Is this a (b)ugfix, a (f)eature, or a (t)ask? t
Issue creator (enter for "mattn <mattn.jp@gmail.com>"):
Comments (ctrl-d, ., or /stop to stop, /edit to edit, /reset to reset):
> .
Added issue example-1.
You may have to inform your SCM that the following files have been added:
  .ditz/issue-5486bdc5eeb7c4f6c2fc784a4a7c41d949d0791e.yaml

ソースを修正する
# echo 'foo' >> README
# ditz close example-1
Closing issue example-1: 'foo' is not found in README..
Choose a disposition:
  1) fixed
  2) won't fix
  3) reorganized
Disposition (1--3): 1
Comments (ctrl-d, ., or /stop to stop, /edit to edit, /reset to reset):
> added 'foo'
> .
Closed issue test2-2 with disposition fixed.
You may have to inform your SCM that the following files have been modified:
  .ditz/issue-5486bdc5eeb7c4f6c2fc784a4a7c41d949d0791e.yaml

コミットする
# ditz commit
summary付でコミットされている
# hg log
changeset:   6:720dc3bf4ef9
tag:         tip
user:        mattn <mattn.jp@gmail.com>
date:        Mon Nov 10 12:43:21 2008 +0900
summary:     # example-1: 'foo' is not found in README.
ditzの場合は、.ditzフォルダや.ditz-configもリポジトリに格納するのが通常の使い方みたいですね。

ところでditzってバッチっぽく動く様になればいろんなツールが出始めると思うのに勿体無い気がするなぁ。


最近のインターネットは便利になったもので、なんと美輪明宏のチンコがあるのかないのかを返してくれるWebAPIまである。インターネットリソースの大きさは今後もきっと拡大して行き、いずれはきっとGoogle Alertや、はてなアンテナ、はたまたEngadget Japanese等でのリーク流出等で実際の「あり/なし」が判明、本当に正しい結果が返って来る事になると思われる。

だがしかし、上記JSONサーバにはRSS/Feedが存在せず、実は「美輪明宏にはチンコがない」とユーザが初めて気付く為には、結果をメール等で通知する様なデーモンプロセスを稼働させてこのサービスを監視(ポーリング)しJSONをパースした結果としてユーザに通知するか、ユーザにとって使用頻度の高い乗用アプリケーションへチェック機能を付けユーザの操作(トリガ)でチェックを行う方法が選択肢となってしまう。たしかにポーリングは簡単に、かつ昼夜を問わず最新の情報を人手無しに監視し続ける事が出来る。
しかしながら、このサーバに負荷を掛けることは今後のインターネット発達が望まれる中、希少な有志の力を自ら塞ぎ込む形と成り得ない。
ここはトリガ形式を考えるのが適切である。

では、トリガ形式としてどんな常用アプリケーションが適切か。私が考えた結果がテキストエディタである。テキストエディタはユーザであればほぼ万人が使っている。今回私は、このテキストエディタの中でもギーク性が高いと言われるvimを選んだ。

まずは、何をもって「美輪明宏のチンコの有無」を確認するか。
テキストエディタであれば、入力補完がユーザのニーズに答えられる物になるだろう。
たとえば
美輪明宏にチンコは
まで入力し、<tab>を押下して「ある」もしくは「ない」が補完されたとすれば、それはかなり使い勝手のある物になると思われる。

運が良いことに、vimでは入力補完にフックメソッドを組み込む事が出来るため、今回私は、以下の様なソースを書いた。
func! s:CheckTimpo()
  " ここにデータ取得処理を入れる
endfunc
iabbr <silent> 美輪明宏にチンコは 美輪明宏にチンコは<C-R>=<SID>CheckTimpo()<CR>
入力I/Fに対するメソッド呼出部が完成した。
あとはJSONをどう解釈するか。ここで私にあるひらめきが。
たしかvim7のDictionaryは、JSON展開形式に似てたはず
そして以下のようなコードを書いた。
func! s:CheckTimpo()
  let ret = system("curl -s http://dzfl.jp/mojo/")
  let true = 'ある'
  let false = 'ない'
  exec "let json = " . ret
  return json['miwa']
endfunc
iabbr <silent> 美輪明宏にチンコは 美輪明宏にチンコは<C-R>=<SID>CheckTimpo()<CR>
以下にこのソースの解説を行う。
JSONサーバからは以下の形式でデータが戻される。
{ "miwa": true }
vim7ではDictionaryが導入されており、そのシンタックスは
{ key: value }
となる。つまりtrue/falseといった予約語を定義してやればJSON形式が扱える事になるのだ。
上記ソースではtrue/falseの定義を行った上でDictionaryへ値展開し、代入している。これによりvim7ではjsonという変数に対して
json["miwa"]
とアクセス出来るのである。true/falseはそれぞれ「ある」/「ない」と定義している為、もし美輪明宏にチンコがあれば
json["miwa"] == "ある"
また、美輪明宏にチンコがなければ
json["miwa"] == "ない"
となるのだ。
この値をスクリプト内メソッドCheckTimpoの戻り値として返し、入力補完を行えるようになる。
以上の処理手順により、vim7を使っているならば何時でも
美輪明宏にチンコは
とまで入力し、補完トリガと成り得るようなキー(例えばスペース)を入力すれば
美輪明宏にチンコはある
と入力補完されるのである。

今回は、vimとJSONでマッシュアップを行ったが、これはあくまで一例に過ぎない。これを応用すれば「某国会議員がカツラなのか」、「モー娘のあの娘はタバコを吸っているかどうか」が、テキストエディタから補完出来るのである。

皆さんも、色んなマッシュアップを試して見てほしいと切に感じている。

以下、今回作成したソース「miwa.vim」を公開しておく。

ダウンロード:
注意:vimの入力補完は、全構文を途切れなく入力する必要がある。

2008/11/06


UN*X版だとssh-agentの実行結果が
SSH_AUTH_SOCK=/tmp/ssh-suhGif2116/agent.2116; export SSH_AUTH_SOCK;
SSH_AGENT_PID=5672; export SSH_AGENT_PID;
echo Agent pid 5672;
みたいになっててログイン時に
eval `ssh-agent`
とするんですがWindowsだと、そう簡単には行かない。
@echo off
if not "%SSH_AGENT_PID%" == "" goto end
for /f "eol=; tokens=1,2 delims==;" %%1 in ('ssh-agent.exe') do (
 if "%%1" == "SSH_AUTH_SOCK" set SSH_AUTH_SOCK=%%2
 if "%%1" == "SSH_AGENT_PID" set SSH_AGENT_PID=%%2
)
ssh-add
:end
開発を始める前にコマンドプロンプトで1回これを動かすとパスフレーズを聞かれ、1回入力すれば以降は聞かれない様になる(sshはmsysGitに含まれる物を使ってる)。 さらにWindowsサポートツールに入ってる"setx.exe"を使えば、ユーザ環境変数への反映も出来る為、新しく起動したコマンドプロンプトでsshコマンドがSSH_AGENT_PID/SSH_AUTH_SOCKを見てくれるようになってもっとウマーとなるはず(↑のバッチでsetしている所でさらにsetxもする必要があるかな)。

とりあえずこれで楽になった。ウマー

2008/10/29


追記 最新版はgithubで作ってます。
mattn's nicodown at master — GitHub
http://github.com/mattn/nicodown/tree/master


適当だけど書いてみた。
タイトル取って来る所はlibxml使うの面倒臭かったのでXMLパーサ使わずベタで(実態参照文字あると変になるので気を付けて)。Windowsの場合だけWin32 APIでシフトJISにファイル名を変換しています。
荒いコードなので色々直し所がありますが、サンプルって事で。
//#define CURL_STATICLIB
#include <curl/curl.h>

#define HEX_DIGITS "0123456789ABCDEF"
#define IS_QUOTED(x) (*x == '%' && strchr(HEX_DIGITS, *(x+1)) && strchr(HEX_DIGITS, *(x+2)))

static char* response_data = NULL;  /* response data from server. */
static size_t response_size = 0;    /* response size of data */

static void
curl_handle_init() {
    response_data = NULL;
    response_size = 0;
}

static void
curl_handle_term() {
    if (response_data) free(response_data);
}

static size_t
curl_handle_returned_data(char* ptr, size_t size, size_t nmemb, void* stream) {
    if (!response_data)
        response_data = (char*)malloc(size*nmemb);
    else
        response_data = (char*)realloc(response_data, response_size+size*nmemb);
    if (response_data) {
        memcpy(response_data+response_size, ptr, size*nmemb);
        response_size += size*nmemb;
    }
    return size*nmemb;
}

int
main(int argc, char* argv[]) {
    CURLcode res;
    CURL* curl;
    char error[256];
    char name[256];
    char data[1024];
    char* buf = NULL;
    char* ptr = NULL;
    char* tmp = NULL;
    FILE* fp = NULL;
    int status = 0;

    // usage
    if (argc != 4) {
        fputs("usage: nicodown [usermail] [password] [video_id]", stderr);
        goto leave;
    }

    // default filename
    sprintf(name, "%s.flv", argv[3]);

    curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, &error);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_handle_returned_data);
    curl_easy_setopt(curl, CURLOPT_COOKIEJAR, "cookies.jar");
    curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt");

    // login
    sprintf(data, "mail=%s&password=%s&next_url=/watch/%s", argv[1], argv[2], argv[3]);
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, "https://secure.nicovideo.jp/secure/login?site=niconico");
    curl_easy_setopt(curl, CURLOPT_POST, 1);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }
    buf = malloc(response_size + 1);
    strcpy(buf, response_data);
    if (strstr(buf, "id=\"login_bar\"")) {
        printf("%s\n", buf);
        free(buf);
        fprintf(stderr, "failed to login\n");
        goto leave;
    }
    free(buf);
    curl_handle_term();

    // get video url, and get filename
    sprintf(data, "http://www.nicovideo.jp/api/getthumbinfo?v=%s", argv[3]);
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, data);
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }
    buf = malloc(response_size + 1);
    strcpy(buf, response_data);
    ptr = strstr(buf, "<title>");
    if (ptr) {
        ptr += 7;
        tmp = strstr(ptr, "</title>");
        if (*tmp) {
            *tmp = 0;
            strcpy(name, ptr);
        }
#ifdef _WIN32
        {
            UINT codePage;
            size_t wcssize;
            wchar_t* wcsstr;
            size_t mbssize;
            char* mbsstr;

            codePage = CP_UTF8;
            wcssize = MultiByteToWideChar(codePage, 0, name, -1,  NULL, 0);
            wcsstr = (wchar_t*)malloc(sizeof(wchar_t) * (wcssize + 1));
            wcssize = MultiByteToWideChar(codePage, 0, ptr, -1, wcsstr, wcssize + 1);
            wcsstr[wcssize] = 0;
            codePage = GetACP();
            mbssize = WideCharToMultiByte(codePage, 0, (LPCWSTR)wcsstr,-1,NULL,0,NULL,NULL);
            mbsstr = (char*)malloc(mbssize+1);
            mbssize = WideCharToMultiByte(codePage, 0, (LPCWSTR)wcsstr, -1, mbsstr, mbssize, NULL, NULL);
            mbsstr[mbssize] = 0;
            sprintf(name, "%s.flv", mbsstr);
            free(mbsstr);
            free(wcsstr);
        }
#endif
    }
    free(buf);
    printf("downloading %s\n", name);
    curl_handle_term();

    // get video url
    sprintf(data, "http://www.nicovideo.jp/api/getflv?v=%s", argv[3]);
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, data);
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }
    buf = malloc(response_size + 1);
    strcpy(buf, response_data);
    ptr = strstr(response_data, "url=");
    if (!ptr) {
        free(buf);
        fprintf(stderr, "failed to get video info\n");
        goto leave;
    }
    tmp = strstr(ptr, "&");
    if (tmp) *tmp = 0;
    tmp = ptr;
    while(*tmp) {
        if (IS_QUOTED(tmp)) {
            char num = 0;
            sscanf(tmp+1, "%02x", &num);
            *tmp = num;
            strcpy(tmp + 1, tmp + 3);
        }
        tmp++;
    }
    strcpy(data, ptr + 4);
    printf("URL: %s\n", data);
    free(buf);
    curl_handle_term();

    // download video
    fp = fopen(name, "wb");
    if (!fp) {
        fprintf(stderr, "failed to open file\n");
        goto leave;
    }
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, data);
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
    res = curl_easy_perform(curl);
    fclose(fp);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }

leave:
    curl_handle_term();
    curl_easy_cleanup(curl);

    return 0;
}
確認はWindowsでしかしてませんが、きっとUN*Xでも動くはず。perlやpythonやruby使わない派の方は雛形として持ってって下さい。



皆さん既に知ってたら御免なさい。ずっと知らなかったのでgithubには手を付けてませんでした。
githubは、cloneするとき
# git clone git://github.com/yappo/konnitiwa.git
の代わりに
# git clone http://github.com/yappo/konnitiwa.git
とgitをhttpにするとclone出来るのは知っていたのですが、pushはsshを使うのでport 22が開いてないとpush出来ない物と信じ込んでいました。
が、今日解決しました。以下私がWindowsで行った手順
Windowsじゃない人は最初の部分はすっ飛ばして下さい。

Git for Windowsを入れる

まずmsys Gitというのを入れます。
msysgit - Google Code
http://code.google.com/p/msysgit/downloads/list
注意点としては、PATH環境変数の設定具合によってはmsys Gitに含まれるコマンドライン郡の方が先に認識され、思う動きにならない可能性があります。例えば私の場合svn/trunkなvimを使っていますがmsys Gitにもvimが含まれている為、commit時にエラーが出たりします。

SSHキーを生成する

msys Gitに含まれるssh-keygenで生成します。
# ssh-keygen -C "xxx@example.com" -t rsa
以下省略

SSHキーをgithubに登録する

アカウントページにid_rsa.pubの値を貼り付けます。
https://github.com/account

github-ssh-pubkey
Titleは適当に"my windows abazure"と名付けました。

github.comへのssh接続を変更する

github.comへのSSH接続にはホスト名"ssh.github.com"、ポート"443"に接続する様に設定します。
※このssh.github.comが味噌です。
~/.ssh/config
Host github.com
    User git
    Hostname ssh.github.com
    Port 443
    IdentityFile c:/docume~1/mattn/.ssh/id_rsa
秘密鍵のパスをお間違え無く。

後はpushしまくれ

本当にpushしまくるのは良くないでしょうが...
yappo's konnitiwa at master — GitHub
こんにちわ
http://github.com/yappo/konnitiwa/tree/master
をforkし
# git clone git@github.com:mattn/konnitiwa.git
Enter passphrase for key 'c:/docume~1/mattn/.ssh/id_rsa':
remote: Counting objects: 9, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 9 (delta 0), reused 6 (delta 0)
Receiving objects: 100% (9/9), done.
とclone出来ますのでREADMEを編集後に
# git commit -a -m "added oops"
Created commit 109bbff: added oops
 1 files changed, 1 insertions(+), 0 deletions(-)
# git push Enter passphrase for key 'c:/docume~1/mattn/.ssh/id_rsa':
Counting objects: 5, done.
Writing objects: 100% (3/3), 264 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@github.com:mattn/konnitiwa.git
   66a8481..109bbff  master -> master
でpush出来ます。port 443なので防火壁内の人でもpush出来るかと思います。既にみんな知ってる話だと恥ずかしいですが...

それと昨日書いたニコニコ動画ダウンロードプログラムをgithubで作っていく事にしました。
mattn's nicodown at master — GitHub
http://github.com/mattn/nicodown/tree/master
codereposでも良かったのですが、なんなくgithubを使ってみたくなったので...

よいgithubライフを。

集合知プログラミング 集合知プログラミング
Toby Segaran
オライリージャパン / ¥ 3,672 (2008-07-25)
 
発送可能時間:通常4~5営業日以内に発送


2008/10/28


miyagawaさんがPlaggerのFetchNicoVideoからダウンローダとして抜き出してくれました。
Tatsuhiko Miyagawa / WWW-NicoVideo-Download-0.01 - search.cpan.org
http://search.cpan.org/~miyagawa/WWW-NicoVideo-Download-0.01/
中身はMooseを使ったモダンなコード。eg/fetch-video.plにそのまま使えそうなサンプルまで入っています。
サンプルではTerm::ProgressBarで進捗表示までされて至れり尽くせり。今日はちょっとだけ修正してファイル名をタイトルから名付ける様にしてみました。
といってもWWW::NicoVideo::Downloadではloginが単体で呼び出せられる様になっていますし、user_agentが得られるようになっているのでそれを同じくmiyagawaさん作のWeb::Scraperに渡しただけです。
#!/usr/bin/perl
use strict;
use warnings;
use Encode qw(encode decode_utf8);
use URI;
use Web::Scraper;
use WWW::NicoVideo::Download;
use Term::ProgressBar;

my($email, $password, $video_id) = @ARGV;

my($term, $fh);

my $client = WWW::NicoVideo::Download->new( email => $email, password => $password );
$client->login( $video_id );

my $helper = scraper { process '#subinfo img', title => '@alt' };
$helper->user_agent( $client->user_agent );

my $title = $helper->scrape(
    URI->new("http://www.nicovideo.jp/watch/$video_id")
)->{title} || $video_id;
$title = encode("cp932", decode_utf8($title)) if $^O eq 'MSWin32';

$client->download($video_id, \&cb);

sub cb {
    my($data, $res, $proto) = @_;

    unless ($term && $fh) {
        my $ext = (split '/', $res->header('Content-Type'))[-1] || "flv";
        my $filename = "$title.$ext";
        $filename =~ s![\\/|:<>"?*]!_!g;
        open $fh, ">", $filename or die $!;
        $term = Term::ProgressBar->new( $res->header('Content-Length') );
    }

    $term->update( $term->last_update + length $data );
    print $fh $data;
}
うむ。便利便利。

そういえばWEB+DB PRESS vol47買った。codereposの記事も面白かった。

WEB+DB PRESS Vol.47 WEB+DB PRESS Vol.47
高橋 徹, 遠藤 康裕, はまちや2, 正野 勇嗣, 前坂 徹, やまだ あきら, 笠谷 真也, 大沢 和宏, 縣 俊貴, ミック, 田中 洋一郎, 有賀 一輝, 石黒 尚久, 石室 元典, 長野 雅広, 池邉 智洋, 和田 正則, 下岡 秀幸, 伊藤 直也, nanto_vi, 武者 晶紀, 大塚 知洋, 山本 陽平, 高林 哲, 小飼 弾
技術評論社 / ¥ 21,615 (2008-10-23)
 
発送可能時間:


追記
otsuneさんから、API叩いた方が仕様変更に強いとコメント頂きました。XML::Simple使ってタイトル取る様に修正しました。
#!/usr/bin/perl
use strict;
use warnings;
use Encode qw(encode decode_utf8);
use XML::Simple;
use WWW::NicoVideo::Download;
use Term::ProgressBar;

my($email, $password, $video_id) = @ARGV;

my($term, $fh);

my $client = WWW::NicoVideo::Download->new( email => $email, password => $password );
$client->login( $video_id );

my $res = $client->user_agent->get("http://www.nicovideo.jp/api/getthumbinfo?v=$video_id");
my $title = $video_id;
if ($res->is_success) {
  my $xs = XML::Simple->new;
  my $ref = $xs->XMLin($res->decoded_content);
  $title = $ref->{thumb}->{title} || $video_id;
  $title = encode("cp932", decode_utf8($title)) if $^O eq 'MSWin32';
}

$client->download($video_id, \&cb);

sub cb {
    my($data, $res, $proto) = @_;

    unless ($term && $fh) {
        my $ext = (split '/', $res->header('Content-Type'))[-1] || "flv";
        my $filename = "$title.$ext";
        $filename =~ s![\\/|:<>"?*]!_!g;
        open $fh, ">", $filename or die $!;
        $term = Term::ProgressBar->new( $res->header('Content-Length') );
    }

    $term->update( $term->last_update + length $data );
    print $fh $data;
}


今のところ使い道見つからないけど、面白い。
Chris Grau / Export-Lexical - search.cpan.org

Export::Lexical - Lexically scoped subroutine imports

http://search.cpan.org/dist/Export-Lexical/
SYNOPSISをちょと変えて
use strict;
use warnings;
package Foo;

use Export::Lexical;
sub foo :ExportLexical {
    print "foo@_\n" or 1;
}
sub bar :ExportLexical {
    print "bar@_\n" or 1;
}
1;
を使う以下のスクリプト
use strict;
use warnings;
use Foo;

no Foo 'foo';

eval { foo(1); } or warn "foo1 is disabled.";
eval { bar(1); } or warn "bar1 is disabled.";

{
    use Foo 'foo';
    no Foo 'bar';

    eval { foo(2); } or warn "foo2 is disabled.";
    eval { bar(2); } or warn "bar2 is disabled.";
}

eval { foo(3); } or warn "foo3 is disabled.";
eval { bar(3); } or warn "bar3 is disabled.";
実行すると
foo1 is disabled. at hoge.pl line 7.
bar1
foo2
bar2 is disabled. at hoge.pl line 15.
foo3 is disabled. at hoge.pl line 18.
bar3
こんな動きをする。レキシカルスコープでnoが使える。ただnoとは言えどメソッド自体は定義されてるからstrictでも通るし、eval無しで実行してもエラーにはならない。

面白い。でも使い道が見つからない。

2008/10/27


以前、「はてなブックマークをPlaggerで同期する際の注意点」という記事で書いたXML::Feed::Atomが複数のsubjectを返せない件で、送ったパッチが取り込まれました。厳密にはちょっと変えられてますが。
はてなブックマークのRSSフィードにはblockquote引用文が含まれており、Atomを使った方が良いという記事でしたが難点としてPlaggerで使用しているXML::Feed::Atomには複数のsubjectつまりタグを返せる仕組みがなかった為、別のソーシャルブックマークへポストした際にタグが削られてしまう事になっていました。
簡単な例で言うと
use strict;
use warnings;
use XML::Feed;
my $feed = XML::Feed->parse(URI->new('http://b.hatena.ne.jp/mattn/atomfeed'))
    or die XML::Feed->errstr;
for my $entry ($feed->entries) {
  print $entry->link."\n";
  print "[$_]" for $entry->category;
  print $entry->summary->{body};
  print "\n";
}
というコードに対して、以前までのXML::Feed::Atomだと
http://b.hatena.ne.jp/mattn/20081024#bookmark-10541285
[coderepos]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10541123
[coderepos]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10538485
[wsgi]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10534904
[googleappengine]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10531400
[]てかRSS意識しすぎw そんなに気になるか。
http://b.hatena.ne.jp/mattn/20081024#bookmark-7152814
[webfont]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10530774
[vimperator]Include this patch f*ck でok
http://b.hatena.ne.jp/mattn/20081023#bookmark-10527748
[perl]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10529111
[chrome]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10522215
[catalyst]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10520297
[dojo]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10516769
[igoogle]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10513534
[ruby]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512893
[merb]merbでscaffold
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512804
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512802
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-9671938
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512759
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-9696628
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512388
[perl]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10498444
[lighttpd]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10504059
[]prototype.jsかぁ…。
http://b.hatena.ne.jp/mattn/20081022#bookmark-10502783
[]
http://b.hatena.ne.jp/mattn/20081021#bookmark-10489204
[vimperator]rcが先す
http://b.hatena.ne.jp/mattn/20081021#bookmark-10488856
[softbank]
http://b.hatena.ne.jp/mattn/20081021#bookmark-10484808
[blosxom]
http://b.hatena.ne.jp/mattn/20081021#bookmark-10488095
[]
http://b.hatena.ne.jp/mattn/20081020#bookmark-7686515
[テスト]TeSt
http://b.hatena.ne.jp/mattn/20081019#bookmark-10465795
[]
http://b.hatena.ne.jp/mattn/20081019#bookmark-10460236
[perl]
こんな形にタグが1つ以上出力されず、しかも空の配列として得られたりしていましたが、XML::Feed 0.22以降だと
http://b.hatena.ne.jp/mattn/20081024#bookmark-10541285
[coderepos][oops]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10541123
[coderepos]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10538485
[wsgi]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10534904
[googleappengine][appengine]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10531400
てかRSS意識しすぎw そんなに気になるか。
http://b.hatena.ne.jp/mattn/20081024#bookmark-7152814
[webfont]
http://b.hatena.ne.jp/mattn/20081024#bookmark-10530774
[vimperator]Include this patch f*ck でok
http://b.hatena.ne.jp/mattn/20081023#bookmark-10527748
[perl]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10529111
[chrome]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10522215
[catalyst]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10520297
[dojo]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10516769
[igoogle][google]
http://b.hatena.ne.jp/mattn/20081023#bookmark-10513534
[ruby][rails]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512893
[merb]merbでscaffold
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512804
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512802
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-9671938
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512759
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-9696628
[merb]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10512388
[perl][twitter][wassr]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10498444
[lighttpd]
http://b.hatena.ne.jp/mattn/20081022#bookmark-10504059
prototype.jsかぁ…。
http://b.hatena.ne.jp/mattn/20081022#bookmark-10502783

http://b.hatena.ne.jp/mattn/20081021#bookmark-10489204
[vimperator]rcが先す
http://b.hatena.ne.jp/mattn/20081021#bookmark-10488856
[softbank]
http://b.hatena.ne.jp/mattn/20081021#bookmark-10484808
[blosxom]
http://b.hatena.ne.jp/mattn/20081021#bookmark-10488095

http://b.hatena.ne.jp/mattn/20081020#bookmark-7686515
[テスト]TeSt
http://b.hatena.ne.jp/mattn/20081019#bookmark-10465795

http://b.hatena.ne.jp/mattn/20081019#bookmark-10460236
[perl]
こんな感じの出力になります。 今回の修正により手放しで、はてなブックマークAtomフィードをdelicios等にポスト出来る様になります。

めでたしめでたし

2008/10/16


追記

色付き表示(-R)を使った場合に対応出来てなかった様です。msmhrtさんが修正済みのパッチを書いてくれているのでそちらを使った方が良いです。

私は日常コマンドプロンプト(cygwinは使わない)で暮らすという間違ったWindowsの使い方をしているのですが、最近のソフトウェア配布物に含まれるソースやREADME.jaは殆どがutf-8で書かれており、特にPerlやPython等と言ったソースからドキュメントを見せる様な代物だと、現状utf-8が見れないWindows版のlessでは苦しかったりします。
本当ならば色んな文字コードに対応するのが良いかと思うのですが、最近Shift_JISやEUC-JPで書かれているコードも無いだろうし、もしあったとしてもそれは特例で
C:¥> nkf -s example.c | less
すれば言いだけですし...問題はデフォルトでutf8なコードが見れるページャ、かつ見ようと思えばShift_JISなファイルも見れるページャが欲しかったんです。
lessは文字コードの扱いとしてutf-8のみ特別な扱い方をしているのですが、それに用いられている utf-8 <=> ucs な関数があります。Windowsのwchar_tは2バイトですがサロゲートペアさえなければ見れるはず。
Downloading less
http://www.greenwoodsoftware.com/less/download.html
からソースを落として以下のパッチを当ててビルド
diff -u less-418.org/Makefile.wnm less-418/Makefile.wnm
--- less-418.org/Makefile.wnm   2007-06-16 07:06:23.000000000 +0900
+++ less-418/Makefile.wnm   2008-10-16 14:13:28.562500000 +0900
@@ -6,7 +6,7 @@
 CC = cl
 
 # Normal flags
-CFLAGS = /nologo /ML /W3 /GX /O2 /I "." /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /c
+CFLAGS = /nologo /MT /W3 /EHsc /O2 /I "." /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D"_CRT_SECURE_NO_WARNINGS" /c
 LDFLAGS = /subsystem:console /incremental:no /machine:I386
 
 # Debugging flags
@@ -14,7 +14,7 @@
 #LDFLAGS = /subsystem:console /incremental:yes /debug /machine:I386
 
 LD = link
-LIBS = user32.lib
+LIBS = user32.lib /nodefaultlib:libc.lib
 
 #### End of system configuration section. ####
 
diff -u less-418.org/screen.c less-418/screen.c
--- less-418.org/screen.c   2008-01-01 09:50:42.000000000 +0900
+++ less-418/screen.c   2008-10-16 18:55:27.562500000 +0900
@@ -2489,7 +2489,18 @@
    int len;
 {
 #if MSDOS_COMPILER==WIN32C
-   WriteConsole(con_out, text, len, NULL, NULL);
+   extern int utf_mode;
+   if (utf_mode) {
+       int n, m;
+       char* limit = text + len;
+       wchar_t *wtext = (wchar_t*) malloc((len * 2 + 1) * sizeof(wchar_t));
+       for (n = 0, m = 0; *text && n < len; n++)
+           wtext[m++] =  (wchar_t) step_char(&text, TRUE, limit);
+       WriteConsoleW(con_out, wtext, m, NULL, NULL);
+       free(wtext);
+   } else {
+       WriteConsole(con_out, text, len, NULL, NULL);
+   }
 #else
    char c = text[len];
    text[len] = '\0';
環境変数LESSCHARSETに"utf-8"を設定して置けばデフォルトでutf-8なファイルが見れる様になり、環境変数PAGERにlessを設定しておけばperldocで化ける事も無くなります。
less-win32-utf8
最近はマシンスペックが良くなって、毎回変換しても大して劣化しなくて良いですね。

2008/10/15


基本部はこちらを参照。
ニコニコ動画のサムネイル画像を取得する Perl スクリプト - Yet Another Hackadelic
http://d.hatena.ne.jp/ZIGOROu/20081014/1223991205
思いっきり個人用ぽいスクリプトになってしまいました...。
Plaggerでアレを適当なフォルダにガサーーーと落として、そのフォルダ上で走らせる。 ガサーーーのレシピはこんな感じ。
plugins:
  - module: Subscription::Config
    config:
      feed:
         - http://www.nicovideo.jp/mylist/7688389

  - module: Filter::FetchNicoVideo
    config:
      mail: xxxxx@example.com
      password: your-password
      dir: /path/to/download/
      download_comment: 1
      id_as_filename: 0
      # for windows
      #filename_encode: shift-jis
スクリプトはガサーーーの中に含まれるXMLをパースしてゴニョゴニョしている。
#!/usr/bin/perl

use strict;
use warnings;

use Encode;
use Template;
use XML::Simple;
use LWP::UserAgent;

my $ua = LWP::UserAgent->new( keep_alive => 4 );
my $xs = XML::Simple->new;

sub save {
    my ($content, $filename) = @_;
    open my $fh, '>', $filename;
    binmode $fh;
    print $fh $content;
    close $fh;
}

sub build {
    my $file = $_;

    # 動画IDと、ソートに用いるサーバ時刻を取得する
    my $ref = $xs->XMLin($file);
    my $video_id = substr($ref->{view_counter}->{id}, 2);
    my $time = $ref->{thread}->{server_time};

    # 動画ファイル名を得る(s/xml/flv/)
    $file =~ s/\.xml$//;
    $file = decode('cp932', $file) if $^O eq 'MSWin32';
    my $movie_file = "$file.flv";

    # ref: http://d.hatena.ne.jp/ZIGOROu/20081014/1223991205
    my $server_id = $video_id % 2 + 1;
    my $thumb_url = "http://tn-skr$server_id.smilevideo.jp/smile?i=$video_id";
    my $thumb_file = "sm$video_id.jpg";
    my $video_url = "http://www.nicovideo.jp/watch/sm$video_id";

    # サムネイルをダウンロード
    unless (-e $thumb_file) {
        my $res = $ua->get($thumb_url);
        &save($res->content, $thumb_file);
    }

    # " - "でファイル名を分割して先頭を題名に、
    my @info = split(/ - /, $file);
    return {
        title   => $info[0] || $file,
        speaker => $info[1] || $file,
        thumb   => $thumb_file,
        file    => $movie_file,
        time    => $time,
        url     => $video_url,
    };
}

# XMLファイル一覧から動画情報を組み立て、サーバ時刻でソートする
my @movies = sort {$a->{time} <=> $b->{time}}
    map(build($_), glob('*.xml'));

# Templateを使用してHTML出力
binmode STDOUT, ':encoding(utf-8)';
my $tt = Template->new({ UNICODE => 1, ENCODING => 'utf-8' });
$tt->process(\*DATA, {movies => \@movies});

__DATA__
<html>
<head>
<title>第1回 Coderepos Con</title>
<style type="text/css">
body { font-family: meiryo; background-color: black; color: white }
a { color: #333399 }
.file { color: orange; }
.movie { font-size: 20px; font-weight: bold; padding: 0.5em; }
.movie img { margin-right: 0.5em; border: 1px solid blue; }
</style>
</head>
<body>
<h1>第1回 Coderepos Con</h1>
<hr />
<div id="content">
[% FOREACH movie IN movies %]
<div class="movie">
    <img src="[% movie.thumb %]" style="float:left;" />
    Name: <a class="file" href="[% movie.file %]">[% movie.title %]</a><br />
    Speaker: [% movie.speaker %]<br />
    URL: <a href="[% movie.url %]">[% movie.url %]</a><br />
    <br clear="all"/>
</div>
[% END %]
</div>
</body>
</html>
" - "でタイトルとか分けてる部分は、この動画ファイルの名づけ方に依存しているので使う人は好き勝手に変えて下さい。
ちなみに動かすとこんなHTMLが出来上がる。
nico-playlist
いやぁ久々ソースにコメント書いたね!(えっ

2008/10/14


mingw32でビルド
C:¥TEMP> wget -O getclip.c "http://sources.redhat.com/cgi-bin/cvsweb.cgi/~checkout~/cygutils/src/clip/getclip.c?rev=1.2&content-type=text/plain&cvsroot=cygwin-apps"
C:¥TEMP> wget -O putclip.c "http://sources.redhat.com/cgi-bin/cvsweb.cgi/~checkout~/cygutils/src/clip/putclip.c?rev=1.2&content-type=text/plain&cvsroot=cygwin-apps"
C:¥TEMP> type common.h
#include <windows.h>
#include <stdio.h>
#include <popt.h>
#include <fcntl.h>

C:¥TEMP> gcc -o getclip.exe -I. getclip.c -lpopt
C:¥TEMP> gcc -o putclip.exe -I. putclip.c -lpopt
できあがり。
C:¥TEMP> ls *.exe | putclip
とか
C:¥TEMP> getclip | xargs rm
とか使えて便利。poptは「Gnuwin32 Popt」あたりから。

2008/10/10


ようやく動いた。
Apache2/mod_rubyでtDiaryを動かそうと思ったけど、エラーが出て動かなかった。どうやらrhtmlをERBでパースする所で出てるみたい。
今日はこの不可解な問題と向き合ったお話。
まずはmod_rubyをインストールするに当たりバイナリを取ってきたけどDLLのエントリポイントが見つからないとエラーが出たのでビルドする。
svn co http://svn.modruby.net/repos/mod_ruby/trunk mod_ruby
で最新を取って来て、以下のパッチを当てる。
Index: bucket.c
===================================================================
--- bucket.c    (revision 141)
+++ bucket.c    (working copy)
@@ -28,6 +28,10 @@
 #include "mod_ruby.h"
 #include "apachelib.h"
 
+#ifdef _WIN32
+# undef read
+#endif
+
 #ifdef APACHE2
 
 VALUE rb_cApacheBucket;
そして以下のMakefile.w32をmod_ruby直下に置く
APACHE_ROOT=C:\Program Files\Apache Software Foundation\Apache2.2
RUBY_ROOT=C:\Ruby
SRCS = \
    apache_cookie.c \
    apache_multipart_buffer.c \
    apache_request.c \
    apachelib.c \
    array_header.c \
    bucket.c \
    connection.c \
    cookie.c \
    error.c \
    mod_ruby.c \
    multival.c \
    paramtable.c \
    request.c \
    ruby_config.c \
    ruby_shared_stub.c \
    server.c \
    table.c \
    upload.c \
    uri.c

OBJS = $(SRCS:.c=.obj)
CFLAGS=/I"$(APACHE_ROOT)\include" /I"$(RUBY_ROOT)\lib\ruby\1.8\i386-mswin32" /DWIN32 /nologo
LDFLAGS=/LIBPATH:"$(APACHE_ROOT)\lib" /LIBPATH:"$(RUBY_ROOT)\lib"
LIBS= libapr-1.lib libaprutil-1.lib libhttpd.lib msvcrt-ruby18.lib

.SUFFIXES: .c .obj

all : mod_ruby.so

.c.obj:
    cl /c $(CFLAGS) /Fo$@ $<

mod_ruby.so : $(OBJS)
    link /nologo /DLL /OUT:$@ /EXPORT:ruby_module $(OBJS) $(LDFLAGS) $(LIBS)

clean :
    -@del /Q *.obj *.so
パス等は環境に合わせて修正して下さい。
コマンドラインからnmakeでビルド(mingw32の場合はもう一工夫いります)。出来上がったmod_ruby.soをApacheのmodulesディレクトリに配置。httpd.confは適当に設定。 次にtDiary。最新版を
svn co https://tdiary.svn.sourceforge.net/svnroot/tdiary/trunk/core tdiary
で取得して設定を済ませる。デフォルトの.htaccessはCGI版になっているので
Options +ExecCGI

# if you run tDiary with symbolic link, use settings below.
#Options +FollowSymLinks

# if making anchor style as 'YYYYMMDD.html', add some settings below.
# SEE header of html_anchor.rb plugin.

#AddHandler cgi-script .rb
AddHandler ruby-script .rb
DirectoryIndex index.rb
AddType application/xml .rdf

<Files "*.rhtml*">
    deny from all
</Files>

<Files "tdiary.*">
    deny from all
</Files>

<Files update.rb>
    AuthName      tDiary
    AuthType      Basic
    AuthUserFile  /home/foo/.htpasswd
    Require user  foo
    SetHandler ruby-object
    RubyHandler Apache::RubyRun.instance
</Files>

<Files index.rb>
    SetHandler ruby-object
    RubyHandler Apache::RubyRun.instance
</Files>
以上の様に修正する。さて実行...とまでは良かったが冒頭で書いたエラーが発生。色々調べている内にERBのresult(binding)メソッドでエラーが発生している事が分かった。
しかもコマンドラインから実行すると現象は発生しないけど、Apacheから動かすとエラーが発生する。さらに調べた所、rhtmlファイル内にあるタブ文字(0x09)が影響している事が分かり、tDiaryのskelディレクトリ内にある全てのrhtmlファイルに対してタブ文字からスペースに置き換える作用を行うと正しく動作する。
これでようやくApache2/mod_rubyで動くtDiaryが完成した。
tdiary-apache2-modruby-win32
ただ、どうしてもタブ文字でおかしくなる原因が分からない。RubyKanjiCodeを指定しても駄目。お手上げです。

誰か原因知りませんか?


Perl Quiz: qw - TokuLog 改めB日記 http://d.hatena.ne.jp/tokuhirom/20081010/1223607626
こうかな?
perl -MTest::More -e 'plan tests => 1;is(join(",",qw/a b/x3),"a,b,a,b,a,b")'

2008/10/08


よくvimrcにパスワードが書いてあるからcodereposで公開したくても出来ない!と目にするので、Pitで設定を管理出来るスクリプト書いた。
インストールは通常通りpluginフォルダに、起動には
  • perlインタフェース付きのvim
  • perlのConfig::Pitモジュール
が必要になります。
前者は頑張ってビルド、後者はcpanからインストールします。
使い方は
:PitLoad プロファイル名
で引数で指定されたプロファイルを読み込み、pitの設定内容をvimのグローバルスコープに読み込みます。文字列/数値/配列/ハッシュが扱え、それぞれvimのstring/number/List/Dictionaryに割り当てられます。
デフォルトではvimrcというプロファイル名が使用され、"g:pitconfig_default"で変更する事も出来ます。

また
:PitReload
で現在のプロファイルを再読み込みします。

プロファイルの保存は
:PitSave
と実行します。
:PitSave プロファイル名
として別のプロファイル名で保存する事も出来ます。現在のプロファイルに対して格納したい変数があれば
:PitAdd 変数名
と実行すれば、以降PitSaveで保存される対象となります。同様に
:PitDel 変数名
で削除です。
なお、Config::Pitに付属するppitや、ruby版に付属するpitでも設定を編集出来ます。コマンドラインから
# ppit set vimrc
とする事でテキストエディタが起動します。(環境変数EDITORの設定をお忘れなく)

よかったら使って見て下さい。
pitconfig.vim
さぁこれで「パスワードが保存されてるからcodereposでvimrcを公開出来ない」とは言わせませんよ。

2008/10/07


http://d.hatena.ne.jp/gfx/20081007/1223340787

こう?
#!perl -w
use strict;
use Test::More 'no_plan';
sub f{
    # 何かする
    package ::Foo;
}
f();
is ref(bless({}, 'Foo')), 'Foo';
__END__

2008/09/26


tokuhiromさんやkazeburoさんの言うように私の所もFastが速かった。
perl の memcached libraries の速度検証 - TokuLog 改めB日記

ワタシの環境だと、Cache::Memcached::Fast の方が速いようだ。

個人的には、 mixi のような大きなサービスでの採用実績があり、速度的にも速い C::M::Fast が現時点ではいい選択肢だとおもう。C::M::libmemcached は、まだまだ開発発展途上な感じなので、もうちょい落ち着くまでは仕事では使えない印象だし。

http://d.hatena.ne.jp/tokuhirom/20080926/1222408445

結果は以下の通り。
Module Information:
 + Cache::Memcached => 1.24
 + Cache::Memcached::Fast => 0.12
 + Cache::Memcached::libmemcached => 0.02008
 + Memcached::libmemcached => 0.2101

Library Information:
 + libmemcached => 0.24

Server Information:
 + localhost:11211 => 1.2.1

Options:
 + Memcached server: localhost:11211
 + Include no block mode (where applicable)? :NO

Prepping clients...

==== Benchmark "Simple get() (scalar)" ====
                  Rate perl_memcached   libmemcached memcached_fast
perl_memcached  2319/s             --           -83%           -86%
libmemcached   13387/s           477%             --           -18%
memcached_fast 16244/s           601%            21%             --
==== Benchmark "Simple get_multi() (scalar)" ====
                 Rate perl_memcached   libmemcached memcached_fast
perl_memcached  664/s             --           -81%           -91%
libmemcached   3509/s           428%             --           -52%
memcached_fast 7306/s          1000%           108%             --
==== Benchmark "Serialization with get()" ====
                  Rate perl_memcached   libmemcached memcached_fast
perl_memcached  2104/s             --           -77%           -81%
libmemcached    8964/s           326%             --           -19%
memcached_fast 11114/s           428%            24%             --
==== Benchmark "Simple get() (w/compression)" ====
                 Rate perl_memcached   libmemcached memcached_fast
perl_memcached  972/s             --           -43%           -43%
libmemcached   1706/s            75%             --            -1%
memcached_fast 1720/s            77%             1%             --
==== Benchmark "Simple set() (scalar)" ====
                  Rate perl_memcached   libmemcached memcached_fast
perl_memcached  5316/s             --           -55%           -71%
libmemcached   11851/s           123%             --           -36%
memcached_fast 18389/s           246%            55%             --
==== Benchmark "Simple set() (w/seriale)" ====
                 Rate perl_memcached   libmemcached memcached_fast
perl_memcached 3190/s             --           -49%           -52%
libmemcached   6201/s            94%             --            -7%
memcached_fast 6668/s           109%             8%             --
==== Benchmark "Simple set() (w/compress)" ====
                Rate perl_memcached   libmemcached memcached_fast
perl_memcached 315/s             --            -2%            -8%
libmemcached   322/s             2%             --            -6%
memcached_fast 342/s             8%             6%             --
Cache::Memcached::Fastのwin32版は、まだ汚いポーティングなので、後日codereposにあげる予定。

2008/09/25


オリジナルはperl版でGTKを使ったUIになっています。
memcachedclient-perl
これを、python(pygtk)、ruby(ruby-gnome2)、lua(lua-gtk)、C(GTK)、java(Swing)に移植してみた。
オリジナルから小さいコードなので、簡単なものですが...
興味のある方は、この辺を覗いて下さい。

2008/09/19


手順だけ。
cd C:\temp\
wget http://search.cpan.org/CPAN/authors/id/T/TI/TIMB/Memcached-libmemcached-0.2101.tar.gz
zcat Memcached-libmemcached-0.2101.tar.gz | tar xv
cd Memcached-libmemcached-0.2101
mkdir src_inst\include\libmemcached
mkdir src_inst\lib
svn co http://svn.coderepos.org/share/lang/c/libmemcached-win32/libmemcached-latest
cd libmemcached-latest\libmemcached
mingw32-make -f makefile.w32
cd ..\..
copy libmemcached-latest\libmemcached\*.h src_inst\include\libmemcached\.
copy libmemcached-latest\libmemcached\memcached.a src_inst\lib\.
copy src_inst\include\libmemcached\*.h src\libmemcached\libmemcached\.
perl Makefile.PL
set OPT="INC=-IC:\temp\Memcached-libmemcached-0.2101\src_inst\include" "LMCD_BUILT_LIB=C:\temp\Memcached-libmemcached-0.2101\src_inst\lib\memcached.a" "LDFROM=$(OBJECT) C:\temp\Memcached-libmemcached-0.2101\src_inst\lib\memcached.a"
nmake %OPT%
あとは
libmemcached-latest\libmemcached\memcached.dll
をパスの通る場所に置いておけばWindowsで
use strict;
use Perl6::Say;
use Memcached::libmemcached qw(
    memcached_create
    memcached_server_add
    memcached_set
    memcached_get
);

my $key = "foo";
my $value = "bar";
my $memc = memcached_create();
memcached_server_add($memc, '127.0.0.1');
memcached_set($memc, $key, $value);
say memcached_get($memc, $key);
こんなソースが通る様になります。Cache::Memcached::libmemcachedも動きます。
さらに言うなら、rubyのmemcachedも動きました。
require 'memcached'
$cache = Memcached.new('127.0.0.1:11211')
$cache.set('foo', 'bar')
print $cache.get('foo')
こちらはCOMPATIBILITYファイルを弄ったり、最新バージョンで無くなったWHEELな処理をカットしないといけませんが...
さらにさらにpython-libmemcachedも動きます。
import cmemcached

c = cmemcached.Client(['127.0.0.1:11211'])
c.set("foo", "bar")
print c.get("hoge")
こちらもcmemcached.pyxでWHEELな処理をカットしないといけませんが...
とりあえず、動くって事です。
ちなみにオフィシャルの反応はまだなし...

2008/09/18


色々やってたら、出来た。これで が動く様になるのではないか!
地味な作業だったけど、以外と効果は大きいかも。今日は時間無いので明日以降にソース綺麗にしてオフィシャルにパッチを送ってみようかと思ってます。
ちなみにUNIXドメインソケットなmemcachedは動きません。(win32ですからね...)

mingw32でコンパイルすると、こんなソースがコンパイル実行出来ました。
#include <winsock2.h>
#include <memcached.h>
#include <stdio.h>

int main(void) {
    memcached_return rc;
    memcached_st *memc;
    char* value;
    int value_length = 0;
    int flags = 0;

    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0) {
        return -1;
    }
    memc = memcached_create(NULL);;
    rc = memcached_server_add(memc, "127.0.0.1", 11211);
    printf("server add: %s\n", memcached_strerror(memc, rc));

    rc = memcached_set(memc, "test", 4, "example", 8, 0, 0);
    printf("set: %s\n", memcached_strerror(memc, rc));

    value = memcached_get(memc, "hoge", 4, &value_length, &flags, &rc);
    printf("get: %s\n", memcached_strerror(memc, rc));
    printf("test = %s\n", value);

    memcached_free(memc);
    return 0;
}
汚いソースを見たいという方はこの辺を...

2008/09/17


Google App Engineにファイルを転送しようとするとき、ファイルの数が多いと途中で失敗したり時間が掛かったりと結構嫌な思いをする事があったのですが、今日公開されたGAE SDK 1.3にてstaticファイルをZIPでまとめる事が出来る様になりました。
Google App Engine Blog: SDK 1.1.3 Now Available for download

Support for zipimport and a new module, zipserve, which serves static files from a zip archive. These allow you to work past the 1000-file app deployment limit.

The development console now includes a memcache viewer (you can use this by accessing http://localhost:8080/_ah/admin while your app is running on the SDK).

URLFetch now allows users to disable automatically following HTTP redirects.

We now allow composite indexes with repeated properties.

http://googleappengine.blogspot.com/2008/09/sdk-113-now-available-for-download.html
使い方も簡単。まず以下の様な構成のzipファイルを用意します。
Archive:  zippage.zip
  Length     Date   Time    Name
 --------    ----   ----    ----
      289  08/09/17 13:03   index.html
      203  08/09/17 12:53   test.html
     4233  08/01/30 15:55   logo.png
 --------                   -------
     4725                   3 files
そしてアプリケーションハンドラの設定を以下の様に設定します。
import wsgiref.handlers
from google.appengine.ext import webapp
from google.appengine.ext import zipserve

def main():
  application = webapp.WSGIApplication(
    [
      ('/zippage/(.*)', zipserve.make_zip_handler('zippage.zip')),
    ])
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
  main()
もちろんハンドラを分ければ、あるURLではpythonのハンドラ、あるURLではZIP内ファイルという事も出来ます。なお、動いている物はこの辺で確認出来ます。
他にも今回のバージョンアップにて開発者用コンソールにMemcache Viewerが使える様になりました。
gae-memcache-viewer
直接値を編集する事も出来ますが、現状マルチバイト文字でエラーが出ているようです。
他、URLFetchでURLの自動フォローを出来ないようになったり等の修正も行われています。

話は変わって、tokuhiromさん作のjavascript v8エンジンで動くCGIインタフェースllv8callですが、tokuhiromさんが入れ込んでる機能をwin32に移植して行っていますがlibmemcachedのwin32版がMSVCでビルド出来なかったので、自前でmemcacehdとお喋りする様にしました。興味がある方はこの辺のソースをご覧下さい。


2008/09/11


ちょっと訳あって、WScriptからjQueryを呼び出す必要があり(嘘です)作ってみました。

これを使うと
$.each([1,2,3], function(index, item) {
    print("foo" + item);
});
$.ajax({
    type: "GET",
    url: "http://www.google.co.jp/",
    async: false,
    success: function(data) {
        print(data);
    }
});
こんなソースが実行出来ます。
$.eachなんかは目茶目茶便利なので、使わない手はありません。とりあえず、$.ajaxでgoogle.co.jpのソースが取得出来るくらいは動きます。
以下全体ソース。
// vim:fdm=marker fdl=0 fdc=0 fdo+=jump,search:
// vim:fdt=substitute(getline(v\:foldstart),'\\(.\*\\){\\{3}','\\1',''):
// {{{
(function(target) {
    target.window = {
        document : {
            defaultView : {}
        },
        navigator : {
            userAgent : "Windows Scripting Host"
        },
        location : {},
        XMLHttpRequest : function() {
            // copied from http://la.ma.la/misc/js/ie_xmlhttp.js
            var self = this;
            var props = "readyState,responseText,responseXML,status,statusText".split(",");
            this.readyState  = 0;
            this.__request__ = new ActiveXObject("Microsoft.XMLHTTP");
            this.__request__.onreadystatechange = function(){
                for(var i=0;i<props.length;i++){
                    try{
                        self[props[i]] = self.__request__[props[i]]
                    }catch(e){
                    }
                }
                self.onreadystatechange()
                if(self.readyState == 4) self.onload();
            }
            this.onreadystatechange = function(){};
        },
        setInterval : function(func, interval) {
            func(); // quickly f*ckin hack
        },
        clearInterval : function(timer) {
        },
        setTimeout : function(func, interval) {
            func(); // quickly f*ckin hack
        },
        clearTimeout : function(timer) {
        }
    };
    var methods = "open','abort','send','setRequestHeader','getResponseHeader','getAllResponseHeaders".split("','");
    var make_method = function(name){
        window.XMLHttpRequest.prototype[name] = function(){
            var params = new Array(arguments.length);
            for(var i=0;i<params.length;i++) params[i] = "_"+i;
            return Function(
                params.join(","),
                ["return this.__request__.",name,"(",params.join(","),")"].join("")
            ).apply(this,arguments);
        }
    };
    for (var i=0;i<methods.length;i++) make_method(methods[i]);
    for (var n in window) target[n] = window[n];
})(this);

function print(msg) {
    WScript.StdOut.WriteLine(String(msg));
}

function require(source, target) {
    target = target||this;
    var fso = new ActiveXObject('Scripting.FileSystemObject');
    var stm = fso.OpenTextFile(source, 1, false, -2);
    var text = stm.ReadAll();
    stm.Close();
    eval(text, target);
    for (var n in windowif (typeof target[n] === 'undefined') target[n] = window[n];
}
// }}}

require("jquery-latest.js");

$.each([1,2,3], function(index, item) {
    print("foo" + item);
});

$.ajax({
    type: "GET",
    url: "http://www.google.co.jp/",
    async: false,
    success: function(data) {
        print(data);
    }
});
setTimeoutとかsetIntervalはf*ckingはhackなので$.get(非同期)は動きません。直す余地ありです。
あと、XMLHttpRequestのbindはmalaさんのをパクってます。


tokuhiromさん作のfcgi-v8をCGIインタプリタとして、cho45さん作のblosxom.rhinoを改造してblosxom.v8というのを作ってみた。
v8にはunevalが無かったのでdankogaiさん作のuneval.jsを使わせて頂いた。
実行画面はblosxom.rhinoの書き換えなので、パクリ気味ですが...
blosxom-v8
fcgi-v8でファイル日付の取得が実装出来ていないので、今は日付は表示されていません。
blosxom.v8
よかったら遊んでみて下さい。

2008/09/10


ひさびさPlagger弄り。
Lingua::JA::Galを使ってフィードテキストをギャル語に変換します。
注意する点はHTMLなフィードをプレーンテキストに置き換えてしまう点です。
これでPublish::MixiDiary使ってギャルなマイミク装ってみませんか。
コードはこの辺に。

Lingua::JA::Gal++

2008/09/05


shell.ccから改造したから分からなくなったんだよね。
v8::Objectに内部保持するnativeな値(例えばハンドルとか)を保持するには
FILE* fp = fopen(...);
v8::Local<v8::Object> obj = v8::Object::New();
obj->Set(v8::String::New("value"), v8::External::New((void*)fp));
という様に、v8::Externalでメンバを追加してやれば良い。
ただ分からなくなってた原因としては、shell.ccが持ってるPrintという関数が
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::AsciiValue str(args[i]);
    printf("%s", *str);
  }
  printf("\n");
  return v8::Undefined();
}
となってて、何でも文字列化してくれる様に思えてた。で実際
var obj = test();
print(obj.value); // ここでクラッシュ
こんな事やると落ちてしまうんだけど、良く考えたらExternalって言ってるんだから値の出しようもない。確かに以下の様なコードをshell.ccに含ませ
global->Set(v8::String::New("hoge"), v8::External::New(NULL));
javascriptから"print(hoge)"で例外を発生させた際のデバッグ出力で確かめたら
==== Stack trace ============================================

(No security context)
    1: DefaultString(this=00C104BD <JS Object>#0#,x=00E0068D <Proxy>#1#)
(No security context)
    2: ToString(this=00C104BD <JS Object>#0#,x=00E0068D <Proxy>#1#)
    6: arguments adaptor frame: 1->0
Security context: 00E006A1 <FixedArray[41]>#2#
    7: /* anonymous */(this=00C10361 <JS Global Object>#3#)

==== Details ================================================

(No security context)
[1]: DefaultString(this=00C104BD <JS Object>#0#,x=00E0068D <Proxy>#1#) {
  // stack-allocated locals
  var v = 01000135 <undefined>
  var s = 01000135 <undefined>
  // expression stack (top to bottom)
  [04] : 01000361 <String[8]: toString>
  [03] : 00E0068D <Proxy>#1#
  [02] : 00E0068D <Proxy>#1#
--------- s o u r c e   c o d e ---------
function DefaultString(x) {?  if ((typeof(x.toString) === 'function')) {?    var s = x.toString();?    if (%IsPrimitive(s)) return s;?  }?  if ((typeof(x.valueOf) === 'function')) {?    var v = x.valueOf();?    if (%IsPrimitive(v)) return v;?  }?  throw %MakeTypeError('cannot_convert_to_primitive', []);?}
-----------------------------------------
}

(No security context)
[2]: ToString(this=00C104BD <JS Object>#0#,x=00E0068D <Proxy>#1#) {
  // expression stack (top to bottom)
  [02] : 01002845 <String[13]: DefaultString>
  [01] : 00C104BD <JS Object>#0#
  [00] : 0100232D <String[8]: ToString>
--------- s o u r c e   c o d e ---------
function ToString(x) {?  if ((typeof(x) === 'string')) return x;?  if ((typeof(x) === 'number')) return %NumberToString(x);?  if ((typeof(x) === 'boolean')) return x ? 'true' : 'false';?  if ((typeof(x) === 'undefined')) return 'undefined';?  return ((x === null)) ? 'null' : %ToString(%DefaultString(x));?}
-----------------------------------------
とNULL参照している事が分かる。v8からすると"訳分かんない物をToStringして表示しようとしただけさ。"と言ったところか。
つまりはv8::Externalの使い方が悪いわけではなく、v8::Externalを表示しようとしてたPrint関数がマズイのだ。さらに元々Externalなんか扱ってなかったshell.ccにExternalを埋め込んだ犯人が悪い。そう...私。
言ってしまえばExternal組み込んだんだから、Printもちゃんと修正しろよって事ですね。
if (argv[0]->IsExternal()) {
    printf("[native code]");
    continue;
}
よくブラウザなんかで見かける"[native code]"という表示になる様にしました。
この修正は「Google の JavaScript エンジン v8 で FastCGI する - TokuLog 改めB日記」での成果物"fcgi-v8"にも組み込んであります。
ただ、Extenalに保持する様な値というのは、ハンドルとかメモリ確保した領域だとか、状態を保持する様な物のはずなので、実際には確保したポインタ等のリストをグローバルな領域にを持っておき、終了時に解放するかPersistentなObjectTemplateでインスタンスを作り、WeakReferenceCallbackにて解放するのがベターという事になる。
Externalを使う側からすると、そこに何が入っててもおかしい訳でもなく、v8として取り決めもない(引数の型はvoidポインタ)。よって複数の種類を格納したい場合は種類も判別出来る物、例えば
typedef enum _OBJECT_TYPE {
    OBJECT_TYPE_FILE_POINTER, // FILE*
    OBJECT_TYPE_WINDOW_HANDLE // ウィンドウハンドル
} OBJECT_TYPE;

typedef struct _OBJECT_VALUE {
    OBJECT_TYPE type;
    void* object;
} OBJECT_VALUE;

// 設定
OBJECT_VALUE* val = new OBJECT_VALUE;
val->type = OBJECT_TYPE_FILE_POINTER;
val->object = fopen("test.dat", "rb");
obj->Set(v8::String::New("value"), v8::External::New((void*)val));

// 取得
v8::Local<v8::Value> valueobj = obj->Get(v8::String::New("value"));
if (!valueobj->IsExternal()) return v8::ThrowException(v8::String::New("Exception: invalid object"));
OBJECT_VALUE* val = static_cast< OBJECT_VALUE* >(v8::Handle<v8::External>::Cast(valueobj)->Value());
if (val->type == OBJECT_TYPE_FILE_POINTER) {
    // FILE*な処理
} else
if (val->type == OBJECT_TYPE_WINDOW_HANDLE) {
    // ウィンドウハンドルな処理
}
こんな構造と設定および取得をすべき、という事になる。ここまで気付くのに結構時間を使ってしまった。

さてこのPrint関数ですが、↑のリンクにある"fcgi-v8"には「Big Sky :: Google Chromeが使っているjavascript v8エンジンにTwitterにアクセス出来るクラスを作ってポストする。」でご紹介したtwitter-v8とは違う方法でPrintを実装してあります。
twitter-v8の方はコンソールに出力する物なのでsetlocaleとワイドキャラクタ出力(%S)でOKですが、fcgi-v8の場合はUCSからUTF-8への変換を組み込んであります。一応v8のプロジェクトサイトでIssue 17 : 「Add Strign::WriteUtf8」としてリクエストされているので、もしかすると今後このコードは必要なくなるかもしれませんね。
v8エンジンは高速な上、今まで私が見た中で一番組み込みやすい代物になっているかと思います。皆さんもv8使った面白いもの作ってみませんか。

twitter-v8、fcgi-v8それぞれ参加者をお待ちしております。

2008/09/03


v8エンジン盛り上がってますね。
さて、今日はv8エンジンのshell.ccを弄って、シェル内部にTwitterオブジェクトを作ってみました。とはいっても、仕組みは簡単。FunctionTemplateから得たPrototypeTemplateにメソッドを追加、さらにInstanceTemplateにプロパティusernameとpasswordを足しているだけです。
  v8::Local<v8::FunctionTemplate> t = v8::FunctionTemplate::New();
  v8::Local<v8::Template> p = t->PrototypeTemplate();
  p->Set("friendsTimeline", v8::FunctionTemplate::New(TwitterFriendsTimeline));
  p->Set("updateStatus", v8::FunctionTemplate::New(TwitterUpdateStatus));
  v8::Local<v8::ObjectTemplate> i = t->InstanceTemplate();
  i->Set(v8::String::New("username"), v8::String::New(""));
  i->Set(v8::String::New("password"), v8::String::New(""));

  global->Set(v8::String::New("Twitter"), t);
このTwitterFriendsTimelineには以下の様なコードを...
v8::Handle<v8::Value> TwitterFriendsTimeline(const v8::Arguments& args) {
  v8::Handle<v8::Value> ret = v8::Undefined();
  v8::Local<v8::Object> This = args.This();
  v8::HandleScope handle_scope;
  v8::TryCatch try_catch;

  v8::String::AsciiValue username(This->Get(v8::String::New("username")));
  v8::String::AsciiValue password(This->Get(v8::String::New("password")));

  CURL* curl = NULL;
  CURLcode res = CURLE_OK;
  char *auth;

  response_data = NULL;
  response_size = 0;
  curl = curl_easy_init();
  if (!curl) return v8::ThrowException(v8::String::New("Error: unknown"));

  int n = strlen(*username) + strlen(*password);
  auth = (char*) malloc(n + 2);
  memset(auth, 0, n + 2);
  strcpy(auth, *username);
  strcat(auth, ":");
  strcat(auth, *password);

  curl_easy_setopt(curl, CURLOPT_URL, "http://twitter.com/statuses/friends_timeline.json");
  curl_easy_setopt(curl, CURLOPT_USERPWD, auth);
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, handle_returned_data);
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
  res = curl_easy_perform(curl);
  curl_easy_cleanup(curl);

  free(auth);

  if (res == CURLE_OK) {
    char* json = NULL;
    json = (char*) malloc(response_size+1);
    memset(json, 0, response_size+1);
    memcpy(json, (char*) response_data, response_size);
    v8::Handle<v8::String> source = v8::String::New(json);
    free(json);

    v8::Handle<v8::Script> script = v8::Script::Compile(source);
    if (script.IsEmpty()) {
        ret = v8::ThrowException(v8::String::New("Error: unknown"));
        goto leave;
    }
    v8::Handle<v8::Value> result = script->Run();
    if (script.IsEmpty()) {
        ret = v8::ThrowException(v8::String::New("Error: unknown"));
        goto leave;
    }
    if (!result->IsArray()) {
        ret = v8::ThrowException(v8::String::New("Error: unknown"));
        goto leave;
    }
    ret = result;
  }

leave:
  if (response_data) free(response_data);
  response_data = NULL;
  return ret;
}
さらにTwitterUpdateStatusには
v8::Handle<v8::Value> TwitterUpdateStatus(const v8::Arguments& args) {
  v8::Handle<v8::Value> ret = v8::Undefined();
  v8::Local<v8::Object> This = args.This();
  v8::HandleScope handle_scope;
  v8::TryCatch try_catch;

  if (args.Length() != 1) return v8::ThrowException(v8::String::New("Error: usage(message)"));

  v8::String::AsciiValue username(This->Get(v8::String::New("username")));
  v8::String::AsciiValue password(This->Get(v8::String::New("password")));

  v8::Handle<v8::Object> global = v8::Context::GetCurrent()->Global();
  v8::Local<v8::Function> func = v8::Function::Cast(*global->Get(v8::String::New("encodeURIComponent")));
  v8::Local<v8::Value> funcargs[1];
  funcargs[0] = args[0]->ToString();
  v8::Local<v8::Value> result_val = func->Call(global, 1, funcargs);
  v8::String::AsciiValue escaped(result_val->ToString());

  char* status = url_encode_alloc(*escaped);

  CURL* curl = NULL;
  CURLcode res = CURLE_OK;
  char *auth;
  char url[2048];

  response_data = NULL;
  response_size = 0;
  curl = curl_easy_init();
  if (!curl) return v8::ThrowException(v8::String::New("Error: unknown"));

  memset(url, 0, sizeof(url));
  strncpy(url, "http://twitter.com/statuses/update.xml", sizeof(url)-1);
  strncat(url, "?status=", sizeof(url)-1);;
  strncat(url, status, sizeof(url)-1);
  free(status);

  int n = strlen(*username) + strlen(*password);
  auth = (char*) malloc(n + 2);
  memset(auth, 0, n + 2);
  strcpy(auth, *username);
  strcat(auth, ":");
  strcat(auth, *password);

  curl_easy_setopt(curl, CURLOPT_URL, url);
  curl_easy_setopt(curl, CURLOPT_USERPWD, auth);
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, handle_returned_data);
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
  curl_easy_setopt(curl, CURLOPT_POST, 1);
  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
  res = curl_easy_perform(curl);
  curl_easy_cleanup(curl);

  free(auth);

  if (res != CURLE_OK) {
    ret = v8::ThrowException(v8::String::New("Error: unknown"));
  }
  if (response_data) free(response_data);
  response_data = NULL;
  return ret;
}
これだけ準備出来れば、あとは
var twitter = new Twitter()
twitter.username = "my-username"
twitter.password = "my-password"
var statuses = twitter.friendsTimeline();
for (var n = 0; n < statuses.length; n++) {
    print(statuses[n].user.screen_name, ":", statuses[n].text);
}
twitter.updateStatus("v8エンジン12気筒");
こんなスクリプト書いて
# shell twitter.js
とかやればfollowerのステータスが出た後でメッセージがポストされます。
簡単ですね。
ちなみに既存のPrintではAsciiValueを使いますが、AsciiValueではUTF-8が8bit落ちするのでsetlocaleを使ってワイドキャラでprintfする様修正しています。
コードはcodereposのこの辺に置いときます。汚いソースですが、よろしければ参考まで...


Tumblr - Post to Tumblr : vim online

Script that allows you to post regular type posts to Tumblr.

といっても3行
*** tumblr.vim.orig Wed Sep 03 14:49:50 2008
--- tumblr.vim  Wed Sep 03 14:47:15 2008
***************
*** 50,55 ****
--- 50,58 ----
      url = "http://www.tumblr.com/api/write"
      email = vim.eval("g:tumblr_email")
      password = vim.eval("g:tumblr_password")
+     enc = vim.eval('&encoding')
+     title = title.decode(enc).encode('utf-8')
+     body = body.decode(enc).encode('utf-8')
      data = urllib.urlencode({"email" : email, "password" : password, "title" : title, "body" : body})
      res = urllib.urlopen(url, data)
  

このパッチを当てたtumblr.vimをpluginフォルダに置き
let g:tumblr_email='your@example.com'
let g:tumblr_password='tumblr-password'
と設定し新しいバッファ
:TumblrNew
とすると
Title:
-- Post follows this line --
と表示されるので、Title部と破線以下に入力し、
:TumblrPost
と実行します。一応rangeにも対応出来ているらしいです。
Textしかポスト出来ませんが、改造すればQuoteも行けるんじゃないですかね。ただvimで編集中のテキストが引用元になるから何がQuoteか分かりませんが...


scons入れてwin32でビルドしてみたよ。ベンチマークも取ってみた。
環境はWindows XP、P4 3GHz CPU、1G memで。

まずmozillaのjsエンジン(ちょっと昔)
JavaScript-C 1.7.0 2007-10-03 (js.exe)
Richards: 87
DeltaBlue: 84
Crypto: 44
RayTrace: 78
EarleyBoyer: 87
----
Score: 74

次にrhino
Rhino 1.6 release 7 (win32 nativeビルド版)
Richards: 6
DeltaBlue: 5
Crypto: 4
RayTrace: 11
EarleyBoyer: 12
----
Score: 7

Rhino 1.7 release 7 2008 03 06(java版 js-14.jar)
Richards: 30
DeltaBlue: 30
Crypto: 24
RayTrace: 47
EarleyBoyer: 53
----
Score: 35

そしてv8エンジン
V8 version 0.2.5 (shell.exe)
Richards: 1020
DeltaBlue: 981
Crypto: 622
RayTrace: 580
EarleyBoyer: 1070
----
Score: 827
はぇーーーーーーーーーーーーーーー!(゚д゚;)

ベンチマークはbenckmarksフォルダにあるrun.jsで起動します。

追記
取り直したらこんな結果もでた
Richards: 1391
DeltaBlue: 1224
Crypto: 940
RayTrace: 774
EarleyBoyer: 1360
----
Score: 1110
もっと詳細が知りたい方は以下を参照。
Google V8 についてしらべてみました - TokuLog 改めB日記

2008/09/02


追記
teramakoさんがもっと良いの 作ってくれました。
きっとそっち使う方が幸せな、ともだちんこになれます。
ですので、私のはubiquityNeta.jsにリネームする事にします。


こう使うのですね?わかります。
(function() {
    if (typeof gUbiquity === 'undefined'return;
    liberator.commands.addUserCommand(["ubiquity"], "Ubiquity",
        function(arg, special){
            var anchor = document.getElementById("content");
            if (window.location == "chrome://browser/content/browser.xul")
                anchor = anchor.selectedBrowser;
            gUbiquity.openWindow(anchor);
            gUbiquity.__textBox.value = arg||'';
            var context = gUbiquity.__makeContext();
            var previewBlock = document.getElementById("cmd-preview").contentDocument.getElementById("preview");
            gUbiquity.__cmdManager.updateInput(arg||'', context, previewBlock);
            if (special) {
                gUbiquity.__cmdManager.execute(context);
                gUbiquity.closeWindow();
            }
        },
    { });
})();
使い方は
:ubiquity map osaka
でプレビュー表示
:ubiquity! map osaka
で直実行です。
ubiquity-vimperator
ソースはcodereposに置いておきます。
ubiquityNeta.js
ubiquityとvimperatorは、ともだちんこですね。わかります。

参考:
ゆびきちが相当面白い!! - ネットランダム

指基地なのか、指既知なのかは置いておいて、ユビキタス度アップ間違いなしのFirefoxアドオンである。

今後、Greasemonkeyにとって変わる可能性すらある。

っていうか、Vimperatorユーザ涙目!

http://d.hatena.ne.jp/fk_2000/20080901/p2

2008/08/29


適当ですが...
show-delicious-comment-if-hatena-bookmark-comment-is-disabled.user.js
これで今日も眠れますね。
しかし名前長いな。


Atompub使うと簡単。
use strict;
use warnings;

use Atompub::Client;
use XML::Atom::Entry;
use utf8;

my $username = 'xxxxxxxxxxxxxxxxxx';
my $password = 'xxxxxxxxxxxxxxxxxx';
my $uri = "http://d.hatena.ne.jp/$username/atom/draft";

my $client = Atompub::Client->new;

$client->username($username);
$client->password($password);

# 下書きを一覧
my @entries = $client->getFeed($uri);
for my $entry (@entries) {
    next if not $entry->id;
    warn $entry->link->href."\n".$entry->title."\n\n";
}

# 新規下書きエントリ
my $entry = XML::Atom::Entry->new;
$entry->title('下書きテスト');
$entry->content('はてダで下書き');
my $entry_uri = $client->createEntry($uri, $entry);
warn $client->errstr||'\n';

# 下書きを上書き
$entry->content('はてダで下書きを上書き');
$client->updateEntry($entry_uri, $entry);

# 下書きを削除
$client->deleteEntry($entry_uri);

# 公開用に新規下書きエントリ
$entry = XML::Atom::Entry->new;
$entry->title('下書き公開テスト');
$entry->content('はてダの下書きを公開してみるテスト');
$entry_uri = $client->createEntry($uri, $entry);
warn $client->errstr||'\n';

# 下書きを公開
$client->ua->default_header('X-HATENA-PUBLISH' => 1);
$client->updateEntry($entry_uri, $entry);
ゴミのエントリは、適当に消して下さい。
ところで1つも下書きエントリがない状態で、getFeedするとルートのlinkが取れてしまうのだけど、これは仕様だろうか。

2008/08/27


ま、ほぼ差異のないソースですが...
windowsでしか動作確認出来てないので、動かなかったら直して下さい。
よかったらもってって下さい。

2008/08/22


APIs for Finding Location

Google launched two APIs for finding the location of a user from a web application. The most simple way, which is also the least precise, is to derive some information about location from the IP. Google AJAX API has an object named google.loader.ClientLocation that includes "metro-level granularity properties": country, region, city, low resolution latitude and longitude. This could be useful if you want to customize a web page for a specific country or to prefill information about location. You probably noticed that google.com automatically redirects to the appropriate international domain and that when you create a Google account your country is already selected.

http://googlesystem.blogspot.com/2008/08/apis-for-finding-location.html

Developer's Guide - Google AJAX APIs - Google Code

When an application makes used of the AJAX API loader, the loader attempts to geo locate the client based on it's IP address. If this process succeeds, the client's location, scoped to the metro level, is made available in the google.loader.ClientLocation property. If the process fails to find a match, this property is set to null.

http://code.google.com/apis/ajax/documentation/#ClientLocation http://code.google.com/apis/gears/api_geolocation.html
試しにやってみたら"google.loader.ClientLocation.address.region"で"「大阪府」って出た。
あと"ClientLocation.latitude"と"ClientLocation.longitude"で緯度経度が得られるので、setCenterで中心位置を変更出来る。
google.load("maps", "2");
google.load("search", "1");
google.setOnLoadCallback(function() {
    var map = new google.maps.Map2(document.getElementById("map"));
    map.setCenter(new google.maps.LatLng(
        google.loader.ClientLocation.latitude,
        google.loader.ClientLocation.longitude), 10);
});
実行結果は以下

続きを読む...



Booで。
class _X:
  s as string

  def constructor():
    s = "ひだまり"

  static def op_LessThan(x as _X, v as string):
    print x.s, v
    return x
 
  static def op_Division(x as _X, v as object):
    if v.GetType() == typeof(int):
      x.s += "スケッチ"
    else:
      x.s += "365"
    return x

X = _X()
_ = 1

X / _ / X < "来週も見てくださいね!"
実行は
C:¥temp> booi yuno.boo
ひだまりスケッチ365 来週も見てくださいね!
でインタプリタ起動するか
C:¥temp> booc yuno.boo
Boo Compiler version 0.8.2.2960 (CLR v2.0.50727.832)

C:¥temp> yuno.exe
ひだまりスケッチ365 来週も見てくださいね!
でコンパイル実行。
boo初めて使ってみたけど、これいいかも。
monoでの実装とかあるのかな?

ちなみにpythonに似てるけど、静的型付けです。あとオペレータの判断順番がpythonと違うので、pythonのまま移植すると「ひだまり365スケッチ 来週も見てくださいね!」になるので要注意


2008/08/19


HTML::TreeBuilderは便利だけど、データ構造がロジックになってしまうのが難点。
Perlでブックオフの店舗を検索し、結果をハッシュの配列に格納する - As a Futurist...

HTMLを解析する練習です。Perlの配列とかハッシュの扱いも少し分かりました。 以下のブックオフの検索をPerlでやっただけです。

http://blog.riywo.com/2008/03/31/164327
ってことでWeb::Scraperで。

#!/usr/bin/perl -w

use strict;
use warnings;
use URI;
use Web::Scraper;
use YAML;

my $str = shift || '新宿';
my $uri = URI->new( 'http://www.bookoff.co.jp/shop/shop.php' );
$uri->query_form(
    action => 'search',
    station => $str,
    shop_name => $str,
);

warn Dump scraper {
    process '//tr[td]', 'res[]' => scraper {
        process '//td[1]', name => 'TEXT',
        process '//td[2]', time => 'TEXT',
        process '//td[3]', tel => 'TEXT',
        process '//td[4]', place => 'TEXT',
    };
    result qw/res /;
}->scrape( $uri );
Web::Scraper便利。
process書かずに
name => '//td[1]/text()'
とか書けるシンタックスシュガーあったらなぁ...とか思った。


既に試した方もいらっしゃるようですね。
visual_studio.vimを入れてみた - ampmmnの日記
TaskList : タスクリストの項目をquickfixに表示する。動作未確認。
Output : アウトプットの内容をquickfixに表示。動作未確認。
Solutions : これはVisualStudioを複数走らせている時に、どれと通信するかを選択するものらしい。自分の環境で実行したら、何かエラーメッセージが出力された。でも機能はしているようだ。
Projects : どのプロジェクトをアクティブ(スタートアップ)にするかを選択する。動作未確認。
http://d.hatena.ne.jp/ampmmn/20080809/1218230946
visual_studio.vim,python,unicodeencodeerror - gnarl、技術メモ
visual_studio.vimを導入してみたものの同梱pythonスクリプトのstr(name)のあたりでunicodeencodeerrorが出て困りましたね。
...
projectsメニューの一部がなぜか文字化けしますが
http://d.hatena.ne.jp/gnarl/20071012/1192181729
さまざまな言語のコメントON/OFFに対応した便利プラグイン - Seasons.NET
ただし、日本語版は、日本語エンコードに不具合があるので、アウトプット文字列が表示できないです。
http://d.hatena.ne.jp/Seasons/20070613/1181629449

もったいないので、日本語版Visual Studioでも動作する様に修正してみました。
ポイントは、DDE通信を行ってウィンドウを取得した後、ウィンドウキャプションで目的のサブウィンドウを探している所。
英語版で想定されているので、"Output"は"出力"、"Find Result 1/Find Result 2"は"検索結果 1/検索結果 2"に、"Build"は"ビルド"に修正する必要があります。
せっかくなので、vimに表示されるメニューも修正しました。見栄えはこんな感じ。
visual_studio_vim日本語版
一応、全てのメニューが動作する様にしてあります。
ちなみに、プロジェクト一覧の取得では
str(dict)
を使ってvimのDictionaryに引き渡している為、ダブルクォートでないと"\xXX\xXX"なマルチバイトを解釈出来ないvimでは正しく表示されていませんでした。
今回の修正では、simplejsonを使い
simplejson.dumps(obj, ensure_ascii=False).decode('utf-8').encode('mbcs')
を使うことでマルチバイトなvimのDictionary形式への変換を行っています。
ソース差分は以下の通り。
diff -cr plugin.orig/visual_studio.py plugin/visual_studio.py
*** plugin.orig/visual_studio.py    Fri Sep 28 20:51:49 2007
--- plugin/visual_studio.py Mon Aug 18 22:59:17 2008
***************
*** 1,3 ****
--- 1,4 ----
+ # -*- coding: ms932 -*-
  '''
  Companion file to visual_studio.vim
  Version 1.2
***************
*** 7,12 ****
--- 8,14 ----
  '''
  
  import os, re, sys, time, pywintypes, win32com.client
+ import simplejson
  
  import logging
  _logging_enabled = False
***************
*** 42,48 ****
          return
      # ExecuteCommand is not synchronous so we have to wait
      _dte_wait_for_build (dte)
!     dte_output (vs_pid, fn_quickfix, 'Output')
      _vim_status ('Compile file complete')
      _vim_activate ()
  
--- 44,50 ----
          return
      # ExecuteCommand is not synchronous so we have to wait
      _dte_wait_for_build (dte)
!     dte_output (vs_pid, fn_quickfix, '出力')
      _vim_status ('Compile file complete')
      _vim_activate ()
  
***************
*** 77,83 ****
          _dte_exception (e)
          _vim_activate ()
          return
!     dte_output (vs_pid, fn_quickfix, 'Output')
      _vim_status ('Build solution complete')
      _vim_activate ()
  
--- 79,85 ----
          _dte_exception (e)
          _vim_activate ()
          return
!     dte_output (vs_pid, fn_quickfix, '出力')
      _vim_status ('Build solution complete')
      _vim_activate ()
  
***************
*** 116,122 ****
          _dte_exception (e)
          _vim_activate ()
          return
!     dte_output (vs_pid, fn_quickfix, 'Output')
      _vim_status ('Build project complete')
      _vim_activate ()
  
--- 118,124 ----
          _dte_exception (e)
          _vim_activate ()
          return
!     dte_output (vs_pid, fn_quickfix, '出力')
      _vim_status ('Build project complete')
      _vim_activate ()
  
***************
*** 147,153 ****
      if not dte: return
      task_list = None
      for window in dte.Windows:
!         if str(window.Caption).startswith('Task List'):
              task_list = window
      if not task_list:
          _vim_msg ('Error: Task List window not active')
--- 149,155 ----
      if not dte: return
      task_list = None
      for window in dte.Windows:
!         if window.Caption.encode('mbcs').startswith('タスク一覧'):
              task_list = window
      if not task_list:
          _vim_msg ('Error: Task List window not active')
***************
*** 155,165 ****
      TL = task_list.Object
      for i in range (1, TL.TaskItems.Count+1):
          TLItem = TL.TaskItems.Item(i)
!         try: filename = TLItem.FileName
          except: filename = '<no-filename>'
          try: line = TLItem.Line
          except: line = '<no-line>'
!         try: description = TLItem.Description
          except: description = '<no-description>'
          print >>fp_task_list, '%s(%s) : %s' % (filename, line, description)
      fp_task_list.close ()
--- 157,167 ----
      TL = task_list.Object
      for i in range (1, TL.TaskItems.Count+1):
          TLItem = TL.TaskItems.Item(i)
!         try: filename = TLItem.FileName.encode('mbcs')
          except: filename = '<no-filename>'
          try: line = TLItem.Line
          except: line = '<no-line>'
!         try: description = TLItem.Description.encode('mbcs')
          except: description = '<no-description>'
          print >>fp_task_list, '%s(%s) : %s' % (filename, line, description)
      fp_task_list.close ()
***************
*** 170,176 ****
  
  def dte_output (vs_pid, fn_output, window_caption, notify=None):
      logging.info ('== dte_output %s' % vars())
!     if window_caption not in ['Find Results 1', 'Find Results 2', 'Output']:
          _vim_msg ('Error: unrecognized window (%s)' % window_caption)
          return
      dte = _get_dte(vs_pid)
--- 172,178 ----
  
  def dte_output (vs_pid, fn_output, window_caption, notify=None):
      logging.info ('== dte_output %s' % vars())
!     if window_caption not in ['検索結果 1', '検索結果 2', '出力']:
          _vim_msg ('Error: unrecognized window (%s)' % window_caption)
          return
      dte = _get_dte(vs_pid)
***************
*** 179,191 ****
      if not window:
          _vim_msg ('Error: window not active (%s)' % window_caption)
          return
!     if window_caption == 'Output':
!         owp = window.Object.OutputWindowPanes.Item ('Build')
          sel = owp.TextDocument.Selection
      else:
          sel = window.Selection
      sel.SelectAll()
!     lst_text = str(sel.Text).splitlines()
      lst_text = _fix_filenames (os.path.dirname(dte.Solution.FullName), lst_text)
      sel.Collapse()
      fp_output = file (fn_output, 'w')
--- 181,193 ----
      if not window:
          _vim_msg ('Error: window not active (%s)' % window_caption)
          return
!     if window_caption == '出力':
!         owp = window.Object.OutputWindowPanes.Item ('ビルド')
          sel = owp.TextDocument.Selection
      else:
          sel = window.Selection
      sel.SelectAll()
!     lst_text = str(sel.Text.encode('mbcs')).splitlines()
      lst_text = _fix_filenames (os.path.dirname(dte.Solution.FullName), lst_text)
      sel.Collapse()
      fp_output = file (fn_output, 'w')
***************
*** 280,286 ****
              startup_project_index = index
          lst_result.append (_dte_project_tree(project))
          index += 1
!     _vim_command ('let s:visual_studio_lst_project = %s' % lst_result)
      _vim_command ('let s:visual_studio_startup_project_index = %s' % startup_project_index)
  
  def _dte_project_tree (project):
--- 282,288 ----
              startup_project_index = index
          lst_result.append (_dte_project_tree(project))
          index += 1
!     _vim_command ('let s:visual_studio_lst_project = %s' % simplejson.dumps(lst_result, ensure_ascii=False).decode('utf-8').encode('mbcs'))
      _vim_command ('let s:visual_studio_startup_project_index = %s' % startup_project_index)
  
  def _dte_project_tree (project):
***************
*** 290,296 ****
      name = _com_property (project, 'Name')
      if not name:
          return []
!     name = str(name)
      properties = _com_property(project, 'Properties')
      if properties:
          try: full_path = str(properties['FullPath'])
--- 292,298 ----
      name = _com_property (project, 'Name')
      if not name:
          return []
!     name = name.encode('utf-8')
      properties = _com_property(project, 'Properties')
      if properties:
          try: full_path = str(properties['FullPath'])
***************
*** 441,447 ****
  def _dte_output_activate (vs_pid):
      dte = _get_dte(vs_pid)
      if not dte: return
!     window = _dte_get_window(dte, 'Output')
      if not window: return
      window.Activate()
  
--- 443,449 ----
  def _dte_output_activate (vs_pid):
      dte = _get_dte(vs_pid)
      if not dte: return
!     window = _dte_get_window(dte, '出力')
      if not window: return
      window.Activate()
  
diff -cr plugin.orig/visual_studio.vim plugin/visual_studio.vim
*** plugin.orig/visual_studio.vim   Thu Sep 27 05:57:47 2007
--- plugin/visual_studio.vim    Tue Aug 19 09:07:25 2008
***************
*** 226,232 ****
  
  function! DTEOutput()
      let &errorfile = g:visual_studio_output
!     call <Sid>DTEExec ('dte_output', &errorfile, 'Output')
  endfunction
  
  "----------------------------------------------------------------------
--- 226,232 ----
  
  function! DTEOutput()
      let &errorfile = g:visual_studio_output
!     call <Sid>DTEExec ('dte_output', &errorfile, '出力')
  endfunction
  
  "----------------------------------------------------------------------
***************
*** 234,243 ****
  function! DTEFindResults(which)
      if a:which == 1
          let &errorfile = g:visual_studio_find_results_1
!         let window_caption = 'Find Results 1'
      else
          let &errorfile = g:visual_studio_find_results_2
!         let window_caption = 'Find Results 2'
      endif
      call <Sid>DTEExec ('dte_output', &errorfile, window_caption)
  endfunction
--- 234,243 ----
  function! DTEFindResults(which)
      if a:which == 1
          let &errorfile = g:visual_studio_find_results_1
!         let window_caption = '検索結果 1'
      else
          let &errorfile = g:visual_studio_find_results_2
!         let window_caption = '検索結果 2'
      endif
      call <Sid>DTEExec ('dte_output', &errorfile, window_caption)
  endfunction
***************
*** 299,305 ****
      endif
      if gui_menu
          echo 'Found '.len(s:visual_studio_lst_dte).' VisualStudio instance(s)'
!         popup! VisualStudio.Solutions
      else
          call <Sid>DTESolutionTextMenu()
      endif
--- 299,305 ----
      endif
      if gui_menu
          echo 'Found '.len(s:visual_studio_lst_dte).' VisualStudio instance(s)'
!         popup! VisualStudio.ソリューション(&S)
      else
          call <Sid>DTESolutionTextMenu()
      endif
***************
*** 307,313 ****
  
  function! <Sid>DTESolutionGuiMenuCreate()
      try
!         aunmenu VisualStudio.Solutions
      catch
      endtry
      let menu_num = 0
--- 307,313 ----
  
  function! <Sid>DTESolutionGuiMenuCreate()
      try
!         aunmenu VisualStudio.ソリューション(&S)
      catch
      endtry
      let menu_num = 0
***************
*** 320,331 ****
          else
              let leader = '\ \ \ &'.menu_num
          endif
!         exe 'amenu <silent> .810 &VisualStudio.&Solutions.'.leader.'\ '.dte_sln.' :call <Sid>DTESolutionMenuChoice('.menu_num.')<cr>'
      endfor
      if len(s:visual_studio_lst_dte) > 0
!         amenu <silent> &VisualStudio.&Solutions.-separator- :
      endif
!     amenu <silent> &VisualStudio.&Solutions.&Refresh :call DTEGetSolutions(1)<cr>
  endfunction
  
  function! <Sid>DTESolutionTextMenu()
--- 320,331 ----
          else
              let leader = '\ \ \ &'.menu_num
          endif
!         exe 'amenu <silent> .810 &VisualStudio.ソリューション(&S).'.leader.'\ '.dte_sln.' :call <Sid>DTESolutionMenuChoice('.menu_num.')<cr>'
      endfor
      if len(s:visual_studio_lst_dte) > 0
!         amenu <silent> &VisualStudio.ソリューション(&S).-separator- :
      endif
!     amenu <silent> &VisualStudio.ソリューション(&S).更新(&R) :call DTEGetSolutions(1)<cr>
  endfunction
  
  function! <Sid>DTESolutionTextMenu()
***************
*** 393,399 ****
      endif
      if gui_menu
          echo 'Found '.len(s:visual_studio_lst_project).' project(s)'
!         popup! VisualStudio.Projects
      else
          call <Sid>DTEProjectTextMenu()
      endif
--- 393,399 ----
      endif
      if gui_menu
          echo 'Found '.len(s:visual_studio_lst_project).' project(s)'
!         popup! VisualStudio.プロジェクト(&J)
      else
          call <Sid>DTEProjectTextMenu()
      endif
***************
*** 401,407 ****
  
  function! <Sid>DTEProjectGuiMenuCreate()
      try
!         aunmenu VisualStudio.Projects
      catch
      endtry
      let menu_num = 0
--- 401,407 ----
  
  function! <Sid>DTEProjectGuiMenuCreate()
      try
!         aunmenu VisualStudio.プロジェクト(&J)
      catch
      endtry
      let menu_num = 0
***************
*** 414,422 ****
          else
              let leader = '\ \ \ &'.menu_num
          endif
!         let project_name_menu = '&VisualStudio.Pro&jects.'.leader.'\ '.escape(project_name, '\. ')
!         exe 'amenu <silent> .820 '.project_name_menu.'.&Build\ Project :call <Sid>DTEProjectMenuBuildChoice('.menu_num.')<cr>'
!         exe 'amenu <silent> .820 '.project_name_menu.'.Set\ Start&up\ Project :call <Sid>DTEProjectMenuStartupChoice('.menu_num.')<cr>'
          exe 'amenu <silent> .820 '.project_name_menu.'.-separator- :<cr>'
          if type(project_children) == type([])
              for child in project_children
--- 414,422 ----
          else
              let leader = '\ \ \ &'.menu_num
          endif
!         let project_name_menu = '&VisualStudio.プロジェクト(&J).'.leader.'\ '.escape(project_name, '\. ')
!         exe 'amenu <silent> .820 '.project_name_menu.'.プロジェクトをビルド(&B) :call <Sid>DTEProjectMenuBuildChoice('.menu_num.')<cr>'
!         exe 'amenu <silent> .820 '.project_name_menu.'.スタートアッププロジェクトに設定(&U) :call <Sid>DTEProjectMenuStartupChoice('.menu_num.')<cr>'
          exe 'amenu <silent> .820 '.project_name_menu.'.-separator- :<cr>'
          if type(project_children) == type([])
              for child in project_children
***************
*** 425,433 ****
          endif
      endfor
      if len(s:visual_studio_lst_project) > 0
!         amenu <silent> .820 &VisualStudio.Pro&jects.-separator- :
      endif
!     amenu <silent> .820 &VisualStudio.Pro&jects.&Refresh :call DTEGetProjects(1, 1)<cr>
  endfunction
  
  function! <Sid>DTEProjectGuiSubMenuCreate(menu, name, value)
--- 425,433 ----
          endif
      endfor
      if len(s:visual_studio_lst_project) > 0
!         amenu <silent> .820 &VisualStudio.プロジェクト(&J).-separator- :
      endif
!     amenu <silent> .820 &VisualStudio.プロジェクト(&J).更新(&R) :call DTEGetProjects(1, 1)<cr>
  endfunction
  
  function! <Sid>DTEProjectGuiSubMenuCreate(menu, name, value)
***************
*** 510,526 ****
  " Menu setup
  
  if has('gui') && ( ! exists('g:visual_studio_menu') || g:visual_studio_menu != 0 )
!     amenu <silent> &VisualStudio.&Get\ File :call DTEGetFile()<cr>
!     amenu <silent> &VisualStudio.&Put\ File :call DTEPutFile()<cr>
      amenu <silent> &VisualStudio.-separator1- :
!     amenu <silent> &VisualStudio.&Task\ List :call DTETaskList()<cr>
!     amenu <silent> &VisualStudio.&Output :call DTEOutput()<cr>
!     amenu <silent> &VisualStudio.&Find\ Results\ 1 :call DTEFindResults(1)<cr>
!     amenu <silent> &VisualStudio.Find\ Results\ &2 :call DTEFindResults(2)<cr>
      amenu <silent> &VisualStudio.-separator2- :
!     amenu <silent> &VisualStudio.&Build\ Solution :call DTEBuildSolution()<cr>
!     amenu <silent> &VisualStudio.Build\ Start&up\ Project :call DTEBuildStartupProject()<cr>
!     amenu <silent> &VisualStudio.&Compile\ File :call DTECompileFile()<cr>
      amenu <silent> &VisualStudio.-separator3- :
      call <Sid>DTESolutionGuiMenuCreate()
      call <Sid>DTEProjectGuiMenuCreate()
--- 510,526 ----
  " Menu setup
  
  if has('gui') && ( ! exists('g:visual_studio_menu') || g:visual_studio_menu != 0 )
!     amenu <silent> &VisualStudio.ファイルを取得(&G) :call DTEGetFile()<cr>
!     amenu <silent> &VisualStudio.ファイルを反映(&P) :call DTEPutFile()<cr>
      amenu <silent> &VisualStudio.-separator1- :
!     amenu <silent> &VisualStudio.タスクリスト(&T) :call DTETaskList()<cr>
!     amenu <silent> &VisualStudio.出力(&O) :call DTEOutput()<cr>
!     amenu <silent> &VisualStudio.検索結果\ 1(&F) :call DTEFindResults(1)<cr>
!     amenu <silent> &VisualStudio.検索結果\ 2(&2) :call DTEFindResults(2)<cr>
      amenu <silent> &VisualStudio.-separator2- :
!     amenu <silent> &VisualStudio.ソリューションをビルド(&B) :call DTEBuildSolution()<cr>
!     amenu <silent> &VisualStudio.スタートアッププロジェクトをビルド(&U) :call DTEBuildStartupProject()<cr>
!     amenu <silent> &VisualStudio.ファイルをコンパイル(&C) :call DTECompileFile()<cr>
      amenu <silent> &VisualStudio.-separator3- :
      call <Sid>DTESolutionGuiMenuCreate()
      call <Sid>DTEProjectGuiMenuCreate()
Visual StudioつまりWindows限定なのでファイルの文字コードはms932としました。
動作確認はVisual Studio 2005でしか行っていません。
visual_studio_ja-20080819.zip
よろしければどうぞ。

2008/08/06


以前試した時は出来なかったんですが、今日試したら動く様になっていました。
growl-for-windows - Google Code

A port of the Mac app Growl for use on Windows machines

http://code.google.com/p/growl-for-windows/
バージョンが1.1から1.2に上がったからかな?
Growl for Windows 1.2
以下のコードで動きました。
まずperl
use strict;
use warnings;
use Encode;
use Net::Growl;
register(host => 'localhost',
         application=>"My Perl App",
         password=>'Realy Secure', );
notify(
       application=>"My Perl App",
       title=>'warning',
       description=>decode_utf8('あめんぼ赤いなアイウエオ'),
       priority=>2,
       sticky=>0,
       password=>'Realy Secure',
);
そしてpython
#!/usr/bin/python
#-*- coding:utf-8 -*-
import Growl
g = Growl.GrowlNotifier(
    applicationName='My Python App',
    notifications=["PyGrowl"],
    defaultNotifications=[0],
    hostname="localhost",
    password="Realy Secure")
g.register()
g.notify(
    icon=open('unk.gif').read(),
    noteType="PyGrowl",
    title='wanings',
    description=u"あめんぼ赤いなアイウエオ",
    sticky=False)
あれ?1.1の時にもdecode_utf8とかu""とか試したんですけどねぇ...。やっぱり1.2になったから?

それと今日、whineというWindowsで動くGrowlアプリケーションを、某カレー通の方に教えて頂きました。ありがとうございました。
Whine
Growl for Windowsではフォント等、ディスプレイのカスタマイズが出来ないのですがwhineでは出来るので、こちらを使おうかと思います。
猫派の私ですが、これはお勧めです。


家では貧弱なマシンを使っているので、極力重い処理は避けたいと色んな設定を入れています。その一つにperlのファイルを開いた際に"ftplugin/perl.vim"がINCを調べるのに実行する
perl -e 'print join(q/,/,@INC)'
というコマンドの抑制。ただ単にperlのモジュールパスを調べるいるだけなんですが、ファイルを開くだけでperlを実行されるのも...という方には以下の設定をvimrcに追加すると実行されなくなります。
let g:perlpath="XXX"
このXXXには上記コマンドの実行結果を設定します。
Windows系ならば
perl -e "print join(q/,/,@INC)"
で得られる文字列となります。ActivePerlだとおそらく
let g:perlpath = "c:/perl/site/lib,C:/perl/lib"
と設定すれば良いかと思います。

ま、最近の人はリッチなマシン使ってる人多いから、メリット無いかな。
ちなみにUNIX系でperlのバージョン上げたりするとモジュールパスが変りますので再度この設定をし直す必要があります。ご注意を。

2008/08/05


途中まで書いて助けを求めたんですが、結局自分で書きました。
CodeRepos::Share - Trac

mattn誰か後は頼んだ... ゴブッ(吐血)

http://coderepos.org/share/changeset/17104
このリンク先のソースは不完全なソースです。

使い方は
:mixiecho
でエコーの一覧表示
:mixiecho ○○遺跡に落書きした!
でポストです。

これでtwitterの様にmixiを使って、マイミク外される事うけあいですね。
mixiecho.js
よかったらどうぞ。
i love vimperator!
※ teramakoさんからi love vimperatorの画像頂きました。


perl版もあったんですね。
Perl App Engine状況報告、Protocol BufferのPerl対応 | エンタープライズ | マイコミジャーナル

Perl App Engineに関連したコードベースにはおもに次の3つがある。

  • Perl App Engine
  • Perl XS module Sys::Protect
  • Protocol Buffers for Perl
http://journal.mycom.co.jp/news/2008/07/29/042/index.html

protobuf-perl - Google Code

Protocol Buffers for Perl.

http://code.google.com/p/protobuf-perl/
さっそく遊んでみました。
まず、protobuf-perlはオリジナルの改造として作られており、スケルトンクラス生成ツール「protoc」をビルドする所からの導入となります。
通常はprotobufディレクトリで
# make -j2 protoc
とすれば出来上がります。ただVisual Studioの場合はlibprotocプロジェクトのソース一覧からperlのgeneratorソースである「perl_generator.cc」が外れてしまっているので、追加してからビルドする必要があります。
出来上がれば、「libprotobuf.dll」と「libprotoc.dll」、「protoc.exe」をパスの通った位置にコピーします。
"main.cc"のソースを見て頂ければ分かりますが、protocol buffersのgeneratorはソースが分離されており、main実行時にプラグインを読み込む様な形で登録されています。
int main(int argc, char* argv[]) {

  google::protobuf::compiler::CommandLineInterface cli;

  // Proto2 C++
  google::protobuf::compiler::cpp::CppGenerator cpp_generator;
  cli.RegisterGenerator("--cpp_out", &cpp_generator,
                        "Generate C++ header and source.");

  // Proto2 Java
  google::protobuf::compiler::java::JavaGenerator java_generator;
  cli.RegisterGenerator("--java_out", &java_generator,
                        "Generate Java source file.");


  // Proto2 Python
  google::protobuf::compiler::python::Generator py_generator;
  cli.RegisterGenerator("--python_out", &py_generator,
                        "Generate Python source file.");


  // Proto2 Perl
  google::protobuf::compiler::perl::Generator perl_generator;
  cli.RegisterGenerator("--perl_out", &perl_generator,
                        "Generate Perl source file.");

  return cli.Run(argc, argv);
}
別の言語でgeneratorを作る場合、"XXX_generator.h"と"XXX_generator.cc"を作り、"main.cc"でincludeとRegisterGeneratorを実行すれば動き出す仕組みになっています。キレイな作りですね。

さて出来上がった"protoc"でperlのラッパクラスを作ります。
先日ご紹介した際に使った"person.proto"を使います。ここで注意しなければならないのが、"--perl_out"オプションを使用する際の引数で指定するファイル名がパッケージ名として使われてしまう点です。
perlパッケージ名称がそれっぽくなる様に以下の様に実行します。
# protoc --perl_out=. Person.proto
UNIX系の方は"person.proto"から"Person.proto"にリネームしてから実行して下さい。Windowsならばそのまま実行出来ます。
出来上がった"ProtoBuf::Person"は以下の様になりました。
## Boilerplate:
# Auto-generated code from the protocol buffer compiler.  DO NOT EDIT!

use strict;
use warnings;
#use 5.6.1;
use Protobuf;
package ProtoBuf::Person;


use constant TRUE => 1;
use constant FALSE => 0;
## Top-level enums:

## Top-level extensions:

## All nested enums:
## Message descriptors:

our $_PERSON = Protobuf::Descriptor->new(
  name => 'Person',
  full_name => 'protocol.Person',
  containing_type => undef,
  fields => [
    Protobuf::FieldDescriptor->new(
      name => 'name', index => 0, number => 1,
      type => 9, cpp_type => 9, label => 2,
      default_value => "",
      message_type => undef, enum_type => undef, containing_type => undef,
      is_extension => FALSE, extension_scope => undef),
    Protobuf::FieldDescriptor->new(
      name => 'age', index => 1, number => 2,
      type => 5, cpp_type => 1, label => 2,
      default_value => 0,
      message_type => undef, enum_type => undef, containing_type => undef,
      is_extension => FALSE, extension_scope => undef),
  ],
  extensions => [
  ],
  nested_types => [],  # TODO(bradfitz): Implement.
  enum_types => [
  ],
  options => Protobuf::MessageOptions->new(
  ),
);

## Imports:

## Fix foreign fields:

## Messages:
Protobuf::Message->GenerateClass(__PACKAGE__ . '::Person', $_PERSON);

## Fix foreign fields in extensions:
## Services:
実装はMooseを使っており、アクセサに対する制御も行っています。
次にサンプルコード
use strict;
use warnings;
no warnings qw(deprecated);
use Protobuf::Decoder;
use ProtoBuf::Person;

my $person = ProtoBuf::Person::Person->new;
$person->set_name('mattn');
$person->set_age(18);

open my $out, '>', 'person_perl.txt';
print $out $person->serialize_to_string();
close $out;

my $other = ProtoBuf::Person::Person->new;
open my $in, '<', 'person_perl.txt';
$other->parse_from_string( do { local $/;<$in> } );
close $in;

printf "name:%s\nage:%d\n", $other->name(), $other->age();
前回と同様にファイルに書き出し、ファイルから読み込んでparse_from_stringで実体化します。結果は前回と同様です。

cppは別として、pythonの場合はpickleが、javaの場合はSerializableがあり、Google Protocol Buffersの使い所が難しい気もしましたが、色々な言語がサポートされてくると言語をまたいだデータ共有という形で使えそうな気がして来ました。特にmemcachedを使ったダイナミックインスタンスエクスポートとか面白いかもしれませんね。
またperl版がMooseと絡んでいる所がとても通ぽくて良いですね。:-)
どうせなら、PerlIOでシリアライズ出来たらな...と思ってしまいましたが、もしかしたら今後サポートされるかもしれませんね。

他にもprotocol buffersはいろんな言語でインプリメンツされているので、皆さんもお好きな言語で遊んでみてはいかがでしょうか。

2008/08/04


vimではコマンドモード時に、<c-x>で現在のディレクトリを補完するようにしているので、これがvimperatorでもやれると便利と思った。
twitter.jsでは「%URL%」とか「%TITLE%」で現在のURLが置き換えられる様になってるけど、他のコマンドでも使いたい場合もある。
例えば、そろそろ誰かが「mixiecho」ってコマンド作ったとして
:mixiecho ちょっwwwコレ http://example.jp/
とかやりたい時には、またtwitter.jsと同じ実装しなきゃいけない。
ま、こんなので困るのは私だけかも。
(function() {
    liberator.mappings.addUserMap([liberator.modes.COMMAND_LINE], ['<c-x>'],
        "insert current URL to command line",
        function () {
            var cmdline = liberator.CommandLine();
            var curcmd = ':' + cmdline.getCommand();
            if (!curcmd.match(/ $/)) curcmd += ' ';
            cmdline.open(curcmd, liberator.buffer.URL);
        }
    );
})();
コマンドラインのコントロール名は決まっているので、そこにkeyEventをdispatchEventしても良かったけど、まっいいや。

2008/07/31


twissr.js
※右クリックして名前を付けて保存です。
※ソース閲覧禁止です。

2008/07/30


kuさんが面白い物見つけてくれました。
mixi for iPhoneから発掘されたmixi日記投稿用API « ku

iPhoneからぜんぜん日記を書く手段がなかったらmixiから、mixi for iPhoneという日記を書いたりするiPhoneアプリが公開されました!

新しいアプリに新しいAPI、日記が投稿できるアプリなら日記投稿用のAPIというわけでmixiのあしあとAPI発掘と同じように掘り起こして見つけました。

ほかのAPIと同様、認証はWSSEでatomPubで日記を書くことができるようになっていました。エンドポイントはhttp://mixi.jp/atom/diary/member_id=mixiIDです。

http://ido.nu/kuma/2008/07/30/digging-mixi-for-iphone-application-and-new-api-for-posting-a-diary-with-a-photo/
って事でさっそくPlagger用にPublish::MixiDiary書いてみました。出来立てほやほやなので、あまりテストしていません。codereposに上げているので、良かったらテストしてみて下さい。

これでtwitterの様にmixiを使って、マイミク外される事うけあいですね。

Publish::MixiDiary
ちなみに中ではたけまるさんのAtompubを使っています。

2008/07/29


まずは素晴らしい出だしだと思う。
google-app-engine-oil - Google Code
Yet another web framework on Google App Engine.

Google App Engine Oil (GAEO) is an open-source web framework running on Google App Engine. It enables the web development on App Engine quick and less configurations.

http://code.google.com/p/google-app-engine-oil/
railsを狙ってるのかなぁ。
これから勉強を始めますが、まずは1分でアプリが作れてしまうという素晴らしい所をご紹介します。

Google App Engine Oil(GAEO)は、上記URLからダウンロード(もしくはsvn trunk取得)して下さい。
"gaeo/bin"にパスが通っている状態から説明します。

アプリケーションテンプレートの作成

gaeoコマンドで生成します。
# gaeo.py hello
The hello project has been created.
これだけでも動きます。試しに
# cd hello
# dev_appserver.py .
...
としてブラウザで"http://localhost:8080/"を開くと
gaeo1
いう画面が起動します。これだけでも実はモデルやディスパッチャやコントローラが生成され、テンプレートが用意されているのでいきなりコーディングが始められます。

コントローラの作成

gaeogenコマンドで生成します。
# gaeogen.py controlller say
Creating application/templates/say/ ...
Creating say.py
これでsayコントローラが生成されます。中身は
from gaeo.controller import BaseController

class SayController(BaseController):
    pass
こんな感じ。あとはこれを少し修正して
#!-*- coding:utf-8 -*-
from gaeo.controller import BaseController

class SayController(BaseController):
  def hello(self):
    self.render(text = 'こんにちわ世界')
とし、"http://localhost:8080/say/hello"にアクセスすると"こんにちわ世界"と表示されます。また"templates/say/"に"hello.html"を以下の様に作成し
こんにちわ {{ word }}
先ほどのsay.pyを以下の様に書き換えます。
#!-*- coding:utf-8 -*-
from gaeo.controller import BaseController

class SayController(BaseController):
  def hello(self):
    self.render(
        template = 'hello',
        values = { 'word' : '世界' }
    )
これでテンプレートを使ったページが出来上がります。

目新しい点

私はここに注目しました。
これまでjsonやxmlをGAEから出力する際には、Content-Typeの設定が必要であったりしましたがGAEOならば上記textやtemplateと同じ様に

#!-*- coding:utf-8 -*-
from gaeo.controller import BaseController

class SayController(BaseController):
  def hello(self):
    self.render(
        xml = '<xml />',
    )
としたり
#!-*- coding:utf-8 -*-
from gaeo.controller import BaseController

class SayController(BaseController):
  def hello(self):
    self.render(
        json = '{ word : "Hello, World!" }',
    )
とするだけで、正しいContent-Typeを返してくれるようになります。

期待したい所

リクエストパラメータself.paramsでパラメータをdictとして受け取れたり、self.sessionで簡単にセッションが扱えたりと開発者にとって扱いやすいフレームワークになっていると思われます。
ただ現状gaeogenコマンドではcontrollerしか生成出来ていません。おそらく今後modelの生成も出来る様になるかと思いますので今後に期待したいです。出来ればrails相当になってくれれば良いですね。

まだちゃんと勉強してないのでこれから色々調べていきます。

2008/07/28


不具合報告を頂きまた。自分でも動かなくなってたのに気付いてませんでした。
Big Sky :: XSLとjQuery/HTMLだけで作る、amazon最速検索
http://mattn.kaoriya.net/software/lang/javascript/20080605165643.htm
どうやら、jQueryがcacheを無効にする時に使用する特殊パラメータ"_"がAmazonで蹴られる様になったのが原因です。
結構ハマった(30分)割には、直し方は簡単。
$.ajaxSettings.cache = true;
まぁ、Amazon検索にはリアルタイムなキャッシュ無効は必要なさそうなので、これで良しとします。
一応、直ったのでこの記事にも実行例置いときます。コードは↑のリンク先を参照下さい。

続きを読む...


2008/07/25


これはいいね。
Google App Engine Blog: Some small updates!
We're happy to announce we've released some small updates to Google App Engine. Among the more significant changes:
  • More apps: Want to create more than 3 applications with your App Engine account? Now you can now create up to 10!
  • Time windows for Dashboard graphs: Zoom in on the data in your dashboard to get a more accurate picture of whats going on. You can zoom in to see graphs for the last 24, 12, and 6 hour periods.
  • Logs export: You can now use appcfg.py to download your application's logs in plaintext format. Use appcfg.py --help for more information on how to download your logs.
  • Send email as logged in user: If you're using the users API, you can now send email from the email address of the currently-logged-in user.
最新版のダウンロードはこちらから。
作れるアプリ数が10個に増え
あと8個
ダッシュボードのデータ時系列が拡大できる様になって
6時間単位で拡大可能
appcfg.pyでログがエクスポート出来る様になって
# appcfg.py request_logs <directory> <output_file>
現在ログイン中のユーザのメールアドレスが得られる様になったらしい。

やるね。Google App Engine。

2008/07/24


とは言ってもUser Agent Switcherが必要なのですが...

使い方は
:ua MyUserAgent
でUser Agent Switcherに"MyUserAgent"という名前で登録されたUser Agentに変更します。
:ua Default
でリセット
:ua!
で設定ダイアログ表示となります。
急遽モバイルページに行きたいけどメニューに手を伸ばすの面倒臭いって方には使えるかも。
ちなみに私はデフォルトで登録されていた物の他に、DoCoMo、Vodafone、SoftBank、Hatenaクローラ等のUserAgentを登録してます。
uaSwitch.js
よろしければどーぞ。

2008/07/23


遅ればせながらGoogle Protocol Buffersで遊んでみました。
protobuf - Google Code
Protocol Buffers are a way of encoding structured data in an efficient yet extensible format. Google uses Protocol Buffers for almost all of its internal RPC protocols and file formats.
まず、インストールは以下の様に行いました。なおprotobufを展開したフォルダは以下のフォルダ。
C:¥temp¥protobuf-2.0.0beta¥

cpp版のインストール

展開したフォルダのvsprojectsフォルダにある"protobuf.sln"をVisual Studioで開きReleaseビルド。
必要であれば環境変数PATHの通る場所へ"protoc.exe"、"libprotobuf.dll"、"libprotoc.dll"を置く。また"libprotobuf.lib"と"libprotoc.lib"はコンパイル時に必要になるので分かりやすい位置に置いておく。

java版のインストール

まずApache MavenをインストールしPATHを通す。
次にjavaフォルダへ移動し
C:¥temp¥protobuf-2.0.0beta¥java> mvn test install
とすれば必要なライブラリがダウンロードされ、テスト後インストールされる。
targetフォルダに"protobuf-java-2.0.0beta.jar"が出来るのでこれまた分かりやすい位置に置く。

python版のインストール

pythonフォルダに移動し、cpp版をビルドした際に出来上がった"protoc.exe"にパスが通っている事を確認して
C:¥temp¥protobuf-2.0.0beta¥python> python setup.py bdist_wininst
...
C:¥temp¥protobuf-2.0.0beta¥python> cd dist
C:¥temp¥protobuf-2.0.0beta¥python¥dist> protobuf-2.0.0beta.win32.exe
※Windowsじゃない人は"bdist_wininst"の代わりに"bdist_rpm"とか"install"とか...

テストコードによる検証

まずデータ定義となるprotoファイルを作る。詳しい説明は省略するが知りたい人はココとかココとか。今回は名前と年齢を格納出来るPersonクラスを扱う。
person.proto
package protocol;

message Person {
  required string name = 1;
  required int32  age  = 2;
}

このファイルを指定して、スタブクラスを生成する。
まずcpp版。
C:¥temp¥protobuf-2.0.0beta¥tmp> protoc --cpp_out=. person.proto
これで"person.pb.cc"と"person.pb.h"が生成される。以下テストコード。
test.cpp
#include <iostream>
#include <fstream>
#include <person.pb.h>

void save(const char* fn) {
    protocol::Person person;

    person.set_name("mattn");
    person.set_age(18);

    std::ofstream ofs(fn);
    person.SerializeToOstream(&ofs);
    ofs.close();
}

void load(const char* fn) {
    protocol::Person person;

    std::ifstream ifs(fn);
    person.ParseFromIstream(&ifs);

    std::cout <<
        "name:" << person.name() <<
        ",age:" << person.age() << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 2) return -1;

    save(argv[1]);
    load(argv[1]);

    return 0;
}
ビルドは以下の手順で行う。
C:¥temp¥protobuf-2.0.0beta¥tmp> cl -I../src -I. /EHsc test.cpp person.pb.cc libprotoc.lib libprotobuf.lib
実行すると
C:¥temp¥protobuf-2.0.0beta¥tmp> test person_cpp.txt
name:mattn,age:18
と表示される。そう...18歳です。ウソです。
生成される"person_cpp.txt"は以下の様なファイルとなる。

^Emattn^P#
次にjava版。"person.proto"というファイル名からPersonクラスを生成する際にファイル名がバッティングしてしまうので"persons.proto"という別名にコピーしておく。
C:¥temp¥protobuf-2.0.0beta¥tmp> protoc --java_out=. persons.proto
"protocol/Persons.java"が生成される。
テストコードは以下の通り。
test.java
import protocol.Persons;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class test {
    public static void main(String[] args) throws Exception {
        Persons.Person.Builder personBuilder = Persons.Person.newBuilder();
        Persons.Person mattn = personBuilder.setName("mattn").setAge(18).build();

        FileOutputStream fos = new FileOutputStream(args[0]);
        mattn.writeTo(fos);
        fos.close();

        FileInputStream fin = new FileInputStream(args[0]);
        mattn = Persons.Person.parseFrom(fin);
        fin.close();

        System.out.printf("name:%s,age:%d", mattn.getName(), mattn.getAge());
    }
}
ビルドは以下の手順で行う。
C:¥temp¥protobuf-2.0.0beta¥tmp> javac -classpath protobuf-java-2.0.0beta.jar; test.java
実行結果はcpp版と同様。

最後にpython版。
C:¥temp¥protobuf-2.0.0beta¥tmp> protoc --python_out=. person.proto
"person_pb2.py"が生成される。以下テストコード
test.py
import sys
from person_pb2 import Person

fn = sys.argv[1]

person = Person()
person.name = "mattn"
person.age = 18
open(fn, "w").write(person.SerializeToString())

person = Person()
person.ParseFromString(open(fn).read())
print "name:%s,age:%d" % (person.name, person.age)
実行結果はcpp版、java版と同様。

所感

結局の所、"protocol buffers"とは、"Serialize Format"生成ツールおよびライブラリと、"Serializable"なクラス郡と言った所だろうか。
実際にはRPC等で転送してみないと良さは分からないかもしれないけど、XMLの様に自信がValidation可能な物ではなさそうなので転送の際には外側からデータのサイズ送信やチェックサム実施も必要になるかもしれない。
そういった意味では、「【ハウツー】XMLはもう不要!? Google製シリアライズツール「Protocol Buffer」 (2) ダウンロードとインストール | エンタープライズ | マイコミジャーナル」の"XMLはもう不要!?"には少し疑問を感じる。

時間があれば、ネットワーク上にシリアライズしたデータを転送してみたいと思う。


少しずつですがバージョンアップしています。
The Unofficial Blosxom User Group :: Blosxom 2.1.0 released

New features

  • Support for external config files and multi-instance installations via environment variables. (Inspired and loosely based on the Debian patches for Blosxom 2.0.2.)
  • Support for multiple plugin directories using $plugin_path and @INC.
  • Support for configuring the plugins to use in a config file.
  • The default feed templates are now in RSS 2.0 format and prevent duplicated stories in feed readers or aggregators if posts are changed after publishing using a GUID element in each feed item.
  • There's now an encoding setting which controls what charset your blog and feeds should be marked as.
http://blosxom.ookee.com/blog/news/blosxom-2.1.0-released.html
バージョンアップの内容としては以下の通り。
  • 環境変数によるconfigファイルの読み込み
  • 複数のプラグインディレクトリをサポート
  • configファイルによるpluginの設定が可能に
  • デフォルトのRSSフィードがRSS2.0に
  • encodingの設定が可能に
また幾らかのバグフィックスも含んでいます。さっそくこのサイトでも導入しました。大きな修正点はありませんでしたがRSS1.0からRSS2.0に修正した事でフィードリーダに更新が配信されてしまったかと思います。
configファイルはmod_envを使って
SetEnv BLOSXOM_CONFIG_FILE /path/to/my/blosxom/config/file/bloxsom.conf
とすることでblosxom.cgiを全く弄らずに動かす事が出来る様になりました。

しばらくは様子見です。


最近TheSchwartが賑わってる様ですね。
TheSchwartzで仕事をあとにまわす - bits and bytes

なければ勝手に作ってくれるのかなと思ってSYNOPSISのコードを実行してみたけどやっぱり自動でできたりはしなそうだったので調べたらCatalyst and TheSchwartz: Reliable JobQueue in a great framework - Voxschema.sqlを使うといいと書いてありました。このスキーマをmysqlで実行すればTheSchwartzのキューを管理するためのテーブルが出来上がります。

http://labs.gmo.jp/blog/ku/2008/06/theschwartz.html

Twitter / ku: TheSchwartzおもしろかった sqliteで動...

TheSchwartzおもしろかった sqliteで動くようにしたいけどsqliteようのschemeかきかたわかんない

http://twitter.com/ku/statuses/843135311

って事でSQLiteでも動く様に出来ないか調べてみました。
schema.sqlは、MySQL用のスキーマで
CREATE TABLE funcmap (
        funcid         INT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
        funcname       VARCHAR(255) NOT NULL,
        UNIQUE(funcname)
);

CREATE TABLE job (
        jobid           BIGINT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
        funcid          INT UNSIGNED NOT NULL,
        arg             MEDIUMBLOB,
        uniqkey         VARCHAR(255) NULL,
        insert_time     INTEGER UNSIGNED,
        run_after       INTEGER UNSIGNED NOT NULL,
        grabbed_until   INTEGER UNSIGNED NOT NULL,
        priority        SMALLINT UNSIGNED,
        coalesce        VARCHAR(255),
        INDEX (funcid, run_after),
        UNIQUE(funcid, uniqkey),
        INDEX (funcid, coalesce)
);

CREATE TABLE note (
        jobid           BIGINT UNSIGNED NOT NULL,
        notekey         VARCHAR(255),
        PRIMARY KEY (jobid, notekey),
        value           MEDIUMBLOB
);

CREATE TABLE error (
        error_time      INTEGER UNSIGNED NOT NULL,
        jobid           BIGINT UNSIGNED NOT NULL,
        message         VARCHAR(255) NOT NULL,
        funcid          INT UNSIGNED NOT NULL DEFAULT 0,
        INDEX (funcid, error_time),
        INDEX (error_time),
        INDEX (jobid)
);

CREATE TABLE exitstatus (
        jobid           BIGINT UNSIGNED PRIMARY KEY NOT NULL,
        funcid          INT UNSIGNED NOT NULL DEFAULT 0,
        status          SMALLINT UNSIGNED,
        completion_time INTEGER UNSIGNED,
        delete_after    INTEGER UNSIGNED,
        INDEX (funcid),
        INDEX (delete_after)
);
というSQLなので、これをSQLite用に書き換えます。
cl.pocari.org - SQLite で auto-increment なフィールドを作成する方法

つまり,SQLite で auto-increment なフィールドを作りたければ,INTEGER PRIMARY KEY を指定してあげればいいらしい.

http://cl.pocari.org/2006-02-12-1.html
こちらの記事でも書かれている通り、SQLiteではINTEGER PRIMARY KEYを指定すればok。
※ちなみにUNSIGNEDとかNOT NULLとか付けると動かないです。

で書き換えたSQLは以下の通り
CREATE TABLE funcmap (
        funcid         INTEGER PRIMARY KEY,
        funcname       VARCHAR(255) NOT NULL,
        UNIQUE(funcname)
);

CREATE TABLE job (
        jobid           INTEGER PRIMARY KEY,
        funcid          INTEGER NOT NULL,
        arg             MEDIUMBLOB,
        uniqkey         VARCHAR(255) NULL,
        insert_time     INTEGER UNSIGNED,
        run_after       INTEGER UNSIGNED NOT NULL,
        grabbed_until   INTEGER UNSIGNED NOT NULL,
        priority        SMALLINT UNSIGNED,
        coalesce        VARCHAR(255),
        UNIQUE(funcid, uniqkey)
);
CREATE INDEX job_idx_1 ON job (funcid, run_after);
CREATE INDEX job_idx_2 ON job (funcid, coalesce);

CREATE TABLE note (
        jobid           INTEGER NOT NULL,
        notekey         VARCHAR(255),
        value           BLOB
);

CREATE TABLE error (
        error_time      INTEGER UNSIGNED NOT NULL,
        jobid           INTEGER NOT NULL,
        message         VARCHAR(255) NOT NULL,
        funcid          INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX error_idx_1 ON error (funcid, error_time);
CREATE INDEX error_idx_2 ON error (error_time);
CREATE INDEX error_idx_3 ON error (jobid);

CREATE TABLE exitstatus (
        jobid           INTEGER PRIMARY KEY,
        funcid          INTEGER NOT NULL DEFAULT 0,
        status          SMALLINT UNSIGNED,
        completion_time INTEGER UNSIGNED,
        delete_after    INTEGER UNSIGNED
);
CREATE INDEX exitstatus_idx_1 ON exitstatus (funcid);
CREATE INDEX exitstatus_idx_2 ON exitstatus (delete_after);
あとはコレを
# sqlite3 the_schwartz < theschwartz_schema.sql
としてDBファイルを作る。
さてサーバのコードはDSNを"dbi:SQLite:the_schwartz"にするだけ。
#!/usr/bin/env perl
package MyWorker;
use strict;
use utf8;
use warnings;
use base qw( TheSchwartz::Worker );
use Data::Dumper;

binmode STDERR, ":encoding(cp932)" if $^O eq "MSWin32";

sub work {
    my ($class, $job) = @_;
    warn "お仕事ですよ! @{[ Dumper($job->arg) ]}\n";
    $job->completed();
}

package main;
use strict;
use warnings;
use TheSchwartz;

my $client = TheSchwartz->new(
    databases => [ +{ dsn => 'dbi:SQLite:the_schwartz' } ]
);
$client->can_do('MyWorker');
$client->work();
さらにクライアントのコード
#!/usr/bin/env perl
use strict;
use warnings;
use TheSchwartz;

my $client = TheSchwartz->new(
    databases => [ +{ dsn => 'dbi:SQLite:the_schwartz' } ]
);
$client->insert('MyWorker' => +{ hoge => "fuga" });
実行すると
お仕事ですよ! $VAR1 = {
          'hoge' => 'fuga'
        };
という風にサーバ側で表示されます。 ちなみに、DBD::SQLiteのバグで
closing dbh with active statement handles at .../Data/ObjectDriver/Driver/DBI.pm line 566.
という表示が出る様ならばココにある"issue-17603.tar.gz"のパッチを当てるといいです。どうやらこのバグ、デグレっぽいですね。

ま、結局SQLiteでやっちゃったら同一マシンだし負荷分散にはならない(UIロックを避けるという意味では有用)んですけどね。

2008/07/11


卑怯と言われようが構いません。

let X = "ひだまり"
let _ = "スケッチ"

try
  X / _ / X < また見て下さいね
catch /^Vim\%((\a\+)\)\=:E488/
  echo eval(
    \  substitute(
    \    substitute(
    \      v:exception,
    \      '^.*E\(\d\+\).*:\s*\(\(.\) / \(.\).*\). <\(.*\)$',
    \      '\2"\3".(\1-123)."\5"', ''), '/', '.', 'g'))
endtry

2008/07/08


http://subtech.g.hatena.ne.jp/cho45/20080708/1215450151
http://subtech.g.hatena.ne.jp/miyagawa/20080708/1215473551
#!/usr/bin/env python
# -*- coding: utf-8 -*-

class X:
  def __init__(self):
    self.s = "ひだまり"

  def __lt__(self, v):
    print "%s %s" % (self.s, v)
    return self
 
  def __div__(self, v):
    if isinstance(v, int):
      self.s += "スケッチ"
    else:
      self.s += "365"
    return self

X = X()
_ = 1

X / _ / X < "来週も見てくださいね!"
ごめんなさい。それが何かしらない...orz

2008/07/07


twitter APIはXMLにリクエスト投げてもBUSY画面でtext/htmlなヘッダを返す時があるので、gtktwitterでは応答ヘッダがapplication/xmlじゃ無いときは処理しない様にしてあったんですが、wassrpod.plはAPI応答にtext/htmlを返していたので少し手を入れさせて頂き、見事gtktwitterでwassrにポストする事が出来ました。あとプロフアイコンもPROXY経由で取れる様に修正しました。おかしかったら直して下さい(id:yappo)
wassrpod経由でgtktwitter
動かすには、環境変数「HTTP_PROXY」に「localhost:9277」を設定してgtktwitterを起動すればok。ただtwitterの設定が残ってるだろうから、UN*Xならば「~/.config/gtktwitter/config」を、Windowsならば「~/Application Data/gtktwitter/config」を修正して起動して下さい。同じアカウント名使ってるならばそのままでも動くかも。

ま、gtkwassrもあるんですけどね...

関連URL:YappoLogs: WassrPodというMacからwassrを快適に使うツールを作ったよ

2008/06/27


TheSchwartzも良いのですが、MySQLが必要だったりポーリングのタイムラグがあったりと、要件に合わない場合というのも出て来たりします。こんな時はどうするか、解決策の一つにメッセージバスがあります。
メッセージバスとして一般的に有名なのが、DBusです。Linux等では数多くのアプリケーションが採用しており、GNOMEもBonoboからの移行を表明、KDE4でも採用される様です。ちなみに私が愛用している軽量デスクトップROXは当初に採用しており、ちょっとしたpythonのコードでリッチなアプリケーション間通信が実現出来ています。他にも既に色々なプロジェクトでDBusが使われています。
さて今日はNet::DBusを使い、TheSchwartzと似たような事が出来ないかをWindows上でやってみようかと思います。
まず、DBusのWindows実装であるwinDBusをインストールします。winDBusはこちらで配布されておりバイナリもダウンロード出来ます。
ここでまず、DBusの動作を確認します。winDBusを展開したディレクトリの中のbinフォルダに移動し以下の様にしてデーモンを起動します。
C:¥winDBus¥> dbus-daemon.exe --config-file=C:/winDBus/dbus/bin/session.conf
winDBusを"C:¥Programme"に展開された場合には
C:¥Programme¥> dbus-daemon.exe --session
とするだけでも動作します。winDBusの場合、unixドメインソケットをサポートしていませんので、上記session.confに書かれている
<listen>tcp:host=localhost,port=12434</listen>
の値を環境変数「DBUS_SESSION_BUS_ADDRESS」に設定します。
set DBUS_SESSION_BUS_ADDRESS=tcp:host=localhost,port=12434
その上でdbus-daemon.exeと同じ位置にあるtest_names.exeを実行し
Successfully acquired name 'org.freedesktop.DBus.Test'
Successfully acquired name 'org.freedesktop.DBus.Test-2'
Successfully acquired name 'org.freedesktop.DBus.Test_2'
と表示されれば動作確認は終了です。
次にNet::DBusをインストールします。まずNet::DBusはビルドにpkg-configを使うので`pkg-config --cflags dbus-1`が扱えないVCではビルドに難があります。私の場合はMinGWを使用しました。
まず、winDBusに同梱されていなかったdbus-1.pcを作成します。winDBusをインストールしたディレクトリを指定して
prefix=C:/winDBus/dbus
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include

Name: D-Bus
Description: D-Bus Protocol Library
Version0.90
Libs: -LC:/winDBus/dbus/lib -ldbus-1
Cflags: -IC:/winDBus/dbus/include
という内容でdbus-1.pcを保存します。次に環境変数PKG_CONFIG_PATHにこのdbus-1.pcを保存したディレクトリを設定し、cpanからインストールします。おそらくこれでインストール出来るかと思いますが、出来なかった方はVCのコンパイラのパスを外し、MinGWのパスを設定して確認して見て下さい。
これでようやくperlからDBusを使える準備が揃いました。
さて、サーバのソース。DBusのセッションからバスを取得し、サービスをエクスポート、ワーカーの役目をするオブジェクトを作成します。ワーカーオブジェクトにはJobStartというメソッドを定義します。
#!/usr/bin/perl

use strict;
use warnings;
use threads;
use YAML;
use Net::DBus;
use Net::DBus::Service;
use Net::DBus::Reactor;

package MyWorker;
use base qw(Net::DBus::Object);
use Net::DBus::Exporter qw(net.kaoriya.mattn.DBusWorker);

sub new {
    my $class = shift;
    my $service = shift;
    my $self = $class->SUPER::new($service, "/MyWorker");
    bless $self, $class;
    
    return $self;
}

dbus_method("JobStart", [["dict", "string", "string"]], [["dict", "string", "int32"]]);
sub JobStart {
    my $self = shift;
    my $job = shift;
    my $thread = threads->new(sub {
        my $job = shift;
        printf "processing job%03d : %s\n", threads->tid, $job->{request};
        sleep 10;
        printf "done job%03d\n", threads->tid;
    }, $job);
    return { "id" => $thread->tid };
}

package main;

my $bus = Net::DBus->session();
my $service = $bus->export_service("net.kaoriya.mattn.DBusWorker");
my $object = MyWorker->new($service);
Net::DBus::Reactor->main->run();
DBusは非同期通信も提供していますが、リクエストを連続で送った際サーバがビジー状態だとクライアントが側が応答を待ってロックします。
そこで上記ソースではスレッドを起こし、重たい処理をさせる様になっています。スレッド内では"threads->tid"で言わばジョブ番号の役割をするスレッドIDを使ってユニークな処理が出来るかと思います。
さらにクライアントのコード。
#/usr/bin/perl

use strict;
use warnings;

use Net::DBus;
use Net::DBus::Reactor;
use Net::DBus::Callback;
use Net::DBus::Annotation qw(:call);

my $request = shift @ARGV || 'request at '.time();
my $bus = Net::DBus->session();
my $service = $bus->get_service("net.kaoriya.mattn.DBusWorker");
my $object = $service->get_object("/MyWorker");
my $res = $object->JobStart({ request => $request });
printf "job was accepted as job%03d\n", $res->{id};
DBusセッションからもらったMyWorkerオブジェクトのJobStartメソッドにリクエスト文字列を指定して送信します。
まずクライアントの実行結果は
C:¥winDBus¥test¥> perl client.pl
job was accepted as job001
C:¥winDBus¥test¥> perl client.pl
job was accepted as job002
C:¥winDBus¥test¥> perl client.pl
job was accepted as job003
クライアントではjob001を実行した後、10秒待ってjob002、job003を投入しました。そしてサーバの実行結果
processing job001 : request at 1214560175
done job001
processing job002 : request at 1214560192
processing job003 : request at 1214560195
done job002
done job003
おぉ!非同期ですね!
DBusでは、引数の構成および戻り値の構成をdbus_methodにて指定でき、文字列(string)や数値(int32等)、配列(array)、タプル(tuple)、構造体(struct)を引き渡したり、戻り値として返したり出来ます。
さらに、Net::DBusに同梱されている"example-client-async.pl"の様に非同期送信結果を受け取り、完了イベントで結果を受け取るという事も出来ます。
#/usr/bin/perl

use warnings;
use strict;

use Net::DBus;
use Net::DBus::Reactor;
use Net::DBus::Callback;
use Net::DBus::Annotation qw(:call);

my $bus = Net::DBus->session();

my $service = $bus->get_service("org.designfu.SampleService");
my $object = $service->get_object("/SomeObject");

print "Doing async call\n";
my $reply = $object->HelloWorld(dbus_call_async, "Hello from example-client.pl!");

my $r = Net::DBus::Reactor->main;

sub all_done {
    my $reply = shift;
    my $list = $reply->get_result;
    print "[", join(", ", map { "'$_'" } @{$list}), "]\n";

    $r->shutdown;
}

print "Setting notify\n";
$reply->set_notify(\&all_done);

sub tick {
    print "Tick-tock\n";
}


print "Adding timer\n";
$r->add_timeout(500, Net::DBus::Callback->new(method => \&tick));

print "Entering main loop\n";
$r->run;
今回はWebで扱いやすい様に、非同期で結果を待機する処理ではなくサーバ側でスレッドを実行していますが、デスクトップクライアントを作る際にはdbus_call_asyncを使う事になるかと思います。

TheSchwartzも面白いですが、DBusも面白いですね。
perlの他にも、C(GLib)はもちろんpythonやJava等でも実装されています(参照)。
皆さんも試してみては如何でしょうか。


vimで自前でYAML読み込むとか面倒くさすぎる。
こう言う時はif_pythonとかif_perlとかを使わせてもらう。運良くvimスクリプトはjsonと相性が良いのでYAMLを読み込み、JSONに変換してvimに戻してあげる。
function! LoadYAML(file)
  perl << EOF
use YAML::Syck;
use JSON::Syck qw(Dump);
eval {
  VIM::DoCommand("let ret = " . Dump(LoadFile("".VIM::Eval('a:file'))));
};
VIM::DoCommand("let v:errmsg = substitute('$@', \"\\n\", '', 'g')") if $@;
EOF
  if !exists('ret')
    throw v:errmsg
  endif
  return ret
endfunction

echo LoadYAML("config.yaml")
戻り値はDictionary形式になります。
読み込めなかった場合はthrowしているのでvim7限定になりますがcatchしてやって下さい。

さて、if_perlですがperl510で動かなくなってました。今日パッチを作成してvim-devに送ったのですが、如何せん自信がありません(Shibuya.xsでvimmerな皆様、どうか私にお力をお貸し下さい)。
パッチは
vim72-perl510-fix.diff
にあります。不具合報告等あればご連絡下さい。

2008/06/24


ようやくblosxomで動く用になりました。
tomblooハックス - share on WordPressにポストするためのMetaWeblog API « ku

アップデート 2008.6.24

mattnさんにいただいたBig Sky :: tomblooハックス90_MetaWeblog.jsのパスワードをパスワードマネージャに保存するパッチを文字化けしないようにしてtombloo - Google Codeにコミットしました。ページのView raw fileのところからダウンロードしてください。

http://ido.nu/kuma/2008/06/07/werwer/
google codeのtomblooから90_MetaWeblog.jsを取得し、"extensions/tombloo@brasil.to/chrome/content/library/"に入れると動きます。詳しい説明はkuさんのページを確認して下さい。
kuさんのスクリプトから、若干手を入れさせて頂いていてXMLRPCのエントリポイントである"extensions.tombloo.posters.MetaWeblog.endpoint"の他に"extensions.tombloo.posters.MetaWeblog.mediapath"という設定が出来る様になっています。
ブログツールの中には画像をアップロードするパスが設定出来る物もあり、環境に合わせて
mediapathの設定
の様に設定します。これで90_MetaWeblog.jsが使えるブログツールが増えたかと思います。恐らくMTでもendpointを
http://www.example.com/cgi-bin/mt/mt-xmlrpc.cgi
の様に設定すれば動きます。確認してませんが。
今回blosxomでは"SourceForge.net: BXR: Blosxom XML-RPC Interface"を使い、特定のカテゴリ(90_MetaWeblog.jsは「reblog」というカテゴリ)のみ
  • インデックスやRSSから隠す
  • dynamic_cacheしない
  • アクセスランキングに載せない
処理を入れ、reblogサイトっぽく動く様になりました。名づけてreblosxomとします!
blosxom-xmlrpc.cgiを色々なブログツールに対応させる為のパッチを当てれば、日本語周りや動的なカテゴリ変更などにも対応出来るblosxom-xmlrpc.cgiが出来上がります。オリジナルのBXRで上手く行かない人は試してみて下さい。
実際に以下の様にして
reblosxomでポスト
ポストした結果が「reblogカテゴリ」となります。
自分の写真しかreblogしてないので、全くreblogじゃないんですが家族や友人の写真からreblogなんて事ならokですかね...
わずか3クリック程でreblog出来てしまうtombloo & 90_MetaWeblog.js すばらしいです。

今回blosxomプラグイン等に行った修正は後日公開したいと思います。

ku++ brazil++ tombloo++ blosxom++

2008/06/20


これすごいわ。
wildoptions=autoとcomplete=lが凄い件 - Dis Communication - 符号無し

vimperator1.2pre 2008-06-19以降で実装されてるwildoptions=autoとcomplete=lがlifechangingだったので紹介しておきますね。

何が凄いって
:set complete=l
が凄い。「たかがtab押さなくても良くなっただけだよね」と思うでしょうが、使ってみたら無茶苦茶便利。
vimperator使いだと、おそらくURLを開くのに
:open http://...
とか「<c-l>」してアドレスバーにフォーカス合わしたりしてるかと思いますが、firefox3で登場したアドレスバーの便利機能(おせっかい機能とも言われてるらしい)、「AweSomeBar」
firefox-awesomebar
:open
で使えるのです。例えば
:open twitter teramako
とすると
vimperator-awesomebar
と出るんです。URLなんか殆ど打たなくても良くなります。あとは通常と同じくtabで選べばok。すばらしい。
これでおそらくvimperatorがブラウジング最速になったかと思います。

ところで、vimperator使ってると稀にパススルーモードが外れてしまい、「Google Readerでキーバインドが効かなくなった」とか「LivedoorReaderで上手く動かなくなった」って事になったりします。
私の場合、以下の様にキーマップして回避しています。
noremap <c-i> :js modes.passAllKeys = true<cr>
ただ、これを使ってて気づいたのですがどうやらvimperatorの補完はmapで実行する「:ex」コマンドにも反応してる臭い。これはいずれ直るかと思うけど気になる様だったらパッチを送ってみよう。 とりあえずは、mapで実行するんじゃなくてaddUserMap使ってやる事にした。
javascript <<EOM
liberator.mappings.addUserMap([liberator.modes.NORMAL], ['<C-i>'],
    'Pass through mode',
    function () { liberator.modes.passAllKeys = true }
);
EOM

2008/06/19


グダグダの文章ですみません。
perl-monger.orgってはてなグループじゃダメなの? - DTP+印刷営業メモ

なんとなくだが近所の人がperl-monger.orgな話をしていてさっきページを見ていたんだけど、これってはてなグループ(なんとか部みたいな)のじゃダメなのかなぁ、と思った。

...snip...

っていうかWikiに色々なコンテンツを追加していくてのはダメなんだろうかと普通に思ったんだけど。

Wikiでもログイン制にしてしまえば同じような事は出来るし、弄れば同じ風貌に出来なくもないと思う。ただWikiだと筆者に掛かる責任ぽい物だとか、評価だとか、「perl-mongers」っていう組織感だとかが表現し辛いんじゃないかなぁ。
例えば「Journal of miyagawa」がWikiだったら少し感じが変ってくる様な気がする。(私だけかもしれない)

記事を原稿用紙に書くのかフリースケッチに描くのか...そんな程度の違いなのかもしれないけど、私は今の形がCoolだと思う。

答えになってないなぁ。

追記
組織感ってのは、例えばsubtechみたいなもんかな。


面白いもの見つけた。(via ku's twitter post)
Tumblr, RMagick and a Photo Frame! - igvita.com

Over the course of the past year or so, I got myself into a habit of using Tumblr for storing insightful snippets and quotes from my daily endeavours on the internet. Usually, this entails saving a paragraph (too long for tweet, and not enough for del.icio.us) so I can revisit it later.

However, having Tumblr to save the quotes is nice, but I rarely (if ever) went back to read them. Pondering this dilemma, I spotted my Phillips digital photo frame, and a quick weekend project was born: Tumblr API, RMagick to render the images, and an SD card full of wisdom and quotes for easy consumption!

どっちかって言うと、書いてる理由が面白い。要は「Tumblrの引用は面白いけど一々見に行くのめんどいよね!ならって事でデジタルフォトスタンドを使っちゃおうと思った」て感じかな。

動かすにはrubyのImageMagick拡張rmagickが必要。
私はPhillips digital photo frameは持ってないけど、chumbyとか使うと良く似た事出来るのかな?
ソースはそのまま使えました。日本語はImageMagickがghostscriptを使ってるのでTrueTypeフォント直接指定すれば動きます。
--- render-tumblr.rb.orig   Sat Jun 07 19:32:46 2008
+++ render-tumblr.rb    Thu Jun 19 12:02:57 2008
@@ -62,7 +62,7 @@
        img.background_color = "transparent"
        img.pointsize = 23
        img.antialias = true
-       img.font = "Helvetica"
+       img.font = "c:\\windows\\fonts\\meiryo.ttc"
    end
    quote.write "text.png"
    
@@ -72,12 +72,12 @@
        img.background_color = "transparent"
        img.pointsize = 18
        img.antialias = true
-       img.font = "Helvetica"
+       img.font = "c:\\windows\\fonts\\meiryo.ttc"
    end
    source.write "source.png"  
 end
 
-doc = Hpricot.XML(open("http://igrigorik.tumblr.com/api/read?type=quote&num=5"))
+doc = Hpricot.XML(open("http://mattn.tumblr.com/api/read?type=quote&num=5"))
 (doc/'post').each do |post|
    text = CGI::unescapeHTML((post/'quote-text').inner_html).gsub(/&[^;]*;?/,'')
    source = CGI::unescapeHTML((post/'quote-source').inner_html).gsub(/\<.*?\>/, '').gsub(/&[^;]*;?/,'').strip

実行するとquotesディレクトリに以下の様なファイルが出来上がりました。
render-tumblr-34978042

render-tumblr-36274314

render-tumblr-37077963

render-tumblr-37111519

render-tumblr-38695135

面白いですね。あとは何とかしてこれをchumbyとかで動かせればいいのですが、いかんせんchumby持ってない!chumbyでrubyが動くかどうかも知らない。
って事で誰か後はよろしく。

2008/06/18


試してみた
memcpy 最適化 - id:kazuhookuのメモ置き場

バイト単位でコピーするアホなコードの方が、勝手にベクトル化される分、gcc 内蔵のヤツより最大3倍高速なんだってwww

x64じゃないけど、最近のgccはどれくらい最適化が出来てるのかを見たかったので確認してみた。
#include <stdio.h>
#include <string.h>
#include <memory.h>
#include <time.h>
#include <sys/time.h>

void *(memcpy2)(void *__restrict__ b, const void *__restrict__ a, size_t n){
    char *s1 = b;
    const char *s2 = a;
    for(; 0<n; --n)*s1++ = *s2++;
    return b;
}

static double getsec() {
    struct timeval tv;
    double t;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + (double)tv.tv_usec*1e-6;
}

int main() {
    char foo[BUFSIZ];
    char bar[BUFSIZ];
    int n = 0;
    double t;

    t = getsec();
    for(n = 0; n < 30000000; n++)
        memcpy2(foo, bar, sizeof(foo));
    printf("adhock memcpy: %f\n", getsec()-t);

    t = getsec();
    for(n = 0; n < 30000000; n++)
        memcpy(foo, bar, sizeof(foo));
    printf("normal memcpy: %f\n", getsec()-t);

}
※「-O3 -ftree-vectorize -msse2」
※gcc (GCC) 4.3.0 20080305 (alpha-testing) mingw-20080502

P4 3GHzでこんな結果
adhock memcpy: 3.484375
normal memcpy: 3.296875
オプション効いてるのかなぁ...
ちなみに「-O3」とかオプション全部抜いたら
adhock memcpy: 75.281250
normal memcpy: 4.515625
こんな酷い結果になった。
「-ftree-vectorize」抜いても微妙にしか変らなかった。

シャアにはまだ遠い様だ。

ちなみに、こんな結果もある様なので今後に期待。


blog chart.jp

「ブログチャート」なら、今注目のブログのヒットチャートや木になるブログの影響力レベルがすぐに分かります。

もちろんあなたのブログも登録可能。自分のブログの特徴を他のブログと比較してみて下さい。

http://blogchart.jp/
404 Blog Not Found:"ブログ"とは"\xa5\xd6\xa5\xed\xa5\xb0"ですかとblogchart.jp

文字文字化け化けなmailが来たので調べてみたら...

  • Content-Type:ヘッダーもMime-Version:ヘッダーも不在
  • Subject:ヘッダーもMIME header encodingされていない
  • 全部EUC-JPのまま

もうしわけないけど、最近のSPAMだってこのあたりはきちんとしてるよ!

http://blog.livedoor.jp/dankogai/archives/51067631.html
メールの文字化けもなんだけど、届いたブログチャート貼り付け用スクリプトが
<script type="text/javascript" src="http://blogchart.jp/js/blogparts.js"></script>
<script type="text/javascript"><!--
id="1784";blogurl="http://mattn.kaoriya.net";partstype="b";viewBlogparts();
// --></script>
思いっきりグローバル汚染。...orz

2008/06/17


元ネタ
M.C.P.C.: はてなお気に入りAPIを使ってブログパーツ作例

はてなお気に入りAPIを使ってブログパーツ?を作ってみた...

http://blog.dtpwiki.jp/dtp/2007/09/api_60ed.html
ソースは以下
jquery.hatenaFav.js
// --------------------------------------------------------------
// jquery.hatenaFav.js : hatenaお気に入り画像表示ブログパーツ
// based on: http://blog.dtpwiki.jp/dtp/2007/09/api_60ed.html
// (required: jquery.js)
// --------------------------------------------------------------

(function($){
  $.fn.hatenaFav = function(options){
    return this.each(function(index){
      var it = this, $this = $(this);
      it.opts = $.extend({}, $.fn.hatenaFav.defaults, options);
      if (it.opts.loader) $('<img/>').attr('src', it.opts.loader).appendTo($this);
      else $this.html('loading...');
      $.ajaxSetup({cache:true});
      $.getJSON('http://s.hatena.ne.jp/' + it.opts.user + '/favorites.json?callback=?', function(data) {
        $this.html('');
        $.each(data.favorites, function(index, item) {
          if (index > it.opts.max) {
            $('<a/>').attr('href', '#').click(function() {
              options.max = data.favorites.length;
              $this.hatenaFav(options);
              return false;
            }).attr('class', 'hatena-fav-more').css('font-size', '0.8em').text('more...').appendTo($this);
            return false;
          }
          $('<img/>').attr('src', 'http://www.hatena.ne.jp/users/' + item.name.slice(0, 2) + '/' + item.name + '/profile_s.gif')
          .css({
            width: it.opts.size,
            height: it.opts.size,
            border: it.opts.border,
            title: 'id:' + item.name,
            alt: 'id:' + item.name
          }).wrap('<a/>')
            .parent().attr('href', 'http://d.hatena.ne.jp/' + item.name + '/').attr('target', it.opts.target).appendTo($this);
        });
      });
    });
  };
  $.fn.hatenaFav.defaults = {
    user:   'jkondo',
    size:   '16px',
    border: '0px',
    target: '_blank',
    max:    10,
    //loader: 'http://mattn.kaoriya.net/images/ajax-loader.gif'
  };
})(jQuery);
使い方はこんな感じ
<style type="text/css"><!--
.hatena-fav-container {
    font-family: meiryo, osaka;
    font-weight: bold;
    text-align: left;
    width: 200px;
    border: 1px solid gray;
}
.hatena-fav-container a, .hatena-fav-container a:visited {
    color: blue;
}
.hatena-fav-title {
    background-color: #ddd;
    text-align: center;
}
.hatena-fav-icons img {
    margin: 1px;
}
--></style>
<script type="text/javascript" src="jquery-latest.js"></script>
<script type="text/javascript" charset="utf-8" src="jquery.hatenaFav.js"></script>
<script type="text/javascript"><!--
$(function() {
    $('.hatena-fav-icons').hatenaFav({ user: 'mattn', size: 16 });
});
--></script>
<div class="hatena-fav-container">
    <div class="hatena-fav-title">
        <a href="http://mattn.kaoriya.net/">はてなお気に入り</a>
    </div>
    <div class="hatena-fav-icons"></div>
</div>
hatenaFavメソッドの引数に渡すuserとして、はてなのユーザIDを入れるとそのユーザのお気に入りユーザが表示されます。
実行結果は↓

続きを読む...


2008/06/16


はてなダイアリーモバイルでも、はてなスターが付けられる様になった様です。
モバイル版はてなダイアリーではてなスターに対応しました - はてなダイアリー日記

本日、モバイル版はてなダイアリーではてなスターを見たり付けたりできるようになりました。

これまでモバイル版ではてなスターに対応していたサービスははてなハイクだけでしたが、はてなダイアリーでも同様に☆を閲覧したり追加したりできるようになりました。

http://d.hatena.ne.jp/hatenadiary/20080613/1213342619
モバイル端末で試した所、確かにスターが付けられます。「もしや...」と思って自分のサイトでもやってみたら...
できちゃいました。
スターを付けるURLは
http://s.hatena.ne.jp/star.add?sid={SID}&rks={RKS}&uri={記事URL}&location={元の場所}
の様なのですがモバイル端末ではログインし直す事もある為、リンク先には「ログイン」や「かんたんログイン」へのリンクが張られています。
これがあれば外部ドメインでもスターが付けられるって事ですね。
先日、blosxomでモバイル様にはてなスターを表示する為のプラグインを書いたのですが、その際に修正したフレーバに以下のリンクも付け足しました。
<a href="http://s.hatena.ne.jp/star.add?sid=&rks=&uri=$url$path/$fn.htm&location=$url$path/$fn.htm"><img src="http://s.hatena.com/images/add_bl.gif" border="0"/></a>$hatenastar_mobile::stars<br />
おそらく一度かんたんログイン等でログインすれば、はてなドメイン上のクッキーは少しの間は使える筈なので、二三個付ける場合でも再度ログインする事は無いと思います。

2008/06/13


あんまり確認してないですが...
tomblooハックス - share on WordPressのためのMetaWeblog API poster « ku

MetaWeblog APIはWordPressだけでなくMovableTypeでもサポートされているのでMovableTypeでも使うことができる。ただユーザ名とAPI用パスワードの設定を簡単にはできないので、ファイルに直接書くことになる。

--- 90_MetaWeblog.js.orig   Fri Jun 13 10:45:53 2008
+++ 90_MetaWeblog.js    Fri Jun 13 10:48:48 2008
@@ -427,11 +427,23 @@
        var endpointUri = createURI(this.endpoint);
        var hostname = endpointUri.prePath;
        var formSubmitURL = endpointUri.prePath;
-       var httprealm = null;
-       var logins = lm.findLogins({}, hostname, formSubmitURL, httprealm);
+       var logins = lm.findLogins({}, hostname, formSubmitURL, null);
        var loginInfo = logins.shift();
        if ( ! loginInfo ) {
-           throw "No login infomation found. Please make Firefox remeber your login information at " + this.endpointUri.replace(/\bxmlrpc\b/, 'wp-login');
+           var ps = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);
+           var [user, pass] = [{ value : null }, { value : null }];
+           var ret = ps.promptUsernameAndPassword(
+               window, formSubmitURL, "tombloo metaWeblog poster", user, pass, null, {});
+           if(ret){
+               var nsLoginInfo = new Components.Constructor(
+                   "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
+               loginInfo = new nsLoginInfo(
+                   formSubmitURL, formSubmitURL, null, user.value, pass.value, '', '');
+               passwordManager.addLogin(loginInfo);
+           }
+           if ( ! loginInfo ) {
+               throw "No login infomation found. Please make Firefox remeber your login information at " + this.endpointUri.replace(/\bxmlrpc\b/, 'wp-login');
+           }
        }
 
        var mw = new MetaWeblogAPI(loginInfo.username, loginInfo.password, this.endpoint);
これで外部ファイルにパスワード保存しなくても良くなったりするかなぁ...

時間があまり無いので今度検証する。

2008/06/12


JavaScriptコンソールからパスワードぶっこ抜き - hogehoge

JavaScriptコンソールの上部の入力欄はChrome特権のコードも実行できるんで、XPConnectからゴニョゴニョっとやると平文パスワードを覗くことができちゃうんだね。

PCから離れるときは画面ロックをしないと危険ということかな。

ホントだ怖い。
具体的なコードは

続きを読む...


2008/06/11


これだけメソッドがあれば大概の事は出来る。
The Console Module

The Console module provides a simple console interface, which provides cursor-addressable text output, plus support for keyboard and mouse input.

The Console module is currently only available for Windows 95, 98, NT, and 2000. It probably works under Windows XP, but it hasn’t been tested on that platform.

試しに作ってみた。以下ソース

Oppai.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys, time, codecs
import Console

class Oppai:
  def __init__(self):
    self.nest = 1

  def __getattr__(self, name):
    if name == 'Oppai':
      self.nest += 1
      return self
    else:
      return self.__dict__[name]

  def __call__(self):
    aa = [u"""
    _  ∩
  ( ゜∀゜)彡 おっぱい!おっぱい!
  (  ⊂彡
   |   | 
   し ⌒J
""", u"""
    _  ∩
  ( ゜∀゜)彡 おっぱい!
  (    | 
   |   | 
   し ⌒J
""", u"""
    _  
  ( ゜∀゜)  おっぱい!
  (  ⊂彡
   |   | 
   し ⌒J
"""]

    c = Console.getconsole()
    c.title("Oppai")
    for n in range(self.nest * 4):
      c.page()
      oppai = aa[n % 4 in (0, 2) and (n % 4)/2+1 or 0]
      l = 0
      for line in oppai.split("\n"):
        c.text(0, l, line.encode("mbcs"))
        l += 1
      time.sleep((n % 4) in (0, 2) and 0.5 or 0.15)
Oppai = Oppai()

if __name__ == '__main__':
  Oppai.Oppai.Oppai.Oppai()
使い方は
import Oppai

Oppai.Oppai.Oppai.Oppai()
こんな感じ。動かすと属性参照した分だけ、おっぱいアニメーションが流れます。
おっぱいそん
簡単ですね!

2008/06/06


このサーバではJSON::Syck::Load(YAML::Syck::LoadJSON)が動かなかった(おそらくXSコンパイルされてない)のでJSON::PPを使ってますが、使える人はJSON::Syck::Loadを使うという事で
hatenastar_mobile
# Blosxom Plugin: hatenastar mobile
# Author(s): mattn
# Version: Fri, 06 Jun 2008

package hatenastar_mobile;

use strict;
use warnings;
use vars qw($stars);
use LWP::UserAgent;
use URI::Escape;
use HTTP::Request;
use JSON::PP;

$stars = '';

my $permalink_flavour = 'htm';
my @mobile_ua = qw(UP\.Browser KDDI PDXGW DoCoMo J-PHONE L-mode Vodafone SoftBank);

sub start {
    return 1 if map { $ENV{'HTTP_USER_AGENT'} =~ /$_/ } @mobile_ua;
}

sub story {
    my($pkg, $path, $fn, $story_ref, $title_ref, $body_ref) = @_;
    return 0 if $ENV{'PATH_INFO'} !~ /\.$permalink_flavour$/;

    eval {
        my $uri = 'http://s.hatena.ne.jp/entries.json?uri=' . URI::Escape::uri_escape("$blosxom::url$path/$fn.$permalink_flavour");
        my $ua = LWP::UserAgent->new;
        my $req = HTTP::Request->new(GET => $uri);
        my $res = $ua->request($req);
        $res->is_success or return 0;
        my $json = decode_json( $res->content );
        my @sts = @{$json->{entries}->[0]->{stars}};
        for my $st (@sts) {
            $stars .= '<img src="http://s.hatena.ne.jp/images/star.gif" title="' . $st->{name} . '" />'
        }
    };
    1;
}
パーマリンクとなるflavour名をpermalink_flavourに指定し、flavourに"$hatenastar_mobile::stars"を入れると表示されます。

2008/06/02


GoogleからjQueryやdojoがロード出来るAjaxライブラリが公開されました。

The AJAX Libraries API is a content distribution network and loading architecture for the most popular open source JavaScript libraries. By using the Google AJAX API Loader's google.load() method, your application has high speed, globaly available access to a growing list of the most popular JavaScript open source libraries including:

http://code.google.com/apis/ajaxlibs/
でも、リンク先に載ってるコード、良く見たら動かないよ><
googl ajax library typo
正しくは
<script src="http://www.google.com/jsapi"></script>
<script>
  var renderResults = function(results) {
      $.each(results, function(index, item) {
        $('#result')
          .append('<div id="result' + index + '"></div>');
        $('#result' + index)
          .append('<a href="' + item.url + '">' + item.title + '</a>')
          .append('<div>' + item.content + '</div>')
          .append('<span>' + item.visibleUrl + '</span>')
          .css('padding', '0.5em');
        $('#result' + index + ' div')
          .css('border', '1px dotted gray')
          .css('width', '500px')
          .css('padding', '1em')
          .append('<span>');
        $('#result' + index + ' span')
          .css('color', 'green')
          .css('margin-left', '1em');
      });
  }

  // Load jQuery
  google.load("jquery", "1");

  // on page load complete, fire off a jQuery json-p query
  // against Google web search
  google.setOnLoadCallback(function() {
    $.getJSON("http://ajax.googleapis.com/ajax/services/search/web?q=google&v=1.0&callback=?",

      // on search completion, process the results
      function (data) {
        if (data.responseData.results &&
            data.responseData.results.length>0) {
          renderResults(data.responseData.results);
        }
      });
    });

</script>
<div id="result"></div>
こんな感じになります。あと、visualization等の場合はgoogle.loadの第3引数が有効で"callback"の記述が出来たのですが、jQueryやdojo等サードパーティな物では動かなかった。残念。
ちなにみ上のコードを動かすと、こんな感じになります。

続きを読む...



about: rel-bookmark戦争
『オレ、rel-bookmarkも嫌いなんだわー』とブツクサ言うsnj14 - otsune tumblr まとめサイト 画像保管庫Q
『オレ、rel-bookmarkも嫌いなんだわー』とブツクサ言うsnj14 - otsune tumblr まとめサイト 画像保管庫Q(追記)
3:14 - 『オレ、rel-bookmarkも嫌いなんだわー』とブツクサ言うsnj14
さて、私も少し考えてみた。
Basic HTML data types

Refers to a bookmark. A bookmark is a link to a key entry point within an extended document. The title attribute may be used, for example, to label the bookmark. Note that several bookmarks may be defined in each document.

http://www.w3.org/TR/REC-html40/types.html#h-6.12
mattn的に訳すと

Bookmarkについて。 bookmarkは広範囲に渡るドキュメント内の唯一のキーとなるエントリポイントへのリンクとなります。ブックマークをラベリングする際にはタイトル属性が使用され、ドキュメント内には幾らかのブックマークが定義され得る事に気をつけるべきでしょう。

そしてkuさんが言及している部分
Basic HTML data types

Authors may wish to define additional link types not described in this specification. If they do so, they should use a profile to cite the conventions used to define the link types. Please see the profile attribute of the HEAD element for more details.

http://www.w3.org/TR/REC-html40/types.html#h-6.12
訳すと
作者は本仕様では記述し得ないリンク型を別途定義したいと思うかもしれません。 であれば、リンク型を定義するのに一般的に用いられるprofileを使用すべきでしょう。 その他の詳細に関してはHEAD要素のprofile属性を参照して下さい。
そしてrel-bookmarkについて、microformats wikiでは
rel-design-pattern - Microformats

By convention (citation needed), this entry point also captures the notion of a "permalink".

http://microformats.org/wiki/rel-bookmark
訳すと

一般的には(言及が必要)、このエントリポイントとはpermalinkの概念で表現される物。

意訳過ぎたらごめんなさい
と言う事で
rel-bookmarkとは、それが含まれるドキュメントの内1点を指すべき物であり、rel-bookmarkがドキュメント内で実際のリンク先を指すべき、とまでは決めてない。あくまでpermalink扱い。
が私の考え。が上から読み取った私の解釈。
例えば末っ子の小学生が「姉ちゃん」と言えば「姉」を指し、60代サラリーマンが「姉ちゃん」と言えば「キャバクラの姉ちゃん」になる訳で...
つまりはそのテリトリ内で一意に現すことが出来る何か...がrel-bookmarkなのではないかと思った。


これ、すごいっす。
The Memcache API - Google App Engine - Google Code

High performance scalable web applications often use a distributed in-memory data cache in front of or in place of robust persistent storage for some tasks. Google App Engine includes a memory cache service for this purpose.

http://code.google.com/appengine/docs/memcache/
何が凄いって仕様が凄い。
通常のset/getの他、dictとして格納出来るset_multi/get_multiもある。Tagtterの様にタグ毎にキャッシュしたい場合には持って来いなAPIです。
どうやらdangaをベースにしている様で、LiveJournalsourceforge、さらにはWikiPediaでも使われているライブラリらしいです。

さて、今日Tagtterにこのmemcache APIを使ってみました。
使い方としてはページをそのまま!Tagtterの場合、動的なコンテンツと言えばタグ、Vote、ユーザですが、これらが変更されない限りトップページタグページは変る事は無いのです。他の情報はjavascript(jQuery)で動的に取得しており、サーバには格納されていません。
どんなに大胆な使い方かと言うと
class TopPage(webapp.RequestHandler):
  def get(self):
    cache = memcache.get('tagtter_top_page')
    if cache:
      # キャッシュがあるならそのまま出力
      self.response.out.write(cache)
      return

    ・・・ 巨大なデータ処理 ・・・

    path = os.path.join(os.path.dirname(__file__), get_template(self))
    cache = template.render(path, template_values)
    # キャッシュに溜め込む
    memcache.set('tagtter_top_page', cache, cache_time)
    self.response.out.write(cache)

class AddTagsAPI(webapp.RequestHandler):
  def get(self, username):

    ・・・ タグの追加処理 ・・・

    # 結果を出力
    dump_json(self, { 'status': 'ok', 'message': 'done' })
    # キャッシュをクリアしてやる
    memcache.flush_all()
上記の様に、キャッシュがあればそのまま出力し、タグが追加されればキャッシュをクリアするという方法。
おかげでトップページタグページの表示速度が激変しました。
但し、ユーザ一覧ページ等は、pageというパラメータにより出力する内容が異なる為、このままでは対応出来ません。やっちゃうとどんどん同じページが表示されてしまいます。
今のところ、トップページとタグページにしか取り入れていませんが今後少しずつ取り入れて行きたいと思います。

ところで、公開されたAPIのもう一つImages APIも使ってみました。
使い方は簡単。
from google.appengine.api import images

...

image = images.Image(data) # input data
image.resize(100, 100)
image.rotate(90)
data = image.execute_transforms(output_encoding=images.JPEG)
現状、合成したり90度単位でない回転等は出来ませんが、コレ使えばなんでもありちゃんかいっと思った。いずれやる。

動いている物は以下
Image API Test


以前、MOONGIFTさん所で見付けた「Growl for Windows」を試してみた。
Growl for Windows
Features currently supported:
  • Fully compatible with original Growl for Mac
  • API for local and network applications
  • Notification forwarding
  • Experimental support for WebKit-based display styles
http://www.tripthevortex.com/growl/
まず、Growl本家からtarボールを持ってくる(SDKはdmgなので...)
Growl Developer Downloads

Information regarding the SVN repository and other areas of interest are addressed in the developer documentation. Growl requires Xcode 2.3 to compile, which is available for free from Apple.

Growl 1.1 Source
http://growl.info/downloads_developers.php
次に、"/Growl-1.1-source/Bindings/python"に移動し以下のパッチを当てる。
--- Growl.py.orig   Wed Jan 24 05:36:17 2007
+++ Growl.py    Fri May 30 11:22:18 2008
@@ -72,7 +72,7 @@
         if userInfo.has_key(GROWL_NOTIFICATION_STICKY):
             sticky = userInfo[GROWL_NOTIFICATION_STICKY]
         else:
-            priority = False
+            sticky = False
         data = self.encodeNotify(userInfo[GROWL_APP_NAME],
                                  userInfo[GROWL_NOTIFICATION_NAME],
                                  userInfo[GROWL_NOTIFICATION_TITLE],
おそらく開発者のtypoかと思う。
そして"python setup.py install"する。
気持ち悪い人は"python setup.py bdist_wininst"する

後はサンプルコード
Objective-Cのモジュールをインストールしていないので、netgrowl経由でしか送信出来ないです。
#!/usr/bin/python
import Growl
g = Growl.GrowlNotifier(
    applicationName='GrowlExample',
    notifications=["PyGrowl"],
    defaultNotifications=[0],
    hostname="localhost",
    password="My Really Secure Password")
g.register()
g.notify(
    noteType="PyGrowl",
    title='wanings',
    description='toilet spilled some xxx!',
    sticky=False)
まずGrowl for WindowsをインストールするとシステムトレイにGrowlというアイコンができ、「Settings」を選ぶと設定画面が表示されます。次にNetworkタブを開き
  • Listen for incoming notifications
  • Allow remote application registration
にチェックを入れます。そして"Server Password"に上記ソースにあるpassword、"My Really Secure Password"を入れます。(実際はソースも設定も変更して使用しましょう)
growl settings 1

後は上記サンプルプログラムを起動すると以下の様にGrowlが表示されます。
growl notify
なお、設定の"Applications"にて挙動を変えられますで色々試してみてもいいかもしれません。
growl settings 1
また、上記ではpythonの例を示しましたがperlで"Net::Growl"を使い
use strict;
use warnings;
use Net::Growl;
register(host => 'localhost',
         application=>"growl.pl",
         password=>'My Really Secure Password', );
notify(
       application=>"growl.pl",
       title=>'warning',
       description=>'toilet spilled some xxx more!',
       priority=>2,
       sticky=>False,
);
この様なサンプルでもGrowlは表示されます。
ちなみ、Growl for Windowsのせいなのか幾らかの日本語で文字化けが発生しました。
registerしたアプリケーションを消すには
%USERPFOFILE%¥Local Settings¥Application Data¥Vortex¥Vortex.Growl.WindowsClient
のディレクトリを一斉に消すと設定が消えてくれます。
個別で消す方法は見つかりませんでした。

色々な言語でBindingされている様なので皆さん試してみてはどうでしょうか

2008/05/30


追記
最新のWeb::Scraperでは"Web::Scraper::user_agent"でユーザエージェントが変更出来る様になっています。

昨日書いたエントリ「WWW::MechanizeとXPathでtwitterのfriendsを全部取ってみる」のはてなブックマークでotsune氏から
「Web::Scraper/eg/twitter-friends.pl」
というコメントを貰いました。
ただ、この「Web::Scraper/eg/twitter-friends.pl」は、アンカーのrel属性でcontactとなっている物を並べており、私のようなfollowerが少ない人ならばいいのですが、多い人だと100人超えてしまいます。
結果、100人以上取得出来ない事になります。

以上、おしまい!...というのもアレなので...

無理やり感たっぷりですが、Web::Scraperを使ってみました。
ログインしないと、friendsが辿れないのでそこはWWW::Mechanizeに任せ、LWP::UserAgentを継承しているのを利用して、Web::Scraperの__uaを置き換えました。(ウホ!無理やり)
何か他に、キレイなやり方ないですかねぇ...

#!/usr/local/bin/perl

use warnings;
use strict;
use WWW::Mechanize;
use Web::Scraper;
use YAML;

my $username = 'your_username';
my $password = 'your_password';

my $m = WWW::Mechanize->new(timeout => 10);
$m->add_header('Accept-Encoding', 'identity');
$m->get('http://twitter.com/login');
$m->submit_form(
    form_number => 1,
    fields    => {
        username_or_email  => $username,
        password           => $password,
    },
    button    => 'commit',
);

undef &Web::Scraper::__ua;
*Web::Scraper::__ua = sub {
    $m;
};
my $twitter = scraper {
    process 'tr.vcard',
        'friends[]' => scraper {
            process 'td strong a', nick => 'TEXT';
            process 'td.thumb img', image => '@src';
            process 'td.thumb img', name => '@alt';
            process 'td strong a', description => '@title';
            process 'td.thumb a', url => '@href';
        };
    result 'friends';
};

my $num_page = 1;
while (1) {
    my $uri  = URI->new("http://twitter.com/friends/?page=$num_page");
    my $friends = $twitter->scrape($uri);
    %$friends or last;
    $num_page++;
    warn Dump $friends;
}


全然難しくない話
filtered feed for coderepos
CodeReposフィードにはcategoryにshare配下のパスが埋め込まれている様なので、これをマッチングに使ってるだけです。
"Search Text"には"lang/javascript"とか"lang/python"とか入力します。
これで"lang/javascript"以下だけ欲しい人とか、"lang/perl"以下だけ欲しい人とか、"poem"だけ欲しい人とかがウォッチしやすくなるのかな? ちなみに正規表現でやるまでもなかいかな...と思って単純マッチングにしてあります。正規表現がお好み人はcloneして変更して下さい。
Pipes: Filtered Feed for CodeRepos

2008/05/28


「はてなスター」の置かれている位置によっては思いっきり誤爆してしまうhatenaStar.jsですが、皆さんのご不満を軽減すべく誤爆確認機能をつけました。これまで
:hatenastar 1
としていた場面で
:hatenastar 1?
とすると、つけようとしている「はてなスター」がブリンクします。
点灯している「はてなスター」を見て、「あぁ、あそこに付けるんだな」と思って頂ければと思います。
あとは安心して":↑<bs>"でコマンドヒストリを出してバックスペースで"?"を消し
:hatenastar 1
と実行して下さい。
hatenaStar.js

2008/05/27


追記
検索順位が変ってしまったようです。

おそらくこれ、Technoratiがやりたかった事のはず。

- Yahoo! Search Results
http://www.yr-bcn.es/demos/microsearch/

microsearch [Wiki]

Using microformats

We support two kinds of microformats: hCard and hCalendar. hCard is used to add personal information to your webpage, hCalendar is used to add events. You can use the hCard creator and the hCalendar creator to create the code that you will need to insert into your webpage.

http://www.yr-bcn.es/dokuwiki/doku.php?id=microsearch#using_microformats
1段目の引用部にあるリンクで、「mattn」と検索してみて下さい。おそらく10秒から20秒程かかりますが検索結果が現れるかと思います。他の検索サービスと違う事に気付いた方、すばらしい。
そう、microformatsを使ってるのです。このサイト「Big Sky」に埋め込まれたmicroformatsの内、「hCard」と呼ばれる物(ここでは「Osaka, Japan」)を使用して地図上の大阪付近に「mattn」とプロットしているのです。
microsearch
2段目の引用部

Using microformats

hCardとhCalendarという、2種類のmicroformatsをサポートしています: hCardとは、貴方のウェブページに個人情報を加えるのに用いられ、hCalendarはイベントを加えるのに使用されます。 これらをウェブページに追加するには挿入するにはhCard CreatorFOAF-a-matic -- Describe yourself in RDFを使う事でコードを生成出来ます。

から、Yahooもmicroformatsに何かしらの期待を持っている事が伺えます。いずれ検索メタデータとして扱っていこうという事でしょうか。
リンク先には、今後GRDDLへの対応も予定されているので、ちょっとYahooが面白い事をやってくれそうな気がしてきました。

2008/05/26


以前、「Big Sky :: 意外と知られていないvimのtips(calendar.vimの日記機能をGoogle Calendarと同期させる)」という記事を書いたのですが、どうやらこれを完成品にまで作り上げてくれた人が居るようです。
見たところ、私のソースを流用されてはいなさそうですが仕組みは似通っています。
diaryvgc - Google Code

Synchronism Python Script between VIM Calendar diary and Google Calendar .

DiaryVGC == Diary Vim Google Calendar

http://code.google.com/p/diaryvgc/
私の場合はファイルの存在や更新時刻で動作しますが、こちらはログファイルを使った全て。また私の様に「--- Google Calendar ---」という文字列で対象ファイルを絞ったり、イベント期間を設定出来たりはしません。
私の場合は「--- Google Calendar ---」という文言を見付けて、それだけを対象ファイルとしていますが、こちらはログファイルを使った全て。
ただ、中国の方のようでコード内にcp936やらgb2312といったエンコーディング名がちらほら。
適当にpatch当てて、動くまでは確認しました。
Index: diaryvgc.py
===================================================================
--- diaryvgc.py (revision 4)
+++ diaryvgc.py (working copy)
@@ -257,15 +257,15 @@
                             if sys.platform[:3] == 'win':
                                 try:
                                     diaryinfo = open(tfile).read()
-                                    info_done = unicode(diaryinfo,'cp936')
+                                    info_done = unicode(diaryinfo,'mbcs')
                                     #print "=============win===UTF8=============="+tfile
                                 except Exception,ex:
-                                    diaryinfo = codecs.open(tfile,'r','gb2312').read()
+                                    diaryinfo = codecs.open(tfile,'r','mbcs').read()
                                     info_done = diaryinfo.encode("utf-8")
                                     #print "============win====GB2312=============="+tfile
                             else:
                                 try:
-                                    diaryinfo = codecs.open(tfile,'r','gb2312').read()
+                                    diaryinfo = codecs.open(tfile,'r','mbcs').read()
                                     info_done = diaryinfo.encode("utf-8")
                                     #print "=============notwin===GB2312=============="+tfile
                                 except Exception,ex:

使い方は
diaryvgc.py --user your-gmail-id --pw your-pass-word --dir /path/to/vim/diary
で更新です。
追記
よくみたら場所で「China」がデフォルトになってますね。
使う人は適当に「Japan」とかに書き換えるのが良いかと...

自分が作った物が何かしらの形で流用されるってのは、うれしいですね。


先日、Open Tech Pressの連載コラム「Vimマスターへの道」の中に「Vim/gVimをユーザフレンドリなエディタに変えるCream」という記事を見つけました。
Creamは以前から知っており、Creamの作者から「calendar.vim」の取り込み可否を尋ねられた事があります。現在は少し古いバージョンがCreamにバンドルされています。
上記本文中に、「...例えばカレンダ機能を追加するCalendarプラグインが使えないと自分は生きていけないと語っていた者と何回か出会ったことがある。」の文言を見つけた時には正直嬉しかったです。
ただ、Calendarに付属している日記機能は現状メモ書き用途にしか使えず、現状のCalendarは単なるカレンダでしかありません。何かとリンクする訳でもなく、このデータを使ってvimと連携する事もありません。しいて言うならば日記ファイルがあるとcalendar.vimで日付が点灯するくらいです。

vim_calendar1

今日は、あまりvimと関係しないかも知れませんが、このcalendar.vimとGoogle Calendarのスケジュールを同期するプログラムをご紹介したいと思います。
Google Calendarは、Googleが無償で提供するカレンダアプリケーションです。
携帯からも使用でき、ホビーユーザからヘビーユーザまで満足出来るアプリケーションです。
Google CalendarにはAPIが公開されており、Googleのアカウントをお持ちで開発経験のある方ならば自前のアプリケーションからGoogle Calendarに対してアクセスする事が出来ます。
今回はPythonを使用しますのでgdata-python-clientから「gdata.py-1.0.5.tar.gz」をダウンロードしました。
calendar.vimでは、ホームディレクトリに置かれた「diary」というディレクトリ(変更可能)から年月日をそれぞれディレクトリに分割した構成で拡張子「.cal」のファイルを管理します。
この「.cal」ファイルにGoogle Calendarのスケジュールを取得し、書き込みたいと思います。
gdata-python-clientでは、Googleアプリケーションのデータストレージ(gdata)に対する認証からカレンダへのアクセスまで行ってくれます。
認証は以下の手順で行います。
cal_client = gdata.calendar.service.CalendarService()
cal_client.email = myemail
cal_client.password = mypasswd
cal_client.source = "Google-Calendar_Python_Sample-1.0"
cal_client.ProgrammaticLogin()
これで、GData認証サービスへのトークン取得、認証までが完了しています。
そして以下がカレンダへのアクセスです。今回は当月から3ヶ月分のスケジュールを取得しています。
today = datetime.date.today()
query = gdata.calendar.service.CalendarEventQuery("default", "private", "full", "")
query.start_min = str(today + relativedelta(day=1))
query.start_max = str(today + relativedelta(months=+3,day=1,days=-1))
feed = cal_client.CalendarQuery(query)
for i, an_event in enumerate(feed.entry):
  for a_when in an_event.when:
    print an_event.title.text, a_when.start_time, a_when.end_time
今回は、横着して月始めと月末の計算に便利な「dateutil」を使用しています。
これで、当月の一日から、2ヵ月後の月末までのイベントオブジェクト(feed)が取得出来ましたので、これをcalendar.vimの日記ファイルとリンクします。
def update_schedule(path, an_event, a_when):
    dir = os.path.dirname(path)
    if not os.path.isdir(dir):
        os.makedirs(dir)
    lines = []
    resep = re.compile("^" + schedule_sep + "$")
    has_data = False
    if os.path.isfile(path):
        fp = open(path)
        for line in fp.readlines():
            if resep.search(line):
                has_data = True
            lines.append(line)
    if not has_data:
        lines.append("%s\n" % schedule_sep)
    lines.append("%s - %s\n" % (
        a_when.start_time.replace("-", "/"),
        a_when.end_time.replace("-", "/")))
    lines.append("\t%s\n" % an_event.title.text.encode("utf-8", "replace"))
    print "updating schedule of '%s'" % path
    fp = open(path, "w")
    fp.writelines(lines)
    fp.close()
schedule_sepは「--- Google Calendar ---」というセパレータを示し、「.cal」ファイルの下部からデータが開始します。
データは、「開始日付 - 終了日付」の次の行にイベント名をインデントした以下の形式にしています。
--- Google Calendar ---
2007/09/25 - 2007/09/26
    休暇予定
上記update_scheduleでは、セパレータ以降にGoogle Calendarから取得した開始日付、終了日付、タイトルを書き込んでいます。

ただ、このアプリケーションを何度も実行すると同じデータが繰り返し追加される事になりますので、update_scheduleと同様の処理でセパレータ以降をクリアするremove_scheduleを実装し
# clean exising schedules
for i, an_event in enumerate(feed.entry):
  for a_when in an_event.when:
    remove_schedule(datetime2diary(a_when.start_time))

# append newer schedules
for i, an_event in enumerate(feed.entry):
  for a_when in an_event.when:
    update_schedule(datetime2diary(a_when.start_time), an_event, a_when)
とする事で毎回データを作り直しています。
これをあとは、プログラム引数からユーザIDとパスワードを貰い、動作するアプリケーションとして仕上げれば以下のようなCalendarの画面が出来上がります。

vim_calendar2

一応、Windows版のPython2.5で検証してますが、おそらく2.4でも動作するかと思います。
このアプリケーションには、まだ更新する機能がありませんが、ぼちぼち暇を見つけて更新して行こうかと思います。

ダウンロード:


2008/05/23


I wrote a plugin which moved the focus along the input fields.
When the web page has multiple <input> or <textarea> fields,
the focus walks the input fields by pressing A-i (or M-i).

http://www.pqrs.org/tekezo/tmp/vimperator/walk-input.js

It seems there was no similar function in the core code, would you merge this?

Regards,
Takayama Fumihiko.
http://www.mozdev.org/pipermail/vimperator/2008-May/001497.html
便利。<M-i>で順に入力フィールドへフォーカスが飛ぶ。
Dotan Cohenから「TABとどう違うの?」って返信への返信通り、"gi"だと届かないフィールドへフォーカスを飛ばすのはTABの押しすぎで指がつる。
使わせて頂く事にしよう。

tekezo++


Perlハブサイトコアメンバーに訊きたい質問 - [51]注目!パソコンの使い方-はてなダイアリー

Perl日本人ユーザーハブサイト(Perl-users.jp - 日本のPerlユーザのためのハブサイト) が立ち上がり、他言語でもユーザーハブサイトができ、挙句の果てにはRailユーザーハブサイトまでできた模様。

これまでいくつものPerlユーザーハブサイトをコピペしてPerlを使ってきた立場から今回のサイトが末永く続くように運営に関わろうとする篤い志の方へその志の大きさを客観的に示してもらえるよう質問を用意しました。プロフィールページがあったら少しはサイトも長く続くかと考えています。

質問の順番も重要でしょうから、追加・改変はご自由に、コピペして100の質問のようにご利用ください。

http://d.hatena.ne.jp/Akira51/20080523/1211500311

  • rubyは好きですか?
  • pythonは好きですか?
  • perl命と言いながらjava-jaに参加してませんか?
  • 織田祐二は好きですか?
  • 今日のパンツは何色ですか?

2008/05/19


lomoさんが言ってた
caramel*vanilla » Twitterユーザーにタグ付けするWebサービスが欲しい
共通点のあるユーザーを効率的に探したい
... 誰かなんとかしてくれませんか?
http://caramel-tea.com/2008/05/twitter_tagging/
を作ってみた。まだ途中でlomoさんが要望してる機能の内
caramel*vanilla » Twitterユーザーにタグ付けするWebサービスが欲しい
  • 誰でも自由にユーザーページを登録できる
  • 誰でも自由にユーザーにタグをつけられる
  • Bookmarkletかなにかで今見ているTwitterのユーザーページを登録する仕組みを作る
  • 既存のタグに対して+-して数をカウントする
  • ユーザーごとの個別ページにはタグクラウドを表示する
  • ユーザーごとの個別ページにはアイコンと最新発言を表示する
  • ユーザーごとの個別ページからFollowできるようにする
  • GreasemonkeyなどでTwitterのユーザーページにもタグクラウドを表示させる
  • ユーザーごとの個別ページにはそのユーザーにタグをつけたユーザーのアイコンを表示させる
  • 付けられたタグが似たユーザーのアイコンを表示させる
  • タグごとのランキングページを生成する
  • ユーザーページとランキングページのRSSを配信する
http://caramel-tea.com/2008/05/twitter_tagging/
までしか出来てない。
まぁとりあえず動いてるんで遊んでやって下さい。
なお、ログインするためにはtwitterでログインしている必要があります。お気を付けて。
「add User」でユーザを追加して、ユーザページにて「add Tags」でタグが追加出来ます。ログイン状態であればユーザページ上のタグにマウスを当てると「+-」が表示されます。
Tagtter - Find twitter's fun on tag cloud!
まだbetaですし、データ構造も変えるかもしれないので、今後変更削除してしまうかもしません。ご了承下さい。

Enjoy!

2008/05/13


これまで本サイトの画像の扱いはblosxomのimagerefという、自作ながらお粗末なプラグインを使っていました。
私としては、記事本文に記述される画像へのパスはサイトのURLとは切り離して考えるべきと思っているので、以下のプラグインを使っています。

imageref
# Bloxsom Plugin:ImageRef
# Author: Yasuhiro Matsumoto
# Version: 0.1

package imageref;
use strict;
my $images_url = "http://mattn.kaoriya.net/images";
sub start {1}
sub story {
    my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
    $$body_ref =~ s@<!--\s+imageref\s+thumb:([^:\. ]*)(\.\S+)?\s+-->@<a href="$images_url/$1$2" rel="shadowbox"><img src="$images_url/$1-thumb$2" alt="$1" border="0" /></a>@ig;
    $$body_ref =~ s@<!--\s+imageref\s+thumb:([^:]+?):([^:\. ]*)(\.\S+)?\s+-->@<a href="$images_url/$2$3" rel="shadowbox"><img src="$images_url/$2-thumb$3" alt="$1" border="0" /></a>@ig;
    $$body_ref =~ s@<!--\s+imageref\s+([^:\. ]*)(\.\S+)?\s+-->@<img src="$images_url/$1$2" alt="$1" border="0" />@ig;
    $$body_ref =~ s@<!--\s+imageref\s+([^:]+?):([^:\. ]*)(\.\S+)?\s+-->@<img src="$images_url/$2$3" alt="$1" border="0" />@ig;
    1;
}
1;
※今回の修正を含んだソースです。
※少しvimの色付け変ですね...
これを使うと、記事本文に
以下の画像を見てください。<br />
<!-- imageref パンダ:panda.png -->
と書くことで
<img src="http://mattn.kaoriya.net/images/panda.png" alt="パンダ" border="0"/>
というHTMLが出力されます。例えばサイトの引越しや画像格納パスの変更で全ての記事に手直しをする必要が無くなる訳です。

また、このプラグインでは
以下の画像を見てください。<br />
<!-- imageref thumb:パンダ:panda.png -->
と書く事で
<a href="http://mattn.kaoriya.net/images/panda.png"><img src="http://mattn.kaoriya.net/images/panda-thumb.png" alt="パンダ" border="0"/></a>
と変換してくれる為、使い手側は「panda.png」とサムネイル画像「panda-thumb.png」を用意しておけばいい事になります。
実際は、「panda.png」から「panda-thumb.png」への画像変換は手作業で
convert panda.png -resize 300x200 panda-thumb.png
※convertはImageMagick付属コマンド
とやってるのですが、ここも本当はオートメーション化したいと思っています。

で、今回はこのプラグインを紹介したかったのではなく、jQueryでサムネイル画像から実画像への表示をビジュアルに表現してくれる「Shadowbox」というプラグインを導入しました。
mjijackson » Shadowbox 1.0 released
なぜ導入に至ったかというと、このimagerefプラグインを少し修正すれば簡単にShadowboxが使えるのでは無いかと思ったからです。
Shadowboxを使うためには、jqueryのロードは必要になります。そして初期化処理として
<script type="text/javascript"><!--
$(document).ready(function() {
    Shadowbox.init();
});
--></script>
というコードを書き、あとは
<a src="xxx-big.jpg">
  <img src="xxx-small.jpg"/>
</a>
となっている部分に「rel="shadowbox"」を足して
<a src="xxx-big.jpg" rel="shadowbox">
  <img src="xxx-small.jpg"/>
</a>
とすれば良し。
class属性ではなく、rel属性を使っているので少し気味が悪い気もしますがそんなの気にしない。

今回はこれをimagerefが勝手に出力してくれる様に修正したまでです。

これでいつも通りにimagerefを使えば
<blockquote>
    <!-- imageref thumb:giraffe-photo.jpg -->
</blockquote>

キリン
となる訳です。
ちなみにこのShadowbox、画像だけでなく
twtter - mattn
といったインラインHTMLやFlash、動画もサポートしている様です。結構手軽に導入出来る割に得られる効果が大きいかもしれません。
一度使って見られてはどうでしょうか?

関連記事
MOONGIFT: » クールに表示「Shadowbox.js」:オープンソースを毎日紹介

追記
shadowboxやめてjquery.lightbox(balupton edition)にしました。


そろそろNabeAtzzにも飽きてきたので、エドはるみグリモン作ってみた。
Firefoxで閲覧中に現れる「ing」とか「グ」に反応してエドはるみが登場します。 Googleでsomethingとか検索してしまったら鬱陶しい事間違いありません。
edo-good
動作画面はこのような画面になります。
good-goo-goo-goo-good.user.js
ぜひご活用下さい。

2008/05/11


twitterを辞めてwassrに移ってしまった皆様、常にwassrだけ常駐されておられる皆様、こんにちわ。
vimperatorでtwitter出来るvimperator plugin「twitter.js」を改造してwassr出来る「wassr.js」を作ってみたよ。
機能としては
  • ステータス更新
  • ステータス参照
  • TODO管理
  • 足跡参照
が出来ます。まずステータス参照は「twitter.js」と同じく
:wassr
ステータス更新は
:wassr wassrかわいいよwassr
TODOは
:wassr -todo
で一覧表示
:wassr -todo+ かめはめ波を発射する
でTODO追加。
追加後のID一覧が表示されます
:wassr -todo- XXXXXXX
で削除。
XXXXXXXはTODOのID
:wassr -todo* XXXXXXX
で開始
:wassr -todo/ XXXXXXX
で停止
:wassr -todo! XXXXXXX
で完了となります。TODOのIDは補完が効くので<tab>で選んで下さい。
また
:wassr -footmark
で足跡の一覧も見れます。
オプションは
:wassr<tab>
で候補が出ます。
よろしければどぞ。
wassr.js

wassrかわいいよwassr

2008/05/09


元々はきゃーさん所で知った「小粋空間: サイドメニュー折りたたみの動作遅延対処」をパクってやってたんですが、良く考えたらこのサイトってjQuery使ってたなぁ...と
$.each({
    tags : true,
    category : true,
    archives : true,
    comments : false,
    trackbacks : false
}, function(i) {
    var f = $('#' + i + 'name');
    f.click(function() {
        $('span', this).html(($('#' + i + 'list').css('display') != 'none') ? '▼' : '▲');
        $('#' + i + 'list').toggle('fast');
    }).css('cursor', 'pointer').append('<span>▲</span>');
    if (this == true) f.click();
});
すんなり少量のコードになりました。jQuery++


twitter検索しまくり - noreplyのブログ
twitter_viewで,fとかすると今表示されてるステータスをfav
http://d.hatena.ne.jp/noreply/20080503/1209785708
twitter_viewってtwitteViewer.jsの事だったんだろうかと悩み中...

2008/05/02


以前、「Big Sky :: 意外と知られていないvimのtips(vimでソースコードをHTML化するコツ)」という記事を書いて、なかなか好評だったのですが(前のサーバで公開した記事なので反響は見えませんが)、vimでHTMLを編集しておられる方って結構多いと感じました。
私もしかり、このブログにおいても全てvimで編集しています。
ソースコードは上記リンクに記した通り「:TOhtml」を使い、その他はほぼ全て打ち込んでいます。ただ別のサイトにある記事やリンクを引用する場合、少し手間が発生します。例えば
<a href="http://example.com">サンプル</a>
とタイプする場合、ブラウザのアドレスバーからURLを、さらにブックマーク画面を出してタイトルをコピーして上の様な記述に置き換えるとなるとキーボードだけで操作出来なかったりして少し煩わしい感じがしますね。オーサリングツールやmarkdown等で楽する事も出来ますがちょっとした更新で、しかもvimが使いたいなんて場合もありますよね。
また、ブラウザは起動していないけどURLならもう知ってるなんて場合、タイトル抜き出すのが大変なんて思っていませんか?
今日は私が使っているvimスクリプトを紹介してみます。
まずソース
let g:Anchorize_Format = '<a href="%s" class="external" target="_blank">%s</a>'
function! Anchorize_TITLE(url)
  silent! split _FETCHTITLE_
  silent! exec "0r!curl -s ".a:url
  if executable('nkf')
    if &enc == 'utf-8'
      silent! %!nkf -X8
    elseif &enc == 'cp932'
      silent! %!nkf -Xs
    endif
  endif
  silent! %join!
  silent! %g/^\s*$/d _
  silent! %s/^.\{-}<title[^>]*>\([^<]\+\)<\/title>.*/\1/i
  let ret = getline('.')
  silent! bw!
  if exists('Anchorize_Format')
    let format = Anchorize_Format
  else
    let format = '<a href="%s">%s</a>'
  endif
  return printf(format, a:url, ret)
endfunction
function! Anchorize_URL(v)
  let url = input('URL:')
  if len(url)
    if exists('Anchorize_Format')
      let format = Anchorize_Format
    else
      let format = '<a href="%s">%s</a>'
    endif
    if a:v
      silent! normal! gvs
      let word = getreg('"')
      exec "normal! a".printf(format, url, word)
    else
      let word = expand('<cword>')
      exec "normal! ciw".printf(format, url, word)
    endif
  endif
endfunction
nnoremap <leader>,u :call Anchorize_URL(0)<cr>
vnoremap <leader>,u :<c-w>call Anchorize_URL(1)<cr>
vnoremap <leader>,U s<c-r>=Anchorize_TITLE(getreg('"'))<cr><esc>
これをanchorize.vimとかでpluginディレクトリに放り込むとインストール完了。
使い方は、まず
今日もGoogleで検索
の「Google」という部分を「http://www.google.co.jp/」へのリンクにしたい場合には「Google」の部分までカーソルを移動して
<leader>,u
「<leader>」は何も設定していなければ「¥」になっているかと思います。
とタイプすると
URL:
というプロンプトが出るのでここに
URL:http://www.google.co.jp/
とタイプしてENTERを押すと
今日も<a href="http://www.google.co.jp/">Google</a>で検索
と変換されます。また空白を含んだ、例えば「Google Search」の様な場合「Google Search」をビジュアル選択して「<leader>,u」とすれば同じ動作になります。
さらに例えば
今日もhttp://b.hatena.ne.jp/でホットエントリ
というテキストのURL部分をビジュアル選択し、今度は
<leader>,U
「U」は大文字
とタイプすると
今日も<a href="http://b.hatena.ne.jp/">はてなブックマーク - ソーシャルブックマーク</a>でホットエントリ
と変換してくれます。
急いでネタ記事をアップしたいvimmerには重宝するのではないかと思います。なお、動作にはnkfというフィルタコマンドが必要です。Windowsであれば「nkf win32」あたりで検索すればヒットするかと思います。ちなみに、vimrc等でAnchorize_Formatという変数を
let g:Anchorize_Format = '<a href="%s" class="external" target="_blank">%s</a>'
等といった感じに編集する事も出来るので、class属性を常に付けたい人にも使えるかと思います。
元々公開するつもりも無かったスクリプトなので、拡張性ありませんがよろしければどうぞ。


vimperator otsuneさんが以前、「Firefox 3.0b5とvimperator0.6とAutoPagerizeとLDRizeとMinibufferとldr_cooperation.jsを組み合わせると快適過ぎる - otsune's SnakeOil - subtech」で紹介していたvimperator、もうインストールしてしまった人はいるだろうか。
まだインストールしていない人は、今後vimperaorでウェブブラウジングが実際にどの様に変っていくかが知りたいかと思います。
Firefoxをvimぽくする拡張、vimperatorを入れると、まさしくFirefoxがvimぽくなる。このぽくが重要。vimのままを期待している人は拍子抜けするかもしれない。でも、キーボードしか触りたくないユーザにとってvimperatorは心地よい操作感を与えてくれると思います。
一般的な使い方は「ナレッジエース - Firefoxをキーボード操作できるプラグイン「Vimperator」の使い方」が参考になるかと思います。
ここでは通常のウェブブラウジング以外の操作がどの様に変るかをご紹介したいと思います。
例えば通常以外の操作を以下の様に大別してみます。
  • Googleでの検索
  • ソーシャルブックマークへポスト
  • twitter
  • はてなスター

Googleでの検索

vimperatorではテキスト選択もキーボードで行う。「i」を押してキャレットが表示されるので「hjkl」で移動したり「0」や「$」で行頭や行末に、「gg」や「G」でページの先頭やページの末尾に移動する。選択したくなったら「v」を押して選択モードに移り、同じく「hjkl」等で移動後「y」でyank(コピーの意味)出来ます。 さらに単語を検索もキーボードで。例えば上のGoogle検索結果で「firefox」を検索するならば
/firefox<ENTER>
と入力します。「firefox」という単語がハイライトされvimの様に「n」で次の検索単語へジャンプ出来る。リンクの上までジャンプし「ENTER」を押すとリンク先にもジャンプ出来ます。
そして肝心な検索ですが、vimperatorには既に数多くのpluginが作られており、その中の一つに「googlesuggest.js」があります。
この「googlesuggest.js」を使えば先ほどyankした単語を簡単に検索出来ます。
:google <s-insert><ENTER>
「<s-insert>」はSHIFTを押しながら「INSERT」キー。「<c-v>」(CTRLを押しながらvでもok) とすればGoogleの検索結果が出来ます。ただ、そこまでしなくても「googlesuggest.js」を使えば
:google fire<tab>
とすれば
:google firefox
と補完してくれます。
「googlesuggest.js」はcodereposからダウンロード出来るので「~/.vimperator/plugin」(Windowsなら「%USERPROFILE%¥vimperator¥plugin¥」)にコピーすれば完了(ブラウザ再起動「:restart」が必要)。

ソーシャルブックマークへのポスト

これもvimperator pluginで行う。使うのは「direct_bookmark.js」でインストール方法は「googlesuggest.js」と同じ。
ブックマークしたいページを見付けたら
:sbm
タグやコメントを付けたいならば「はてな方式」で
:sbm [vimperator]それvimperatorで出来るよ
といった感じに。現状既に「はてなブックマーク」、「del.icio.us」、「livedoor clip」に対応しています。タグは補完が可能でサーバからタグ一覧を再取得するには
:btags
さらにブックマークエントリページに行きたいならば
:bentry
「:bentry h」で「はてなブックマーク」に...といった感じ

twitter

twitterは「twitter.js」を使う。
:twitter
とすればfollowerのステータスが表示されます。
vimperator-twitter
ステータスを更新したければ
:twitter 声が小さい!おいっす!
とでもすればいい。

はてなスター

「hatenaStar.js」を使う。使い方は
:hatenastar
現状豪快仕様プラグインでページ内の全ての「はてなスター」をクリックする為、「はてなスター」がいっぱい付いたページで実行するとエライ事になるので注意。
既にid:retletさんがやってしまってますが...
一応、何番目の「はてなスター」かを引数でも指定出来るので
:hatenastar 1
とかしとくとブラウザが爆発する心配もありません。

その他、色んなvimperator pluginがあるので興味ある方は試してみてはどうでしょうか。
coderepos にあるvimperator plugin
また「ldr_cooperation.js」を使えばgreasemonkeyのminibufferと共存する事も出来るので、キーボードだけでブラウザを操作したい人には「ウマー」な環境が出来上がるかと思います。

Firefox 3のリリース時に、「えっvimperatorで出来ないの?」なんて事にならない様、vimperator plugin開発者の皆様、頑張っていきましょう。

2008/05/01


幾らかのサーバにしか対応出来ていませんが、とりあえずリリース。
現在
  • はてなダイアリー
  • はてなグループダイアリー
  • ライブドアブログ
  • Seesaa ブログ
  • Nowa
に対応出来ています。入力欄にpermalinkを入力して「Generate RSS」をクリックするとフィードが表示されます。
BeautifulSoupを使っているので、あまり白熱したコメント欄だとサーバが火を噴くかもしれません。
また過激な議論、18才未満に相応しく無いもの等は、強制でフィード出力を停止する場合があります。

CommentRSS
よろしかったら使ってみて下さい。


日頃よりご愛好頂きありがとうございます。まぐれです。
おそらくまもなくランキングから消え去りますので、記念に...
feed-meter-297-20080501

2008/04/28


床屋。もし結果同じ髪型に仕上がる床屋が2軒あったとして、片方は5分で片方は40分で仕上がる店があったとしたら。私は迷わず40分掛かる床屋に行くだろう。

髪切り落とされる様をただ見る時間。髭をそるクリームを塗られ、髭を剃られるまでの「じらし」。これも床屋の魅力であると思う。
別にしっかり肩を「パンパン」叩いてくれとも思わない。しっかり叩いたら床屋で無くなる気がする。

もしかしたら、コンピュータ業界も同じなのかもしれない。
要件定義から、すぐさま答えの出る業界ならば面白くもなんともないかもしれない。
ああでもない。こうでもないと考えてこそ面白いのであって、仕様が、そして納期が決められているからこそ面白いのだ。別の担当者が作る部品の完成をドキドキしながら待つ時間。それも床屋と同じと考えれば気が楽になったりしないだろうか。しないか...
もしかしたら気の効きすぎた、仕事を100%こなすソフトウェアというのは、実は魅力的でないのかもしれない。

プログラミング言語を取ってみても、各言語毎にそれぞれライブラリがあり目的を達成させる為の近道になる物が沢山ある。
それも良い事だとは思うけれど、気の効いてない言語もまた楽しかったりするんだろうな。

でも、髭くらいはちゃんと剃れ。

2008/04/24


書き換えてしまえ...
*** elementtree/ElementTree.py.orig Thu Apr 24 21:02:09 2008
--- elementtree/ElementTree.py  Thu Apr 24 21:34:42 2008
***************
*** 1252,1254 ****
--- 1252,1267 ----
          tree = self._target.close()
          del self._target, self._parser # get rid of circular references
          return tree
+
+ try:
+     from xml.parsers import expat
+ except ImportError:
+     import xmllib
+     import SimpleXMLTreeBuilder
+     def __init__fake(self, html=0):
+         self.__init__orig(html)
+         xmllib.XMLParser.__init__(self, accept_utf8=1)
+     clazz = SimpleXMLTreeBuilder.TreeBuilder
+     clazz.__init__orig = clazz.__init__
+     clazz.__init__ = __init__fake
+     XMLTreeBuilder = SimpleXMLTreeBuilder.TreeBuilder
ちなみにpatch当てなくても、ElementTree.pyの最下行に

try:
    from xml.parsers import expat
except ImportError:
    import xmllib
    import SimpleXMLTreeBuilder
    def __init__fake(self, html=0):
        self.__init__orig(html)
        xmllib.XMLParser.__init__(self, accept_utf8=1)
    clazz = SimpleXMLTreeBuilder.TreeBuilder
    clazz.__init__orig = clazz.__init__
    clazz.__init__ = __init__fake
    XMLTreeBuilder = SimpleXMLTreeBuilder.TreeBuilder
を足せば出来上がる。こうすると大概のものは小細工無しに動くと思う。たぶん
エラーが出てたGoogle App Engineのサンプル、muvmuvのレビュービルド「http://localhost:8080/build」も問題なく通った。
良いやり方ではないですが...


Google App EngineでwebSimple.pyを使って作りました。

twitterのfollower発言で、漢字が読めなかった貴方。「@mattn_jp それなんて読むの?」とか聞くのが恥ずかしい貴方。そんな貴方にピッタリのサービスです。twitter followerの発言に「読み」を付けて表示します。(まぁ私は難しい単語なんて使いませんが...)

文字の分解にはYahoo! JAPANの「日本語形態素解析Webサービス」を使用しています。

色の変っている部分にマウスを当てると、ツールチップにて読みを教えてくれるようになっています。

よみふったー
で、いきなりですが「よみふったー」のソースです。
ライブラリとしてはwebSimple.pyのほかにPyWrapperに含まれるElementTree、BeautifulSoupを使っています。
#!-*- coding:utf-8 -*-
import os
import re
import base64
import xmllib
import logging
import elementtree.SimpleXMLTreeBuilder as xmlbuilder
import wsgiref.handlers
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from BeautifulSoup import BeautifulSoup
from webSimple import Simple as webSimple

class MainPage(webapp.RequestHandler):
  def post(self):
    twitter_user = self.request.get('twitter_user').encode('utf-8', 'replace')
    template_valuse = {
        'twitter_user' : twitter_user,
        'statuses' : []
    }
    try:
      twitter_url = "http://twitter.com/statuses/user_timeline/%s.xml" % twitter_user
      twitter = webSimple({ 'base_url': twitter_url })
  
      yahoo = webSimple({
        'base_url': 'http://api.jlp.yahoo.co.jp/MAService/V1/parse',
        'param' : { 'appid' : 'xxxxxxxx', 'results' : 'ma', },
      })
  
      xml = twitter.get().content
      r = re.compile(r'(\&#\d+;)')
      for st in BeautifulSoup(xml)('status'):
        name = r.sub(lambda x: unichr(int(x.group(1)[2:-1])), st.user.screen_name.string).encode('utf-8', 'replace')
        msg = r.sub(lambda x : unichr(int(x.group(1)[2:-1])), st.text.string).encode('utf-8', 'replace')
        xml = yahoo.get({ 'sentence': msg }).content
        words = []
        for word in BeautifulSoup(xml)('word'):
          words.append({
            'reading' : word.reading.string.encode('utf-8', 'replace'),
            'pos' : word.pos.string.encode('utf-8', 'replace'),
            'surface' : word.surface.string.encode('utf-8', 'replace'),
          })
        template_valuse['statuses'].append({
           'screen_name' : name,
           'words' : words,
        })
    except Exception, e:
      template_valuse['error'] = e
      pass
    path = os.path.join(os.path.dirname(__file__), 'yomifutter.html')
    self.response.out.write(template.render(path, template_valuse))

  def get(self):
    path = os.path.join(os.path.dirname(__file__), 'yomifutter.html')
    self.response.out.write(template.render(path, {}))

def main():
  application = webapp.WSGIApplication([('/yomifutter/', MainPage)], debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
  main()
適当なコードで申し訳ない...汗


Google App Engine上でweb.pyとかCherryPyとか動かないという報告がいくらかあったのでまとめてみます。
Google App Engineでは、以下で述べられているように「WSGIに対応したCGI(と呼ばれるもの)であれば、フレームワークとして扱えるよ」と言っています。
Using the webapp Framework - Google App Engine - Google Code

The CGI standard is simple, but it would be cumbersome to write all of the code that uses it by hand. Web application frameworks handle these details for you, so you can focus your development efforts on your application's features. Google App Engine supports any framework written in pure Python that speaks CGI (and any WSGI-compliant framework using a CGI adaptor), including Django, CherryPy, Pylons, and web.py. You can bundle a framework of your choosing with your application code by copying its code into your application directory.

http://code.google.com/appengine/docs/gettingstarted/usingwebapp.html
今日はこの動かないと言われている部分を解決して頂ける(かもしれない)ポイントをご紹介。

web.py

まずweb.pyですが、オフィシャルがtarballで配布している物(web.py-0.23.tar.gz)では動きません。最新はhttp://webpy.org/bzr/webpy.dev/で配布されており、bzrを使って取得する必要があります(必要であればhttp://bazaar-vcs.org/Downloadからbzrを取得して下さい)。
bzr get http://webpy.org/bzr/webpy.dev/
これまでの1スクリプトからソケットサーバまで起動するweb.pyのコーディング習慣ではそのままでは動きません。flupを使ってWSGIモジュールを動かす必要があります。以下の様にWSGIハンドラとしてアプリケーションを作成しwsgiref.handlersに起動させます。mod_pythonの場合と同じですかね。
ソースコードの中を覗いた所、Google App Engineに対応するコードが入っています。以前までは一般的なCGIとして「print "Hello World!"」してしまえばそのまま出力されていましたが方式が変った様で、GET等はreturnで文字列として返す様になっています。
動くコードとしては以下の様になります。
#!-*- coding:utf-8 -*-
import web

urls = (
  '/hello/(.*)', 'hello'
)

class hello:
  def GET(self, name):
    web.header("Content-Type", "text/html; charset=utf-8")
    return "Hello World!"

if __name__ == "__main__":
  web.application(urls, globals()).cgirun()
但し、「web.pyが内部でimportしているopenid.consumerが無いよ!」と怒られるのでpython-openidをいっそ入れてしまうか、以下のパッチを当てる必要があります。
--- web/__init__.py.orig    Wed Apr 23 19:12:33 2008
+++ web/__init__.py Wed Apr 23 19:12:35 2008
@@ -26,7 +26,7 @@
 from httpserver import *
 from debugerror import *
 from application import *
-import webopenid as openid
+#import webopenid as openid
 
 try:
     import cheetah
まだ開発版の様ですから、今後に期待したいです。

CherryPy

次にCherryPyですが少し小細工が必要です。Google App EngineではPure Pythonで無いものは動かないのですがCherryPyに含まれるWSGIServer(実際にはSSL機能)がsocket._fileobjectを使ってしまっていてモジュールのインポートに失敗します。以下の様にしてSSL_fileobjectを殺してやる必要があります。
--- cherrypy/wsgiserver/__init__.py.orig    Sun Jan 13 17:56:50 2008
+++ cherrypy/wsgiserver/__init__.py Wed Apr 23 16:47:34 2008
@@ -57,11 +57,12 @@
 from urllib import unquote
 from urlparse import urlparse
 
-try:
-    from OpenSSL import SSL
-    from OpenSSL import crypto
-except ImportError:
-    SSL = None
+#try:
+#    from OpenSSL import SSL
+#    from OpenSSL import crypto
+#except ImportError:
+#    SSL = None
+SSL = None
 
 import errno
 socket_errors_to_ignore = []
@@ -676,19 +677,19 @@
                 raise socket.timeout("timed out")
     return ssl_method_wrapper
 
-class SSL_fileobject(socket._fileobject):
-    """Faux file object attached to a socket object."""
-    
-    ssl_timeout = 3
-    ssl_retry = .01
-    
-    close = _ssl_wrap_method(socket._fileobject.close)
-    flush = _ssl_wrap_method(socket._fileobject.flush)
-    write = _ssl_wrap_method(socket._fileobject.write)
-    writelines = _ssl_wrap_method(socket._fileobject.writelines)
-    read = _ssl_wrap_method(socket._fileobject.read, is_reader=True)
-    readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True)
-    readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True)
+#class SSL_fileobject(socket._fileobject):
+#    """Faux file object attached to a socket object."""
+#    
+#    ssl_timeout = 3
+#    ssl_retry = .01
+#    
+#    close = _ssl_wrap_method(socket._fileobject.close)
+#    flush = _ssl_wrap_method(socket._fileobject.flush)
+#    write = _ssl_wrap_method(socket._fileobject.write)
+#    writelines = _ssl_wrap_method(socket._fileobject.writelines)
+#    read = _ssl_wrap_method(socket._fileobject.read, is_reader=True)
+#    readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True)
+#    readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True)
 
 
 class HTTPConnection(object):
あとはweb.py同様に
import cherrypy
import wsgiref.handlers

class OnePage(object):
  def index(self):
      return "one page!"
  index.exposed = True
 
class HelloWorld(object):
  onepage = OnePage()

  def index(self):
    return "hello world"
  index.exposed = True

def main():
  app = cherrypy.Application(HelloWorld(), "/helloworld")
  wsgiref.handlers.CGIHandler().run(app)

if __name__ == '__main__':
  main()
とすればGoogle App Engine上でも動きます。上の例にあるCherryPy独特の「/helloworld/onepage」も動きますよ!
これで幾らかのフレームワークが動くようになりました。幾らか敷居が低くなるのではないでしょうか。皆さんも色んなアプリケーションを作ってみませんか。
また時間が出来たら、残るPylonsも検証して見たいと思います。

最後に私が好きなCDを...
いけないチェリー・パイ いけないチェリー・パイ

Sony Music Direct / ¥ 1,391 (2004-07-22)
 
発送可能時間:通常1~4週間以内に発送



こんなのあるんだ...
freshmeat.net: Project details for pytumblr - ロックスターになりたい
pytumblr is a Python library for the tumblr.com API. freshmeat.net: Project details for pytumblr
Google App Engine そう言えば前に「Windowsのエクスプローラで「送る」からShareOnTumblr」なんてのも作ったなぁ。 pythonで作ってあってlinuxなんかでも動くように作ったはず。

でpytumblrですが、ソース見たら簡単なソースだったのでGoogle App Engineで動くように改造してみました。以下パッチ
--- pytumblr.py.orig    Thu Apr 24 03:15:26 2008
+++ pytumblr.py Thu Apr 24 12:02:32 2008
@@ -1,7 +1,9 @@
 #!/usr/bin/env python
+#!-*- coding:utf-8 -*-
 
-import string, httplib, urllib2, urllib
-from xml.dom import minidom
+import urllib2, urllib
+from google.appengine.api import urlfetch
+from BeautifulSoup import BeautifulSoup
 
 class pytumblr(object):
    """Tumblr API Object.
@@ -75,14 +77,11 @@
 
        data = urllib.urlencode(values)
        headers = {"Content-type": "application/x-www-form-urlencoded"}
-       conn = httplib.HTTPConnection(self.url)
-       conn.follow_all_redirects = True
-       conn.request("POST",'/api/write', data, headers)
-       response = conn.getresponse()
-       if ( int(response.status) == 201):
+       response = urlfetch.fetch("http://%s/api/write" % self.url, headers=headers, method='POST', payload=data)
+       if ( int(response.status_code) == 201):
            return 'Success'
-       elif( int(response.status) != 201):
-           raise 'Error - Status %s (%s) returned' %(response.status, response.reason)
+       elif( int(response.status_code) != 201):
+           raise 'Error - Status %s (%s) returned' %(response.status_code, 'something wrong')
 
    def auth(self):
        values = {
@@ -92,13 +91,10 @@
            }
        data = urllib.urlencode(values)
        headers = {"Content-type": "application/x-www-form-urlencoded"}
-       conn = httplib.HTTPConnection(self.url)
-       conn.follow_all_redirects = True
-       conn.request("POST", '/api/write', data, headers)
-       response = conn.getresponse()
-       if ( int(response.status) != 200 ):
-           return '< There was a tiny problem: %s (%s) >' %(response.status, response.reason)
-       if ( int(response.status) == 200 ):
+       response = urlfetch.fetch("http://%s/api/write" % self.url, headers=headers, method='POST', payload=data)
+       if ( int(response.status_code) != 200 ):
+           return '< There was a tiny problem: %s (%s) >' %(response.status_code, 'something wrong')
+       if ( int(response.status_code) == 200 ):
            return '< Authenticated! >'
    
 
@@ -112,25 +108,8 @@
            opts = opts + "?type=" + type
        if ( id != 'None' ):
            opts = opts + "?id=" + id
-       return urllib.urlopen('http://%s.tumblr.com/api/read%s' %(self.user, opts)).read()
+       return urlfetch.fetch('http://%s.tumblr.com/api/read%s' %(self.user, opts)).content
        
    def getblog(self):
-       rxml = minidom.parseString(self.blogread())
-       titles = rxml.getElementsByTagName('regular-title')
-       postid = rxml.getElementsByTagName('post')
-       i = 0
-       n = 0
-       posts = {}
-       while ( i < len(postid)):
-           if ( postid[i].attributes['type'].value == 'regular' ):
-               t = titles [n]
-               t = t.toxml()
-               t = t.replace('<regular-title)', '')
-               t = t.replace('</regular-title)', '')
-               poid = postid[i].attributes["id"].value
-               posts[ poid ] = t
-               n = n + 1
-           if ( postid[i].attributes['type'].value != 'regular' ):
-               pass
-           i = i + 1
-       return posts
+       soap = BeautifulSoup(self.blogread())
+       return soap('post')
思いっきり弄ってますね。。。
minidomの代わりにBeautifulSoupを、urllib2の代わりにurlfetchを使っています。したがってgetblogは自分のregular情報のdictだけを返すのではなくBeautifulSoupを使ってlink、photo、quoteを返す様にしてあります。
実際に動くよって所は以下のサイトで確認して下さい。
pytumblr
このサイトのURLの後ろに
http://mattn.appspot.com/tumblr/mattn
と言った感じにtumblrアカウント名を付けてみて下さい。
今回のデモにはweb.pyというフレームワークを使用してみました。
以下スクリプトソースです。
#!-*- coding:utf-8 -*-
import web
from pytumblr import pytumblr
from BeautifulSoup import BeautifulSoup

def unescape(str):
  return  BeautifulSoup(str, convertEntities=BeautifulSoup.HTML_ENTITIES).contents[0].encode('utf-8', 'replace')

urls = (
  '/tumblr/(.*)', 'tumblr'
)
render = web.template.render('templates/')

class tumblr:
  def GET(self, name):
    web.header("Content-Type", "text/html; charset=utf-8")
    template_values = { 'name': '', 'posts': [] }
    name = name.replace('/', '')
    if name:
      pt = pytumblr(name, None, None)
      template_values['name'] = name
      for blog in pt.getblog():
        if blog['type'] == 'link':
          template_values['posts'].append({
            'type': 'link',
            'text': unescape(blog('link-text')[0].string),
            'desc': blog('link-description') and unescape(blog('link-description')[0].string) or '',
            'link': unescape(blog('link-url')[0].string),
          })
        if blog['type'] == 'photo':
          template_values['posts'].append({
            'type': 'photo',
            'text': unescape(blog('photo-caption')[0].string),
            'link': unescape(blog('photo-url')[0].string),
          })
        if blog['type'] == 'quote':
          template_values['posts'].append({
            'type': 'quote',
            'text': unescape(blog('quote-text')[0].string),
            'link': unescape(blog('quote-source')[0].string),
          })
    return render.tumblr(template_values)

if __name__ == "__main__":
  web.application(urls, globals()).cgirun()
案外短く書けますね。そしてテンプレートHTML
$def with (res)
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/static/css/tumblr.css" type="text/css" />
$if res['name']:
    <title>pytumblr - $res['name']</title>
$else:
    <title>pytumblr</title>
</head>
<body>
$if res['name']:
    <h1>pytumblr - $res['name']</h1>
$else:
    <h1>pytumblr</h1>
    <img src="http://b.hatena.ne.jp/entry/image/http://mattn.appspot.com/tumblr/" title="はてなブックマーク" />
    <div id="content">
$if res['name']:
    <h2>$res['name']'s tumblr</h2>
    $for post in res['posts']:
        $if post['type'] == 'photo':
            <b>PHOTO:</b>$:post['text']<br />
            <blockquote><img src="$post['link']" /></blockquote>
        $elif post['type'] == 'link':
            <b>LINK:</b><a href="$post['link']">$:post['text']</a><br />
            <blockquote class="link">$:post['desc']</blockquote>
        $elif post['type'] == 'quote':
            <b>QUOTE:</b><br />
            <blockquote class="quote">
                $:post['text']<br />
                <cite>$:post['link']</cite>
            </blockquote>
    </div>
    <hr clear="all" />
    <p style="text-align: center">provided by <a href="http://mattn.kaoriya.net">mattn</a>, hosted on google app server.</p>
</body>
</html>
このweb.pyのテンプレートって癖があってpythonインデント方式なのですが、これってpreとかcodeで先頭が入っちゃったら不味いんじゃないかと思ったり...
解決方あるのか、調べてみます。pytumblrで面白いもの作ってみて下さい。

みんなのPython みんなのPython
柴田 淳
ソフトバンククリエイティブ / (2006-08-22)
 
発送可能時間:



クァーってサービスが出来たみたいです。
Qaa(クァー):Twitterで簡単アンケート!(まだまだ開発中)
Qaa

Qaa(クァー)は、Twitterを利用して簡単にアンケートをとることができるサービスです。

Qaa(クァー)を通じて投稿されたアンケートはYes or No形式で回答する事ができ、その結果はアンケート毎に用意されるページでパイチャートで確認する事ができます。


http://qaa.orig.jp/h#about
twitterを使ったソーシャルアンケートサービスですね。
でもいちいちtwitterやIMから「@qaa qid14 YES」とか入力するの面倒臭いのでMinibufferコマンド作ってみました。
アンケートエントリページに行って、「q y」もしくは「q n」で「YES」もしくは「NO」の回答をtwitter経由で投稿します。
qaa_yes_or_no.user.js
よろしければどうぞ。
※一応、firefox3/greasemonkeyからBasic認証のダイアログが出ない件は対応してあります。

2008/04/21


なるべくdictぽく扱えるように作ってみました。
WedataオブジェクトのコンストラクタにAPIKEYを掘り込んで操作します。
api = Wedata('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
databases = api.databases()
for database in databases:
  print database
  for key in database.keys():
    print " %s=%s" % (key, database[key])
  print
print
また、AutoPagerizeのデータベースであれば以下の様にdataプロパティからの属性参照の様に書く事も出来ます。
database = api.database('AutoPagerize')
print "%s : %s" % (database.name, database.description)
items = database.items()
for item in database.items():
  print " %s" % item.data.pageElement
  print " %s" % item.data.insertBefore
  print
Database.create_databaseの戻り値にはデータベース名(キー名)、Item.create_itemの戻り値にはアイテムIDが戻ります。このIDを使ってDatabase.delete_datebaseおよびItem.dalete_itemを呼び出す事が出来ます。
dbid = api.create_database('my_example_database', 'my_example_database', ['name', 'description'], ['value', 'xpath'], False)
api.delete_database(dbid)
今のところGoogle App Engineには対応していません。Google App Engine上での用途を見付けたら対応するかもしれません。
いつものようにコードはcodereposに置いてあります。
/lang/python/wedata

2008/04/18


世界のナベアツにはてなブックマークで挑戦する - ネットランダム

はてなブックマークのコメントで、1から40まで数えて、3の倍数 と3がつく数字の時だけアホになります。

それではどうぞ!

http://d.hatena.ne.jp/fk_2000/20080418/p2
なんと!ソフトウェアでやるべきネタを人力で!!!

むむむ...こうなればこれをまたソフトウェアで表現するべき!
fk_2000氏と愉快な仲間達がお届けする「世界のナベアツ」

ちなみにコードはこんなの
#!-*- coding:utf-8 -*-
import os
from google.appengine.ext.webapp import template
from google.appengine.api import urlfetch
from BeautifulSoup import BeautifulSoup
import logging

try:
  res = urlfetch.fetch('http://b.hatena.ne.jp/entry/rss/http://d.hatena.ne.jp/fk_2000/20080418/p2')
  print "Content-Type: text/html; charset=UTF-8"
  print ""
  items = []
  for item in BeautifulSoup(res.content)('item'):
    logging.info(item.title)
    logging.info(item.title.string)
    items.insert(0, {
      'title' : item.title.string,
      'description' : item.description.string,
    })
  template_values = {
    'items': items
  }
  path = os.path.join(os.path.dirname(__file__), 'nabeatzz_fk_2000.html')
  print template.render(path, template_values)
except Exception, e:
  print "Status: 400"
  print ""
  print e


reblogぽいですが、私は「We Heart It」の方が好きかな。画像だけに絞っているのがカッコイイ
Minibufferからキー「h i」だけでポスト出来るグリモン書いた。オリジナルのブックマークの動作に従い、250x100以上の画像をポストします。ブックマークレットの場合には「Heart It」のリンクが付きますが、このグリモンではLDRizeのカレントノードに含まれるサイズ250x100以上の画像(PNG/JPG/GIF)をweheartit.comにポストします。
あと、コマンド好きな人は
:pinned-or-current-node | weheartit | clear-pin
とかやっとけ!

さっきからtumblr dashboardで使っているのですが便利すぎる!
weheartit.user.js
よかったら、もってけ!

tomblooのコミット権貰ったので気が向いたら...

2008/04/15


出来た。とりあえずの機能としては
  • デザインテンプレート
  • プラグイン機能
  • 検索(プラグイン実装)
  • ページング(プラグイン実装)
  • 編集(プラグイン実装)
  • コメント/トラックバック(プラグイン実装)
まで出来た。なるべくblosxomぽくなるように作ったつもり。動いてる物は
Weblog
にあります。適当にmicroformatに対応してあります。
google-app-engine-blog
この後codereposに上げる予定です。
でもどのディレクトリに上げていいか分かりません><

追記
http://coderepos.org/share/browser/websites/appspot.com/blog/trunk
にアップした。

2008/04/10


pythonにはxmlrpclibがあり、常駐型のSimpleXMLRPCServerやCGIから使えるCGIXMLRPCRequestHandlerというとても有用なモジュールが存在します。
たとえば
def plus(num1, num2):
    return num1 + num2
という関数をXMLRPCサーバから公開したい場合