Rookパッケージがすごい!Rだけで1分で0からウェブアプリ

要R (≥ 2.13.0)です。では,おもむろに以下のスクリプトを実行してください。

install.packages("Rook")
s <- Rhttpd$new()
s$start(quiet=TRUE)
s$browse(1)

ブラウザが立ち上がり,テスト用のウェブアプリが表示されました。たったこれだけで!

なぜこんなことができるかというと,Rは実はウェブサーバーを内蔵していて,Rookパッケージはこれを使ってウェブアプリを稼働させています

Hello, World!

ではお約束にとりかかりましょう。

app.hw <- function(env){
    res <- Rook::Response$new()
    res$write("<html>\n<head><title>Test</title></head>\n<body>\n")
    res$write("<h1>Hello, World!</h1>\n")
    res$write("</body>\n</html>\n")
    res$finish()
}
s$add(app=app.hw, name="HelloWorld")
s$browse("HelloWorld")

これだけです!ほとんど説明は要らないぐらい簡単ですが,まず,最初のスクリプトで生成したsというオブジェクトは,Rhttpdオブジェクトで,ウェブアプリとサーバーの橋渡しをします。Rhttpdオブジェクトに関数の形で作ったウェブアプリを渡します。RhttpdオブジェクトはR5クラスなので,addというメソッドを使ってs$add(app=ウェブアプリの関数, name=ウェブアプリの名前)という形でウェブアプリを登録します。browseメソッドは単なるユーティリティ関数で,ウェブアプリ名かウェブアプリのインデックスを与えるとブラウザを起動してそのウェブアプリを表示してくれます。

ウェブアプリとなる関数の中身ですが,これも単純ですね。まず,今回は使っていませんが,引数はenvだけです。次に,Rookパッケージの名前空間にあるResponseオブジェクトにアクセスしています。ResponseオブジェクトはResponseクラスのgenerator objectで,newメソッドを使ってResponseオブジェクトを生成しています。このあたりはR5クラスの話になるので,無視してもらってもかまいません。あとは,Responseオブジェクト(res)にwriteメソッドでhtmlを与えてやるだけです。最後にfinishメソッドで終了します。

Rhttpdオブジェクトに登録されているウェブアプリは,printメソッドで一覧できます。

> s$print()
Server started on 127.0.0.1:21483
[1] RookTest   http://127.0.0.1:21483/custom/RookTest
[2] HelloWorld http://127.0.0.1:21483/custom/HelloWorld

Call browse() with an index number or name to run an application.

browseメソッドに渡すアプリ名やインデックスはここで確認できます。表示されるURLに直接アクセスしてもかまいません。

Rookの本質

超簡単ウェブアプリサーバーというのは実は仮の姿で,RookはRubyのRackのRでの実装というのが本質です。Rackがどのようなものかはこの記事が分かりやすいですが,要するにサーバーとアプリの橋渡しをするミドルウェアで,Rackに対応したアプリを作れば,Rackに対応したサーバーすべてに対応したことになる,というものです。とはいえRが使えるサーバーは現状Apache(rapache)しかないうえに,rapacheはまだRookに対応していないので,現状では実質的に超簡単ウェブアプリサーバーでしかないのですが。ちなみにRookの作者はrapache,brewの作者と同一なので,rapacheのRook対応は近いうちに実装されると思います。

もう少しウェブアプリっぽいもの

次はリクエストを受け取って処理をするアプリを作ります。といってもRook付属のサンプルなのですが。サンプルはライブラリのRookフォルダの中のexampleAppsフォルダに入っています。

app.hw2 <- function(env){
    req <- Rook::Request$new(env)
    res <- Rook::Response$new()
    friend <- 'World'
    if (!is.null(req$GET()[['friend']]))
	friend <- req$GET()[['friend']]
    res$write(paste('<h1>Hello',friend,'</h1>\n'))
    res$write('What is your name?\n')
    res$write('<form method="GET">\n')
    res$write('<input type="text" name="friend">\n')
    res$write('<input type="submit" name="Submit">\n</form>\n<br>')
    res$finish()
}
s$add(app=app.hw2, name="HelloWorld2")
s$browse("HelloWorld2")

今度はenvを使っています。Rook::Request$new(env)でRequestオブジェクトを生成しています。RequestオブジェクトからはPOSTメソッドやGETメソッドでリクエストの内容を取り出せます。他にも多くの情報が取り出せます。詳細はhelp(Request)にあります。アプリの中身はこれまた単純で,GETの中身がNULLならば何もせず,NULLでなければfriendをGETの中身で上書きする,というものです*1

ところで,formが出てきたのでXSS脆弱性に気をつけましょう。RookにはUtilsクラスがあり,便利なクラスメソッドが用意されています。そのなかにescapse_htmlというのがあります*2

> Utils$escape_html("<>'\"&")
[1] "&lt;&gt;&#39;&quot;&amp;"

もう少しRっぽいもの

今度はファイルを直接読み込んで登録します。読み込むファイルの仕様の記述は見つけられなかったのですが,ファイル内のappという名前の関数を読み込むようです。

s$add(app=system.file("exampleApps", "summary.R", package="Rook"),
      name="Summary")
s$browse("Summary")

CSVファイルを読み込んでsummary()に通した結果を出力するアプリです。

brew

