この記事の位置づけ
ほとんどただのメモですが、後できちんと書き直すかもしれません。
やりたいことは、タイトルの通りShiny <-> Qlik Sense(Desktop)でのデータのやり取りです。
RとQlikView/Qlik Senseの連携を色々と模索してみましたが(QlikView Personal EditionでSQLite経由でRを使う方法とかも試しました)、たぶんこれが最もスマートではないかなーと。
すでに海外ではそういう事例もあるみたいですが、
https://www.reddit.com/r/qlikview/comments/3bgoqu/moving_from_qlikview_dashboarding_to_shiny/
Qlik Communityにも転がっていなさそうだったので、真面目にやってみることにしました。
GETはかんたん
Qlik Sense側
http://ameblo.jp/maichest/entry-11269055205.html
を参考にして、form method="get"で渡す。これはうまくいく。
divName.innerHTML = '<div id="getShiny" style="display: none;"><iframe src="http://127.0.0.1:7458/?a=1" name="mustUnique"></iframe>';
divName.innerHTML += '<form method="get" action="http://127.0.0.1:7458/" target="mustUnique" name="testForm"><input type="text" name="a" value="10" /><input type="submit" /></form></div>';
<注意>innerHTML += で中途半端に切ると、勝手にタグを閉じやがります。こういう小さな親切余計なお世話は結構イライラしますね。。。
Shiny側
正直なんでもよいが、一応getで受け取れることを確認する。
http://stackoverflow.com/questions/25297489/accept-http-request-in-r-shiny-application
を参考にして、
query <- parseQueryString(session$clientData$url_search)
paste(names(query), query, sep = "=", collapse=", ")
とかいうものを使って確認する。
ShinyServerの起動は、ホスト名とポート番号を指定してrunApp(port=7458,host="127.0.0.1")
で。
7458というのは、使っていなかった適当なポート。
こんな感じー@GET
<余談>iframeで右クリックをすると、実はQlik Senseの実体(?)がChromiumであったことが分かります。RStudioはQtっぽいですが、同じような感じですね。今後、こういうWebアプリ+ブラウザ同梱みたいな形式のアプリ開発が流行るのかなーと勝手に思っています。
POSTはたいへん?
POSTのデータを受け取るのは、Shiny側が大変です。
Qlik Sense側
divName.innerHTML = '<div id="getShiny" style="display: none;"><iframe src="http://127.0.0.1:7458/?a=1" name="mustUnique"></iframe>';
divName.innerHTML += '<form method="post" action="http://127.0.0.1:7458/api_url" target="mustUnique" name="testForm"><input type="text" name="a" value="10" /><input type="text" name="b" value="20" /><input type="submit" /></form></div>';
divName.innerHTML += '<input id="url_input" /><input type="button" onClick="testForm.action=\'http://127.0.0.1:7458/session/\' + document.getElementById(\'url_input\').value" value="url" />';
本当はここまでゴテゴテする必要はないのですが、実験用ソースをそのまま貼りつけています。
Shiny側
これが中々大変です。
そもそも、Shiny側はPOSTを簡単に扱ってくれません。
上のstackoverflowのリンクの内容が非常に参考になりますが、registerDataObjを使ってサーバー側で生成されるmessage.urlをクライアントに渡してやって、これをURLに使ってPOSTする、とかいう事をしなけれないけないようです。
※単純にPOSTすると、"Not Found"が返却されます。サーバー側でGETの場合だけui.Rに対応するHTMLソース的なものを返しているようです。
色々実験した結果、とりあえず次のようなソースであれば、「ひと手間加えることで」POSTした結果をうまく渡すことができる、ということが分かりました。
library(shiny)
shinyUI(fluidPage(
uiOutput("test"),
singleton(tags$head(HTML(
'
<script type="text/javascript">
$(document).ready(function() {
// creates a handler for our special message type
Shiny.addCustomMessageHandler("api_url", function(message) {
// set up the the submit URL of the form
$("#form1").attr("action", "/" + message.url);
document.getElementById("shiny_test").innerHTML = message.url;
$("#submitbtn").click(function() { $("#form1").submit(); });
});
})
</script>
'
))),
tabsetPanel(
tabPanel('POST request example',
# create a raw HTML form
HTML('
<div>This is dynamic api_url:<span id="shiny_test"></span></div>
<form enctype="multipart/form-data" method="post" action="" id="form1">
<span>Name:</span>
<input type="text" name="name" /> <br />
<span>Passcode: </span> <br />
<input type="password" name="passcode" /><br />
<span>Avatar:</span>
<!-- <input name="file" type="file" /> <br /> -->
<input type="button" value="Upload" id="submitbtn" />
</form>
')
)
)
))
library(shiny)
shinyServer(function(input, output, session) {
output$test <- renderText({
print(session$clientData$url_search)
query <- parseQueryString(session$clientData$url_search)
print(paste(names(query), query, sep = "=", collapse=", "))
paste0(
paste(names(query), query, sep = "=", collapse=", ")
,
"<script>alert('test!')</script>
<div style='color:red;'>test?</div>"
)
})
api_url <- session$registerDataObj(
name = 'api', # an arbitrary but unique name for the data object
data = list(), # you can bind some data here, which is the data argument for the
# filter function below.
filter = function(data, req) {
print(ls(req)) # you can inspect what variables are encapsulated in this req
# environment
if (req$REQUEST_METHOD == "GET") {
# handle GET requests
query <- parseQueryString(req$QUERY_STRING)
# say:
# name <- query$name
# etc...
}
if (req$REQUEST_METHOD == "POST") {
# handle POST requests here
reqInput <- req$rook.input
# read a chuck of size 2^16 bytes, should suffice for our test
# sasanquaneuf mod begin
# data must be one line and must be the form of http://www.yoheim.net/blog.php?q=20120611
# buf <- reqInput$read(2^16)
contFlg = T
loopCnt = 0
strs <- paste0("?", reqInput$read_lines(1))
buf <- paste0(
'<HEAD><META HTTP-EQUIV="Refresh" CONTENT="0; URL=http://127.0.0.1:7458/',strs,
'" /></HEAD>')
# sasanquaneuf mod end
# simply dump the HTTP request (input) stream back to client
shiny:::httpResponse(
status=200, content_type='text/html', content=buf
)
}
}
)
# because the API entry is UNIQUE, we need to send it to the client
# we can create a custom pipeline to convey this message
session$sendCustomMessage("api_url", list(url=api_url))
})
ポイントとしては、Shiny側ではPOSTされたデータを受け取った後、何事もなかったかのようにredirectする、というところでしょうか。このredirectのタイミングで、今は試験的にPOSTされた値をそのままペタっと貼りつけていますが、ここでPOSTされたデータを処理してから、そのデータのキー的な値をURLの?以降に入力してredirect、というのが望ましい処理ではないかと思います。
こんな感じー@POST
試験実装の都合で、startを押さないといけません。ちなみに、startを押す前はdisplay:none
にしていますが、Shinyへのアクセスは発生します。(上のソースではalertを入れており、このalertがボタンを押す前に表示されます。)
上の方でゴニョゴニョと述べているmessage.urlを手動でコピペします。これは、iframeにしているせいで、簡単にiframeの中からデータを取得できなかったので、とりあえずそういう操作をしています。
Shiny Server側できちんとapiを実装すれば、本当はこのやり取りは不要になる…筈。
urlボタンを押してから送信ボタンを押すと、Qlik Sense Desktopの画面のinputに入力している値がPOSTでShiny Serverに渡されて、Shiny ServerではリダイレクトするためのURLを返却し、それに従ってリダイレクトして画面のようにinputの内容を表示します。
なぜこういうことをしたいのか(ご利益)
とりあえず今の仕組みを利用すれば、こういう感じの処理は確実に実装できます。
なぜこういう感じの処理を実装したいのかというと、単純にQlik Senseのピボットテーブルの処理が速い筈、だからです。
個人的には、Qlik Senseは画面とのやり取りや一部インターフェイスがQlikViewと比較して若干物足りない感はあるのですが、コアロジックの部分はQlikView(11SRxx)よりも速い気がしています。
そのQlik Senseでピボットテーブルみたいな操作を、dplyrとかtidyrとかSQLとかを使わずに、素人が高速にカチャカチャできる、というところにかなりのアドバンテージがあると思っています。
そうやってカチャカチャと変形したり、選択抽出したりしたデータを、適当に整形してデータフレームにして機械学習させたり回帰分析を行ったり、というようなことには確実に需要があるはず、です。
公式にも、今後機械学習や統計処理の部分を強化していくと言われていますが、こうやってShinyとの連携を手ごろに実現できればだいぶいい感じなのでは…と思っています。
まあ、逆に言えば、Shinyで簡単にピボットテーブルを使ったりデータの選択抽出をできるならば、それで十分かも…ということでもあります。そのうちきちんと作りたいなあと思いつつ、誰かやってくれないかなーという期待を込めて、とりあえず途中経過を書いておくのでした。