CGI::Application::HTTP
以前作ったCGIAppベースのApplicationをStandAloneで動かすCGI::Application::HTTPを
http://nomadscafe.jp/archives/CGI-Application-HTTP-0.01.tar.gz
に置いておいてみた。
« 2005年12月 | メイン | 2006年02月 »
以前作ったCGIAppベースのApplicationをStandAloneで動かすCGI::Application::HTTPを
http://nomadscafe.jp/archives/CGI-Application-HTTP-0.01.tar.gz
に置いておいてみた。
Catalystのコンポーネントの読み込みのコード(setup_components)ってけっこう凄くないっすか?
eval "package $class;\n" . q!Module::Pluggable::Fast->import( name => '_catalyst_components', search => [ "$class\::Controller", "$class\::C", "$class\::Model", "$class\::M", "$class\::View", "$class\::V" ], callback => $callback ); !;
eval 式;をつかって、現在のpackage宣言しつつ、Module::Pluggable::Fastを動かす。
tricky杉。
CatalystをデバッグOnで起動したときに最初の方にでてくる
.-------------------------------------------------------------------+----------. | Class | Type | +-------------------------------------------------------------------+----------+ | Traba::Model::DBISweet | class | | Traba::Model::Trackbacks | class | | Traba::Model::URLBL | instance | | Traba::View::RSS | instance | | Traba::View::TT | instance | '-------------------------------------------------------------------+----------'
これの右側のTypeがあるんだけど、この欄のclassかinstanceになるかは、Catalystの起動時のsetup_componentsで判定される。
判定に使われるのは、COMPONENTというクラスメソッド。ちなみに、上は暦トラバのもの。
Catalyst::Componentにはそれが実装されてる。中身的には、configをまとめてコンストラクタのnewを呼び出すというコード。
sub COMPONENT { my ( $self, $c ) = @_; # Temporary fix, some components does not pass context to constructor my $arguments = ( ref( $_[-1] ) eq 'HASH' ) ? $_[-1] : {}; if ( my $new = $self->NEXT::COMPONENT( $c, $arguments ) ) { return $new; } else { if ( my $new = $self->new( $c, $arguments ) ) { return $new; } else { my $class = ref $self || $self; my $new = { %{ $self->config }, %{$arguments} }; return bless $new, $class; } } }
ということで、Catalyst::ComponentをベースとするModel/Viewは(おかしい事をしないかぎり)必ずinstanceになる。
上のリストにある、Traba::Model::URLBLはTrackbackスパムのチェックをするクラスで、id:miyagawaのKwiki::URLBLを参考にして、以下のような感じで実装されている。
package Traba::Model::URLBL; use strict; use warnings; use base 'Catalyst::Model'; use NEXT; use Net::DNS::Resolver; use URI; sub new { my($class, $c, $config) = @_; my $self = $class->NEXT::new($c,$config); $config->{urlbl_dns} ||= [qw/sc.surbl.org bsb.spamlookup.net rbl.bulkfeeds.jp/]; $self->config($config); return $self; } sub is_blocked { my($self, $url) = @_; my $uri = URI->new($url); my $domain = $uri->host; $domain =~ s/^www\.//; my $res = Net::DNS::Resolver->new; for my $dns (@{$self->config->{urlbl_dns}}){ my $q = $res->search("$domain.$dns"); return 1 if $q && $q->answer; } return; } 1;
Catalyst 5.61からmodelなどのcompornentを呼び出すところに、ACCEPT_CONTEXTなるものがある。
compornentの呼び出しはいつからか、
$c->model('MyModel')->foo; $c->view('MyView')->bar;
って方法が使えるようになってる。
現在開発中のコードでは、
my $comp = $c->components->{$try}; if ( eval { $comp->can('ACCEPT_CONTEXT'); } ) { return $comp->ACCEPT_CONTEXT($c); } else { return $comp }
というようになっていて、インスタンスでもクラスでも目的のmethodを呼び出す前にACCEPT_CONTEXTがあれば、ACCEPT_CONTEXTを引数$cで呼び出してくれる。このcontextオブジェクトをクラス側で保持しておけば、Catalyst::Compornentをベースとしていないクラスでも$cが使えるいうわけっす。
ってことで、Class::DBIのプラグインを簡単につくってみた。ただし現在のCatalystのバージョンでは動かない。
package Class::DBI::Plugin::AcceptCatalyst; use strict; use warnings; our $VERSION = '0.01'; sub import { my $caller = caller(); no strict 'refs'; $caller->mk_classdata('catalyst_context'); *{"$caller\::ACCEPT_CONTEXT"} = sub { my ($class, $c) = @_; $class->catalyst_context($c); return $class; }; }
これをロードしておけば、Class::DBIベースのクラスの中でCatalystのContextが使える、ハズ。before_hoge、after_hogeとかのTriggerでこれを利用してLogを取るなんてこともできるんじゃないかな。
ただ、このコードだとメモリリークする可能性があるので、もう一工夫いるかな。
YAML::SyckにConfigが変わるらしい5.64はいつになるだろう。
GoodPic.comでMac OSX でファイルブラウジングを快適に。DocにHDDを追加と紹介されています。関連したTipsのようなもの。
Tigerに対応してくれないIttecを会社Macでは使っているのですが、Ittec項目(Library下)というところにアプリケーションへのエイリアス等をいれてランチャー代わりにつかえます。僕は、そこにApplicationsのエイリアスもいれてます。
ショートカットを整頓してなくても、全部のアプリケーションへアクセスできて便利です。
FinderPopでできるかどうかは試してないです。
ブログ人マップとかlivedoor MAPなど、地図にTrackbackを送ることができるサービスはいくつかあります。
WhereがあるならWhenもいけるんじゃないかと思って、カレンダーにTrackbackを送信することができるサービスを作ってみました。
暦トラバ(beta)
http://traba.nomadscafe.jp/
カレンダー・トラックバックセンターとでも言えばいいでしょうか。カレンダーの日付に対してTrackbackできます。受信したTrackbackは一覧表示されてRSSフィードなども配信してます。
イベント情報とか集まったらモノになると思います。
さらにMAPと組み合わせられたら面白いかなぁというのも考え中。
CarbonEmacsでmac-key-mode使ったら駄目ですかね。
Command-c Command-v、Command-↓での文末移動とかの癖が強い。
mac-key-modeをいれると、Command-xがM-xにならないのがあれだ。
textmateはフォルダを俯瞰できるのがいい。
emacsは日本語が問題ないのがいいですね。
Text::Hatenaがバージョンアップで0.07。
お願いしていたHTML::Parserのprereqのバージョンが下がったのと、Text::Hatena::AutoLinkが増えて、自動リンクに対応してる。
[http://www.hatena.ne.jp/:title] mailto:someone@example.com asin:4798110523 [tex:x^2+y^2=z^2] d:id:kazeburo
みたいなリンクも動く(らしい)
モジュール内部で、Net::AmazonやURI::Titleを使って情報を取りに行ったり、Texの場合ははてなの画像生成URIにリンクしていたりと、サーバのリソース(はてなのリソースも)を結構食います。Femoだと表示都度で変換しているんだけど、そのままでは導入が難しいなぁ。
だけど、このあたりをキャッシュする等をすればそのままはてなができそうな具合になってきてます。
そうそう、AWSのtokenとかアフィリエイトIDがモジュールにそのまま書かれているんだけど、使う時はもちろんOverrideしていいんですよね(w
でもこれは下記のように書くことが出来ます。
sub func {
my %args = @_;
my $hoge = $args{hoge} || 'fuga';
# なんか処理
}
なんとこれを知っているだけで3行もコードが節約出来るんです。
とあって、短く書けてエレガントです。
これだけでも97%ぐらいはかまわないのですが、使い続けるとはまりどころがあります。
&func(hoge=>0); &func(hoge=>"");
として、$hogeに0や""を入れたい場合がそれ。
なぜなら、0や""がブール値コンテキストにおいて、偽となるからです。
Perlにおける真偽の規則は、らくだ本から引用ですが、
1. ""と"0"を除き、すべての文字列は真である
2. 0を除き、すべての数値は真である
3. すべてのリファレンスは真である
4. すべての未定義値は偽である
となってます。なので、$hogeに""や0を入れたくて、&func(hoge=>0);としたとしても
my $hoge = $args{hoge} || 'fuga';
において、$args{hoge}が偽と判断され、'fuga'になってしまいます。
これに対応するならば、definedを使って
my $hoge = (defined $args{hoge}) ? $args{hoge} : 'fuga';
などとすると、希望通りに動くようになります。
$args{hoge}が2回あるので、ちょっとかっこわるいですがね。
↓最近ようやく買ったのですが、やはり勉強になります。
それとかっこいいコードを書くなら、id:naoya氏監修の
がおすすめ。
いまさら気付いたんだけど、XML::FeedってAtom
1.0に対応していないのね。。
FeedとEntryのissuedとmodifiedが、Atom
1.0ではpublishedとupdatedになっているので、Atomのバージョンを見ながら参照先を変えるとかが必要になると思われ。
feed.nomadscafe.jpの方では、無理矢理対応した。カテゴリーへの対応だけがまだできてない。
Modelクラスの部分を↓だんだんBKの固まりになってきそう。
use XML::Atom::Util qw( iso2dt ); sub process{ my ( $self, $c ) = @_; my %ret; local $XML::Feed::RSS::PREFERRED_PARSER = "XML::RSS::LibXML"; my $feed = XML::Feed->parse(URI->new($c->stash->{uri})); if(!$feed){ $c->stash->{errstr} = XML::Feed->errstr; return } my $pf = DateTime::Format::Mail->new(); my $is_atom10 = ($feed->{atom} && $feed->{atom}->version > 0.3) ? 1 : 0; my %feed; $feed{title} = $feed->title; $feed{link} = $feed->link; $feed{description} = ($is_atom10) ? $feed->{atom}->subtitle : $feed->description; $feed{author} = $feed->author; $feed{language} = $feed->language; $feed{copyright} = ($is_atom10) ? $feed->{atom}->{rights} : $feed->copyright; if($is_atom10){ $feed{modified} = $feed->{atom}->updated ? $pf->format_datetime(iso2dt($feed->{atom}->updated)) : undef; }else{ $feed{modified} = $feed->modified ? $pf->format_datetime($feed->modified) : undef; } $feed{generator} = (ref $feed->generator) ? $feed->generator->{resource} : $feed->generator; foreach (qw(title link description author language copyright modified generator)){ utf8::decode($feed{$_}) unless utf8::is_utf8($feed{$_}); } $feed{entries}=[]; for my $entry ($feed->entries) { my %entry; $entry{title} = $entry->title; $entry{link} = $entry->link; $entry{summary} = ($entry->summary) ? $entry->summary->body : undef; $entry{category} = $entry->category; $entry{author} = $entry->author; #issued,modified if($is_atom10){ $entry{issued} = $entry->{entry}->published ? $pf->format_datetime(iso2dt($entry->{entry}->published)) : undef; $entry{modified} = $entry->{entry}->updated ? $pf->format_datetime(iso2dt($entry->{entry}->updated)) : undef; }else{ $entry{issued} = $entry->issued ? $pf->format_datetime($entry->issued) : undef; $entry{modified} = $entry->modified ? $pf->format_datetime($entry->modified) : undef; } foreach (qw(title link summary category author issued modified)){ utf8::decode($entry{$_}) unless utf8::is_utf8($entry{$_}); } push(@{$feed{entries}},\%entry); } $ret{$c->stash->{uri}} = \%feed; $c->stash->{feed} = \%ret; }
feed.nomadscafe.jpもバージョンあがって、0.04。ソースは
http://nomadscafe.jp/archives/
においてあります。
昨日エントリしたFeed2JSONのfeed.nomadscafe.jpのソースコードを
http://nomadscafe.jp/archives/
に置いておきました。
DL&展開して、できたディレクトリに入って、
./script/feeder_server.pl -r
と打って内蔵サーバがport 3000で起動できます。
以下なモジュールが入っていることが必要です。
ライセンスはPerlと同じで。
Feed2JSONをつくってみた。
Feed2JSONは、RSS/AtomをJSONに変換するようなサービスです。rss2jsonの方が一般的。
Catalystで動いていて、指定されたURIをXML::Feedで取得、Catalyst::View::JSONにjson_driver=>'JSON::Syck'でforwardしているだけ。
このBlog AtomのJSONを取得するには
http://feed.nomadscafe.jp/json/https://blog.nomadscafe.jp/atom.xml.js
となる。最後の.jsはIE対策
ついでにYAMLでも出力できる。こちらはYAML::Syckを利用
http://feed.nomadscafe.jp/yaml/https://blog.nomadscafe.jp/atom.xml
なにかJavaScriptのDemoめいたものを作るときに役に立つかも。
さきほど、JSON::Syck
0.04がでてましたが、
JSON::Syckで一部気になるところが。
use JSON::Syck; my $ref ={foo=>undef}; print JSON::Syck::Dump($ref);
こんな感じにしたときに、出力されるのは、
{"foo":}
になります。これだと、JavaScriptのエラーになることがありますね。
できれば、JSON.pmと同じく
{"foo":null}
と、出てほしいところ。
JSON::Syckのコードとか全然読めない。勉強不足だなぁ。
Mac OS X 10.4.4に含まれるSafari
2.0.3でFirefoxと同じく、setSelectionRange、selectionStart、selectionEndがサポートされたようです。
<input type="text" id="textfield" name="textfield" value="foo"/> <script type="text/javascript"> var ele = document.getElementById('textfield'); for(i in ele){ document.write(i + " = " + ele[i] + "<br />"); } </script>
とした、結果の中に
setSelectionRange = [function] selectionStart = 3 selectionEnd = 3
と入ってます。
見ていると、contentEditableなんていうのもあって、
<div contenteditable="true"> この文章はブラウザ上で編集できます。 </div>
というのも動く。編集ができちゃう。
↓お試し用
前から動いたっけ??
昨日のMacのアップデートでSafariが2.0.3(もしくは1.3.2)になったわけだが、ちょっと気になった点が。
<input type="text" name="textfield" onfocus="this.value=this.value+'foo'"/>
というtextboxを作って、textboxをクリックすると、textboxに「foo」と入ります。
一度フォーカスを外して再度クリックすると「foofoo」になると思います。繰り返すと「foo」が増えていきます。
このときのキャレット(文字入力ポインタ)の位置が、firefoxやいままでのSafariなら、必ず最後に来ていたのですが、Safari 2.0.3(1.3.2)だと、Textbox上のクリックしたあたりにキャレットがつきます。
これ、何が問題なのかというと、「Tag入力Suggestテスト」のエントリで書いた調整中の
FemoのTag Suggest Test (http://autocomptest.nomadscafe.jp/)
でTagを候補の中から決定したときにキャレット位置が最後にいかないところにあります。
これでは、次のTagを入力しようとするときに、キャレットを矢印キーやマウスで動かさないとだめです。
う〜ん、不便。
OperaもこのSafariと同じ仕様ぽい。
追記:
del.icio.usのsuggestがうまく動くのでしらべると、どうもSafariでもsetSelectionRangeがうまく動くようだ。
if (this.tagText.setSelectionRange){ //this.tagTextはtextboxエレメント this.tagText.setSelectionRange(this.tagText.value.length,this.tagText.value.length); }
でキャレットを最後に持っていける。
サーバサイドからJSONを吐き出すときのContent-Typeなのですが、各ブラウザによって対応がちょっと違います。
下の表にまとめてみました。
×のところはeval中にエラーがでます。
Content-type | WinIE | Firefox | Safari | Opera(8.5) |
text/javascript | ○ | ○ | △ | × |
text/javascript; charset=utf-8 | ○ | ○ | ○ | × |
text/javascript; charset=utf8(utf-8の間違い) | × | ○ | ○ | × |
text/javascript+json | ○ | ○ | △ | × |
text/javascript+json; charset=utf-8 | ○ | ○ | ○ | × |
text/html; charset=utf-8 | ○ |
Safariでマルチバイトな文字を含む場合は、「charset=utf-8」があったほうがいいです。
Opera(8.5)はtext/javascriptだと根本的にだめみたいです。
Catalyst::View::JSONは「text/javascript+json」とかなっているんだけどどうするのが良いのでしょう。
さっきまで、間違えて「utf8」って書いてしまっていたんだけど、IE以外はうまく認識してくれますね。
確認は下のようなコードで行いました。prototype.jsのAjax.Requestをつかってます。
new Ajax.Request( "/foo",{ method: "get", parameters: params, onComplete: function(originalRequest){ try{ eval("var ret="+originalRequest.responseText); }catch(e){alert(e);} } });
id:naoya氏がclosureのpracticeをいくつか揚げていて非常に勉強になります。
ちょっと思いだしたのは、Text::Hatenaに含まれるText::Hatena::HTMLFilterで、HTML::Parserを
$self->{parser} = HTML::Parser->new( api_version => 3, handlers => { start => [$self->starthandler, 'tagname, attr, text'], end => [$self->endhandler, 'tagname, text'], text => [$self->texthandler, 'text'], comment => [$self->commenthandler, 'text'], }, );
こんな感じで呼び出しています。
各handlerがどうなっているのかと言えば、
sub texthandler { my $self = shift; return sub { my $text = shift; $text = &{$self->{context}->texthandler}($text, $self->{context}); $self->{html} .= $text; } }
のようになっていて、「$self」を保持しつつ無名サブルーチンを返しています。
実はこの方法を最近まで知らなくて、クロージャも理解してなく、HTML::Parserを継承してパーサクラスを書いてますた。ようやくきちんと理解できた気がする。
ところで、この連休はJavaScript(prototype.js有り)をずっと書いていたおかげで、
sub parser{ $self->{parser} = HTML::Parser->new( api_version => 3, handlers => { start => [$self->starthandler->bind($self), 'tagname, attr, text'], end => [$self->endhandler->bind($self), 'tagname, text'], text => [$self->texthandler->bind($self), 'text'], comment => [$self->commenthandler->bind($self), 'text'], }, ); } sub texthandler { my $self = shift; my $text = shift; $text = &{$self->{context}->texthandler}($text, $self->{context}); $self->{html} .= $text; }
と書けたらいいかもとか少し思った。
Form.Element.Observerはprototype.jsが提供する機能で、指定した間隔でフィールドをチェックして内容に変化があればcallbackを実行してくれるものです。
一つ前のエントリーで書いた「Tag入力Suggestテスト」でTagの入力の捕捉に利用してます。
Form.Element.Observerでちょっと気になるのは、指定した間隔でフィールドをチェックするTimerの停止ができないところです。Femoだとフォームが表示されたり消えたりとするので、timerを停止できたほうがCPUやメモリーにやさしいと思われ。
Timerのセットは、Form.Element.Observerの上位クラスであるAbstract.TimedObserverで
registerCallback: function() { setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); },
こんなように書かれています。intervalを呼び出す機能しかありません。
なので、この辺りをすこし書き加えて、確認対象のフィールドがなくなったらclearintervalされるようにしました。
Form.Element.Observer.prototype.registerCallback=function(){ this.interval = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); }; Form.Element.Observer.prototype.clearTimerEvent=function(){ clearInterval(this.interval); }; Form.Element.Observer.prototype.onTimerEvent=function(){ try{ var node = this.element.parentNode.tagName; }catch(e){ this.clearTimerEvent(); } var value = this.getValue(); if (this.lastValue != value) { this.callback(this.element, value); this.lastValue = value; } };
と、してみました。
確認ですが、
<div id="text1div"> Text:<input type="text" name="text1" id="text1" size="50"> </div> <p><a href="JavaScript:void(0);" onClick="$('text1div').innerHTML='deleted'">消す</a></p> <script type="text/javascript"> new Form.Element.Observer($('text1'),1,function(){alert('foo')}); </script>
というHTMLを用意して、ブラウザで表示。「消す」をクリック。
この状態で、
var node = this.element.parentNode.tagName;
がエラーとなってclearintervalが行われました。
動いたんだけど、
try{ var node = this.element.parentNode.tagName; }catch(e){ this.clearTimerEvent(); }
これが微妙ですよね。
何かいい方法ないかなぁ。
Femoに入れようかと思っているTag入力の補助機能のテストをしてます。
FemoのTag Suggest Test
http://autocomptest.nomadscafe.jp/
入力補助自体はよくあるものと同じで、Tagの途中まで入力すると(サーバとの通信間隔があるので1秒ほどかかりますが)候補がでてきて上下キーで選択できます。
Safariでも動きます。ただしWindowsのIEでは確認をしていません。
Tagの入力は、半角(全角でもOK)スペースで区切る方法で、Tagの中にスペースが入る場合は、「"」などで囲えば入力可能になってます。
現在の仕組みでは入力したTagのパースをサーバ側で行ってます。FastCGIで動いているのでそれなりにさくさくと使えると思います。JavaScriptの正規表現だと厳しい。
何か気付いた点などご意見いただけたらうれしいっす。
どうやらWinIEで動かないらしい。現在原因究明中です。
WinIEでも動いた。Opera(8.5)は微妙です。
CGI::ApplicationのMLでCGI::Application::Dispatchのv2.00_01がでてます。
svnで取得できます。
svn://svn.cromedome.net/CGI-Application-Dispatch
新しいdispatch tableがいい感じ。
今までは、PATH_INFOが
/Module/Runmode
で固定でしたが、これをいじる事ができます。
dispatchのカスタマイズをするにはCGI::Application::Dispatchを継承してdispatch_argsをoverrideします。
package MyApp::Dispatch; use base 'CGI::Application::Dispatch'; sub dispatch_args { return { prefix => 'MyApp', table => [ '' => { app => 'Welcome', rm => 'start' }, :app/:rm' => { }, 'admin/:app/:rm' => { prefix => 'MyApp::Admin' }, ], }; } package main; MyApp::Dispatch->dispatch;
tableの中身は
PATH_INFO => Local設定のhashref
の配列です。PATH_INFO中で
:app = Module
:rm = Runmode
:foo = $self->param('foo')で取得可能
になります。Local設定では、prefixやModule、Runmodeを指定可能っす。
これは結構便利なんじゃないでしょうか。
miyagawaさんのCatalyst::View::JSONを試し中。
sub foo : Local { my ($self,$c) = @_; $c->stash->{tags}=[qw/foo bar baz/]; $c->forward('View::JSON'); }
として、prototype.jsのAjax.Requestなどで、
new Ajax.Request( "/foo",{ onComplete: function(originalRequest){ var ret = eval(originalRequest.responseText); alert(Object.inspect(ret['tags'])); } });
こうすると、きちんと
['foo','bar','baz']
と得られるはず。Object.inspectもData::Dumperみたいで便利。
日本語対策というかSafari対策としては、
$c->res->content_type('text/javascript+json; charset=utf-8');
とするのがいいと思う。
Femoで全角スペースでのタグの切り分けをサポートしました。
全角 スペース
というように全角で区切っても 全角、 スペース の2つのタグになります。
もちろん今まで通り半角スペースでも動きますです。
FemoはTagがつけられるメモ帳Webアプリケーションです。ぜひお試しください。ご意見もくださいませ。
ちなみに、タグを切り分ける部分のPerlのコードは以下のようになってます。
my @tags; my $tagtext = $self->tagtext; utf8::decode($tagtext) unless utf8::is_utf8($tagtext); my %seen; while ($tagtext =~ /\G [\p{Zs}\t\r\n\f,]* (?: (") ([^"]*) (?: " | $) | (') ([^']*) (?: ' | $) | ([^\p{Zs}\t\r\n\f,]+) )/gx){ my $tag = $+; my $is_quoted = $1 || $3; next unless length $tag; $tag =~ s/^[\ \t\r\n\f]+//; $tag =~ s/[\ \t\r\n\f]+$//; $tag =~ s/[\ \t\r\n\f]+/ /g; utf8::encode($tag) if utf8::is_utf8($tag); if(my $ds = Date::Simple->new($tag)){ $tag = $ds->as_iso; } push @tags,$tag unless $seen{$tag}++; } return \@tags;
いままでText::Tagsというモジュールを使ってきましたが、今回のアップデートから独自の実装(かなりそのままコピー&ペーストですが)になりました。全角でもタグがsplitされるようになった以外はText::Tagsと互換性があります。
Femoで全角スペースでもTagのsplitができるように、と調べているのですが、
use utf8;
をしている場合、\s
は全角スペースにもmatchするようです。初めて気がついた。
#!/usr/bin/perl use strict; use warnings; use utf8; binmode STDOUT => ":utf8"; my $str ="全角 ス ペ ー ス が入った テキ\tス\nト"; print join ",",split /\s/,$str;
の出力は
全角,ス,ペ,ー,ス,が入った,テキ,ス,ト
となります。
perlretut - Perl
の正規表現のチュートリアルによると、これも知らなかったのですが、
\sは空白キャラクタで [\ \t\r\n\f]を表します
なのですね。
utf8の場合は、ここに書かれているutf8のGeneral
Category Valuesで表現すると
\s = [\p{Zs}\t\r\n\f]
なのかなぁ、、、難しいなぁ。