6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Goで書かれたStatefulなVirtual Web BrowserライブラリのSurfでJavaScriptが処理できるようにしてみたかった

Last updated at Posted at 2018-12-23

はじめに

これは Go3 Advent Calenderの22日の記事です。
大分遅れて申し訳ありません…。

TL; DR;
出来る限り頑張りましたがottoとwebloopでは実現することが出来ませんでした…。orz
今後ottoとwebloop以外の方法もトライしてみたいと思います。

今回Advent Calender駆動開発で、以前作ったSlackにemojiをアップロードするツールを最新化しようかと考えていましたが、いきなり壁にぶち当たりました。
上記のツールはSlackにemojiをアップロードするAPIがないため、Surfという仮想ブラウザのライブラリを使って、半ば無理やりemojiをアップロードしていたのですが、SlackのUIがアップデートされてから動かなくなっていたのです。
Slackのemojiをアップロードするまでの遷移が

  • ページ内のフォームに入力

から

  • 「絵文字を追加する」ボタンをクリック
  • モーダルダイアログ内のフォームに入力

という遷移に変わったため、それに合わせてプログラムを修正する必要がありました。
最初は単純に考えていて、Surfの brower.Click() でダイアログを出して入力するだけでよいと思っていました。

しかしそう単純ではなかったのです…。
SurfのClick()はJavaScriptのイベントの発火はサポートしていなかったのです…。
実際Surfのbrowserのソースを見るとhttpのRequestやResponse、Cookie、DOM用にgoqueryなど、JavaScript関連の処理は行っていない様でした。

これは困りました。これが出来ないとそもそもやりたいことが出来ません。
という訳でSurfにどうやったらJavaScriptを実行する機能が付けられるか挑戦してみました。

まずは調査

GolangでJavaScriptを実行出来る様なヘッドレスブラウザがないかどうか調べていたところ、goqueryのTips and tricksにHandle Javascript-based Pagesという項目がありました。
ここでは以下の2つのライブラリが紹介されていました。

  • JavaScriptパーサー otto
  • ヘッドレスブラウザ webloop

この2つでどうにか出来ないかやってみました。

otto

こちらはJavaScriptを実行して結果を取得できるというものですが、全てGolangで書かれているというものすごいライブラリでした。
これでページ内のJavaScriptを実行すれば出来るはず…と思って調べたのですが、こちらあくまでもjavaScriptを実行するためのライブラリとの事で、HTMLを読み込んでDOMを操作するといったことは出来ないとのことでした。
ですが、イシュー等を見た限り、Reactを動かしたりページ内の一部のJavaScriptを動かして必要な情報を取得したりといった使い方も出来るらしいです。

上記の事から今回はottoではHTMLを渡してJavaScriptを実行することは難しいと判断しました。

webloop

こちらはGolangからwebkit2を動かすgo-webkit2を使っているヘッドレスブラウザだそうで、URLを読み込んでJSを実行したり出来るそうなので、これならやりたい事が出来るのでは、と思いました。

方法としては、webloopはURIからソースを読み込むだけではなく、HTMLをstringにして読み込ませることが出来る様なので、Surfから必要な時にHTMLを取り出してwebloopでJavaScriptを処理して結果をSurfから返すという流れを考えました。

環境のセットアップ

今回使ったのは(ちょっと古いのですが)macOS Sierraです。

go-webkit2のページを見て必要な依存関係をインストールします。
こちらのイシューを見て、Macの場合以下のコマンドを発行すれば良いことが分かりました。

$ sudo port install webkit2-gtk
$ sudo port install webkit-gtk3

しかし、ずっと前にMacPortsをインストールしたまま放置していた私は既に入っていたものが古かったのか、中々ビルドが成功しませんでした。
結局以下のコマンドで一度全部消してインストールし直しました。

$ sudo port -fp uninstall installed

途中依存関係のあるもののinstallに失敗した場合は、一度失敗したものだけcleanしてinstallしたら成功しました。

sudo port clean 失敗したもの
sudo port install 失敗したもの

テストしてみる