brewパッケージはいわゆるテンプレートエンジンで,htmlの中にRのスクリプトを埋め込んだりするためのものです。詳しくはitoshiさんの連載で。参考にさせていただきました,ありがとうございます。

brewはRookといっしょにインストールされているはずです。

こちらの記事にならってヒストグラムを表示するアプリを作ってみます。では,次のコードをindex.rという名前で保存してください。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
  <head>
    <title>Histogram</title>
  </head>
<body>
  <% brew('./hist.r') %>
</body>
</html>

さらに,次のコードをhist.rという名前で保存してください。

<%
image_dir <- file.path(tempdir(), "plots")
if(!file.exists(image_dir)) dir.create(image_dir)

filepath  <- tempfile("hist_", tmpdir=image_dir, fileext=".png")
filename  <- basename(filepath)

data <- rnorm(1000)
png(filepath)
hist(data)
dev.off()
%>
<img src="./plots/<%=filename%>" />

まず,index.rはほぼhist.rの中身を読み込むだけのものです。hist.rは正規分布にしたがうデータからヒストグラムを作成し,imgタグを返しています。Rのスクリプトは<% 〜 %>の中か<%= 〜 %>の中に書きます。<%= 〜 %>の場合はスクリプトの出力がhtml内に埋め込まれます。

簡単なスクリプトですが,一カ所だけ変なところがあります。いま,filepathの中身はtempdir()を使っているので"C:\\Users\\masahiro\\AppData\\Local\\Temp\\RtmpUzjlma/plots\\hist_9654f.png"のようになっています。にもかかわらず,imgのsrcは"./plots/<%=filename%>"です。これはhtmlに出力されるさいには"./plots/hist_9654f.png"のようになります。このあたりのカラクリは次のスクリプトで。

brew.app <- Builder$new(Static$new(urls="/plots",root=tempdir()),
                        Brewery$new(url="/",root="(index.rとhist.rが置いてあるフォルダのパス)"),
                        Redirect$new("/index.r"))
s$add(name="brewApp", app=brew.app)
s$browse("brewApp")

brewで書いたアプリを登録するためのスクリプトです。Brewery$new()の中のrootだけ書き換えてください。Builder,Brewery,Redirectはgenerator objectで(以下略。Builderオブジェクトはウェブアプリ本体で,直接関数をRhttpdに突っ込むより柔軟に設定ができます。まずStaticオブジェクトで静的なコンテンツの場所を設定します。rootは静的なコンテンツの置かれている実際のフォルダを指定します。urlsはrootの中からウェブアプリ内で用いるフォルダを文字列ベクトルで指定します。少しややこしいですが,例えばstaticというフォルダ内にcssjavascript,imagesというフォルダを作っている場合,rootにstaticを指定して,urlsにc("/css", "/javascript", "/images")を指定します。この設定により,前述の"/plots/hist_9654f.png"へのアクセスは"C:\\Users\\masahiro\\AppData\\Local\\Temp\\RtmpUzjlma/plots\\hist_9654f.png"に読み替えられることになります。

次にBreweryオブジェクトでbrewファイルを読み込みます。root内のファイルがbrewファイルとして認識されます。urlにはStaticと同じくウェブアプリに読み込むファイルを指定します。"/"としておけばroot内のファイルが全てbrewファイルとして認識されます。正規表現を使うこともできるようです。

Redirectオブジェクトでは,ウェブアプリのURL(例えばhttp://127.0.0.1:21483/custom/brew/)にアクセスしたときに転送するURLを指定します。

ウェブアプリの公開

一般公開する場合,R内蔵のウェブサーバーがどの程度のものか分からないので,rapacheとの連携を待ったほうがよいような気がします。ただ,内部向けに公開したい場合もあるでしょう。その場合は,RhttpdオブジェクトのstartメソッドでIPアドレスとポートを設定できます。

s$start(listen="192.168.11.15", port="8080")

読むべき資料

ヘルプがけっこう充実しています。

  • help(Rhttpd)
  • help(Response)
  • help(Request)
  • help(Builder)
  • help(Brewery)
  • help(URLMap)

作者のブログ。この人,パッケージ作りまくってますね…すごい…。http://goo.gl/AJKV2

わからなかったこと

Rをサービスとして待機させておくことってできないんでしょうか? R --file=script.Rとかやっても,scriptを実行したら終了してしまう…。コンソール開きっぱなしにしておくのもスマートじゃないですし…。やっぱりrapache待ちでしょうか。でもrapacheを使うとなるとRookのお手軽さがなくなってしまうのが残念。

sessionInfo

> sessionInfo()
R version 2.13.0 (2011-04-13)
Platform: i386-pc-mingw32/i386 (32-bit)

locale:
[1] LC_COLLATE=Japanese_Japan.932  LC_CTYPE=Japanese_Japan.932    LC_MONETARY=Japanese_Japan.932 LC_NUMERIC=C                  
[5] LC_TIME=Japanese_Japan.932    

attached base packages:
[1] tools     stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] Rook_1.0-2 brew_1.0-6

*1:formタグでactionを指定しない場合,元のページにリクエストを送る,というのを今回知りました…

*2:ちなみにRookのパッケージ環境にあるUtilsはgenerator objectではなくUtilsクラスのオブジェクトそのものです。Rhttpd,Response,Requestはgenerator objectです。