はじめに
この間「シェルスクリプト用の WebDriver bindings」を作ったのですが、どうやら世の中は更に進んでおり、Chrome Dev Protocol (CDP) 経由でより高機能な Chrome DevTools の機能による自動操作が使われてきているということを知りました。WebDriver は W3C で標準化されてるので多くのブラウザで使えるというメリットがあるとは言え、機能やパフォーマンスは DevTools の方が優れており、Chrome 以外のブラウザにも搭載されつつあるので対応させたいところです。しかし CDP では HTTP ではなく WebSocket を使います。はたしてそれがシェルスクリプトから呼び出せるのか?ということでやってみました。
websocat
WebSocket は ハンドシェイクにこそ HTTP と同じ形式を使用しますが HTTP ベースのプロトコルではありません。HTTP であれば curl
や wget
コマンドが使えそうですが CDP は WebSocket なので使うことができません。そもそもシェルスクリプトは POSIX 準拠の範囲ではネットワークに対応していません。bash, ksh であれば /dev/tcp から、zsh であれば net/tcp モジュールを使って TCP プロトコルを使うことが可能ですが WebSocket に対応させるのは大変ですし、どちらにしろ使えるシェルが限られます。
そこでいろいろと探してみるとどうやら websocat を使えば WebSocket 通信が出来きそうだということがわかりました。そして都合のいいことに CDP 呼び出しのサンプルまで書いてありました。(読みやすいように整形しています。JSONは実際は一行です。)
$ chromium --remote-debugging-port=9222 &
$ curl -sg http://127.0.0.1:9222/json/new \
| grep webSocketDebuggerUrl | cut -d'"' -f4 | head -1
ws://127.0.0.1:9222/devtools/page/A331E56CCB8615EB4FCB720425A82259
$ echo 'Page.navigate {"url":"https://example.com"}' | websocat -n1 --jsonrpc \
ws://127.0.0.1:9222/devtools/page/A331E56CCB8615EB4FCB720425A82259
{
"id":2,
"result":{
"frameId":"A331E56CCB8615EB4FCB720425A82259",
"loaderId":"EF5AAD19F2F8BB27FAF55F94FFB27DF9"
}
}
この通りにやれば動くであろうということでやってみたのですが以下のようなエラーがでて動きませんでした。
{
"error":{
"code":-32600,
"message":"Message has property other than 'id', 'method', 'sessionId', 'params'"
}
}
問題を解決する鍵は 'Page.navigate {"url":"https://example.com"}'
と --jsonrpc
です。前者は websocat
が対応してる特殊なフォーマットで --jsonrpc
オプションによって以下のような JSON-RPC 2.0 形式に変換されます。
{
"jsonrpc": "2.0",
"method": "Page.navigate",
"params": {"url":"https://example.com"},
"id": 1
}
エラーメッセージから jsonrpc
キーが余計なのでは?と思い、以下のように jsonrpc
を削って JSON で直接送信してみると動作しました。
echo '{"method":"Page.navigate","params":{"url":"https://example.com"},"id":1}' \
| websocat -n1 ws://...
しかし params
キーにオブジェクトが使えるのは、ここによると JSON-RPC 2.0 からのようなのでフォーマット自体は 2.0 で間違いなさそうです。おそらく CDP 側で余計なバリデーションをしてるバグがあるような気がします。
Selenium/WebDriver bindings for shell script との統合
さて私がやりたいのは Selenium/WebDriver bindings for shell script との統合です。とりあえず簡易的に組み込んでみました。
# !/bin/sh
set -eu
. ./lib/webdriver.sh
chrome_options() {
echo '{ "args": [] }'
# echo '{ "args": ["--headless"] }'
}
WebDriver driver="$(ChromeDriver chrome_options "http://localhost:9515")"
debugger=$(driver capabilities | jq -r '.["goog:chromeOptions"].debuggerAddress')
wsdebugger=$(curl -sg "http://$debugger/json/new" | jq -r .webSocketDebuggerUrl)
json='{"method": "Page.navigate", "params": {"url":"https://example.com"}, "id": 1}'
echo "$json" | websocat -n1 "$wsdebugger"
sleep 3
driver quit
unset -f element driver
接続先アドレスを WebDriver から取得しているだけでやっていることは大して変わりません。動作させることは出来たのでこれをわかりやすいインターフェースに変更するすれば終わりです。例えばこんな感じにすると良さそうです。
WebDriver driver="$(ChromeDriver chrome_options "http://localhost:9515")"
driver Page.navigate '{"url":"https://example.com"}'
あとは双方向通信が必要な場合はどうするんだ?という問題があるのですが、たぶん多くは双方向通信は必要ないと思うので大部分の CDP の機能は使えるんじゃないかと思います。WebSocket から知らなかったので調べるのに時間がかかりましたが、websocat
のおかげで思ったより簡単に DevTools の機能が使えそうです。なお CDP でどんな機能が使えるかは https://chromedevtools.github.io/devtools-protocol/ を参照してください。