エラーが解消されたら、まずはgo-webkit2のテストをいくつか動かしてみました。
しかしなんだか動きが怪しい…。
VS Codeでテストを実行すると失敗したり、処理が帰って来なかったりと動かない。
ステップ実行すると成功したりと、もう既に不安しかない。

そしてwebloopの方のテストはデバッグ実行でもうまく動かない…。
webloopは諦め、go-webkit2で頑張ることにしました。

既存のテストにないファイルから読み込んでテストするパターンを試してみます。

test_simple.html
<!doctype html>
<html>
	<head>
		<title>test</title>
	</head>
	<body>
		<button id="button1">test</button>
		<div id="target"></div>
		<script>
			document.getElementById('button1').addEventListener('click', function() {
				document.getElementById('target').innerHTML = '<p>good</p>';	
  			});
		</script>
	</body>
</html>
simple_test.go
func TestWebView_RunSimpleJS(t *testing.T) {
	webView := NewWebView()
	defer webView.Destroy()

	loadOk := false
	webView.Connect("load-failed", func() {
		t.Errorf("load failed")
	})
	webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) {
		switch loadEvent {
		case LoadFinished:
			webView.RunJavaScript(`document.getElementById("button1").click()`, func(result *gojs.Value, err error) {
				if err != nil {
					t.Errorf("RunJavaScript error: %s", err)
				}
				webView.RunJavaScript(`document.getElementsByTagName("body")[0].outerHTML`, func(result *gojs.Value, err error) {
					resultString := webView.JavaScriptGlobalContext().ToStringOrDie(result)
					fmt.Println(resultString)
					if strings.Count(resultString, "<p>good</p>") > 1 {
						loadOk = true
					}
					gtk.MainQuit()
				})
			})
		}
	})

	f, err := os.OpenFile("test_simple.html", os.O_RDONLY, 0755)
	if err != nil {
		t.Errorf("File open err: %s", err)
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)

	htmlData := ""
	for scanner.Scan() {
		htmlData = htmlData + scanner.Text() + "\n"
	}

	glib.IdleAdd(func() bool {
		webView.LoadHTML(htmlData, "")
		return false
	})

	gtk.Main()

	if !loadOk {
		t.Error("!loadOk")
	}
}

結果無事にステップ実行しなくても成功しました。

API server listening at: 127.0.0.1:16583
dbus[55590]: Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that org.freedesktop.dbus-session.plist is loaded!
dbus[55594]: Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that org.freedesktop.dbus-session.plist is loaded!
<body>
		<button id="button1">test</button>
		<div id="target"><p>good</p></div>
		<script>
			document.getElementById('button1').addEventListener('click', function() {
				document.getElementById('target').innerHTML = '<p>good</p>';	
  			});
		</script>
	

</body>
PASS

本命のHTMLを突っ込んで見る

さて、簡単なHTMLが動作したので本命のページから抜いてきたHTMLを突っ込んでJavaScriptを実行してみたのですが…。
結果は駄目でした。
JavaScriptを実行する箇所が待っても返ってきません。

まだ調査の余地はあるのですが、時間も大分過ぎてしまっているので今回はここまでとすることにしました :bow:

やってみて

そもそもscriptタグ内で指定された外部のスクリプトは取得されているのかとか、普通に見る場合は認証が必要なページだが大丈夫なのだろうかなど、色々と検証が不足しているのですが、Webloopがちゃんと動くのかどうかが知りたいところです。
また、認証情報ごと渡してJavaScriptを動かす必要があるページだけ動作させられれば良さそうですが、その辺りも調べてみたいです。
他にもいくつかGolangで動作するHeadless Browserはある様なので、そちらが使えそうかどうか見てみたいと思いました。

P. S.

このイシューの今年の9月辺りの書き込みを見たところ、ドキュメントには載ってないけど /api/emoji.add にユーザートークンとmultipart formアップロードすればemojiが追加できると書かれていました。
いやーAPIが追加されていないか、もっと早い段階で念のため調べておくべきでした…。orz
このAPIを使用したプログラムに書き換えれば、取り敢えずやりたい事は出来そうです。
皆さんも時間が経ったらまずは欲しいAPIが提供されてないか確認しましょう。

6
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?