うだうだ
とあるサイトをJsoupでスクレイピングして情報を収集していたのですが、ある日を境に収集に失敗するように。調べてみるとreact.jsというのを使って動的にページを作成するように変わったようです。
こうなると単なるhtmlをダウンロードするjsoupではお手上げ、ブラウザのように振舞いJavascriptを実行してページを読み込まないといけません。
playframework1ではテスト機能をtestrunnerというモジュールとして内蔵しています。その中でhtmlテスト用にhtmlUnitというライブラリを標準で使用するようになっています。いわゆるheadlessブラウザで簡単なWebページやJavascriptであれば表示(画面はないのでDOMとして読み込み)が可能です。
htmlUnit
は確かにJavaScriptを実行できるのですが、JavaScriptエンジンにMozilla Rhihoというオープンソースのエンジンを採用していて、その仕様に制限を受けることが間々あります。Yahoo!JAPANのような皆が見るようなサイトでもJavaScriptエラーが出まくり例外スローを抑える設定をしないとページ読み込みさえ完遂できないことがあります。(WebClient
を生成するときのバージョン指定で古いブラウザであるINTERNET_EXPLORER
を指定すると相手方で手加減してくれるワンチャンあるようです)
自分でサイトを一から作成するときのテストツールとしては使えないこともないですが、一般に公開されているサイトのスクレイピング用には不向きと言わざるを得ません。
次なる候補としてui4j
というのを使ってみることにしました。JavaFXに含まれるWebKitのラッパーライブラリということで使いやすそうだったからです。
導入…できない?
ui4jのgithubレポジトリ(webfolderio/ui4j: Web Automation for Java (github.com))を見ると、mavenレポジトリから取得できるよ、と書いてあります。headlessモードに対応させるためのMonocleも同じように取得できるので conf/dependencies.yml
にて解決しそうです。
<dependency>
<groupId>io.webfolder</groupId>
<artifactId>ui4j-webkit</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.testfx</groupId>
<artifactId>openjfx-monocle</artifactId>
<version>jdk-11+26</version>
<scope>test</scope>
</dependency>
require:
- play
- play -> docviewer
- io.webfolder -> ui4j-webkit 4.0.0
- org.testfx -> openjfx-monocle jdk-11+26
こんな感じに記述して、play deps
もエラーなく成功。でも実際に実行してみると、javafx.application.Platform
というクラスが見つからないと例外が出てしまいました。
java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at play.modules.launcher.Launcher.main(Launcher.java:57)
Caused by: java.lang.NoClassDefFoundError: javafx/application/Platform
at io.webfolder.ui4j.webkit.WebKitBrowser.<init>(WebKitBrowser.java:59)
at io.webfolder.ui4j.webkit.WebKitBrowserProvider.create(WebKitBrowserProvider.java:27)
at io.webfolder.ui4j.api.browser.BrowserFactory.getBrowser(BrowserFactory.java:115)
at io.webfolder.ui4j.api.browser.BrowserFactory.getWebKit(BrowserFactory.java:151)
at io.webfolder.ui4j.api.browser.BrowserFactory$getWebKit.call(Unknown Source)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:119)
at test.run(test.groovy:13)
at play.modules.launcher.GroovyLauncher.executeScript(GroovyLauncher.java:90)
at jobs.LaunchScript.main(LaunchScript.java:33)
... 4 more
Caused by: java.lang.ClassNotFoundException: javafx.application.Platform
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
... 15 more
な、なにゆえに。と調べてみるとui4j
に依存するライブラリとしてダウンロードされた、javafx-base.jar
、javafx-graphics.jar
、javafx-web.jar
、javafx-controls.jar
の中身が空っぽでした。
さらに調べを進めると、そもそもそれらのライブラリは実行環境によって、win/linux/macを指定するものらしいです。(例:<classifier>win</classifier>
)
解決できた
で、どうやって解決したかというと、下記のようにui4j
の定義の前に javafx
関連の定義を書くことで解決できました。先勝ちのようですね。
require:
- play
- play -> docviewer
- org.openjfx -> javafx-base 11.0.2 win
- org.openjfx -> javafx-graphics 11.0.2 win
- org.openjfx -> javafx-web 11.0.2 win
- org.openjfx -> javafx-controls 11.0.2 win
- io.webfolder -> ui4j-webkit 4.0.0 win
- org.testfx -> openjfx-monocle jdk-11+26
ちなみに、いったん実行環境(プラットフォーム)の指定なし版をダウンロードしまうと、実行環境を指定した版を取得してくれないことがありました。そんなときは強制キャッシュクリアのオプション(--clearcache
)を付けましょう。
BrowserEngine browser = BrowserFactory.getWebKit()
Page page = browser.navigate('about:blank')
page.show()
page.getDocument().getBody().append('<h1>Hello, world!</h1>')
Thread.sleep(5000)
page.close()
browser.shutdown()
ひとまずブラウザの表示までの動きが確認できました。

しかし?
ブラウザが起動できることは確認できたので一般のサイトを表示させてみよう、ということでお馴染みの Yahoo! JAPAN を開いてみました。ところが、java.lang.NoClassDefFoundError: com/sun/media/jfxmedia/MediaManager という例外が発生して止まってしまいました。まだ何か足りないようです。
[main] INFO io.webfolder.ui4j.api.browser.BrowserFactory - Initializing WebKit
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [30%]
Exception in thread "JavaFX Application Thread" java.lang.NoClassDefFoundError: com/sun/media/jfxmedia/MediaManager
at com.sun.javafx.webkit.prism.PrismGraphicsManager.getSupportedMediaTypes(PrismGraphicsManager.java:155)
at com.sun.webkit.MainThread.twkScheduleDispatchFunctions(Native Method)
at com.sun.webkit.MainThread.lambda$fwkScheduleDispatchFunctions$0(MainThread.java:35)
at com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.ClassNotFoundException: com.sun.media.jfxmedia.MediaManager
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
... 10 more
その後、さらにJavaFXについて調べた結果、OpenJDK11を利用している場合、jarの取得だけでは不十分ということが分かりました。OpenJDKではJavaFX部分が分離されており、実行時にJavaFXのモジュールを利用する場合はVM引数で明示しなければならないそうです。
具体的には下記のようなVM引数を付けることで、ブラウザで Yahoo! JAPAN を表示するところまで到達しました。(headlessの場合はjavafx.graphicsも?)
--module-path="C:\Tools\bin\javafx-sdk-11.0.2\lib"
--add-modules=javafx.controls,javafx.web
--add-exports javafx.web/com.sun.webkit=ALL-UNNAMED
OpenJDK 11にJavaFXを導入する
https://blogs.osdn.jp/2018/11/12/merge-openjfx.html
JDK に JavaFX が同梱されなくなったため、 JavaFX アプリケーションの開発には別途 OpenJFX の導入が必要になりました。
JavaFXライブラリのインストール – ソフトウェアエンジニアリング – Torutk
https://www.torutk.com/projects/swe/wiki/JavaFX%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB
本ページは、Java SE 11以降でJavaFXライブラリを利用した開発をするためのインストール作業を記述します。
java – Package ‘com.sun.webkit.dom’ is declared in module ‘javafx.web’, which does not export it to module – Stack Overflow
https://stackoverflow.com/questions/47684470/package-com-sun-webkit-dom-is-declared-in-module-javafx-web-which-does-not
というわけでめでたくウェブページを表示するところまでたどり着きました。(まだ下記のようなエラーは出ますが)
[main] INFO io.webfolder.ui4j.api.browser.BrowserFactory - Initializing WebKit
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [30%]
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [50%]
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [55%]
[JavaFX Application Thread] INFO io.webfolder.ui4j.webkit.WebKitBrowser - Loading https://www.yahoo.co.jp/ [100%]
Exception in thread "JavaFX Application Thread" java.lang.IllegalAccessError: class io.webfolder.ui4j.webkit.browser.WebKitPageContext (in unnamed module @0x30c93896) cannot access class com.sun.webkit.dom.DocumentImpl (in module javafx.web) because module javafx.web does not export com.sun.webkit.dom to unnamed module @0x30c93896
at io.webfolder.ui4j.webkit.browser.WebKitPageContext.createDocument(WebKitPageContext.java:122)
at io.webfolder.ui4j.webkit.WebKitBrowser$WorkerLoadListener.changed(WebKitBrowser.java:161)
at io.webfolder.ui4j.webkit.WebKitBrowser$WorkerLoadListener.changed(WebKitBrowser.java:1)
at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.base/javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
at javafx.base/javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:113)
at javafx.base/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
at javafx.web/javafx.scene.web.WebEngine$LoadWorker.updateState(WebEngine.java:1251)
at javafx.web/javafx.scene.web.WebEngine$LoadWorker.dispatchLoadEvent(WebEngine.java:1366)
at javafx.web/javafx.scene.web.WebEngine$LoadWorker.access$1200(WebEngine.java:1244)
at javafx.web/javafx.scene.web.WebEngine$PageLoadListener.dispatchLoadEvent(WebEngine.java:1231)
at javafx.web/com.sun.webkit.WebPage.fireLoadEvent(WebPage.java:2513)
at javafx.web/com.sun.webkit.WebPage.fwkFireLoadEvent(WebPage.java:2358)
at javafx.web/com.sun.webkit.network.URLLoader.twkDidFinishLoading(Native Method)
at javafx.web/com.sun.webkit.network.URLLoader.notifyDidFinishLoading(URLLoader.java:871)
at javafx.web/com.sun.webkit.network.URLLoader.lambda$didFinishLoading$5(URLLoader.java:862)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
at java.base/java.lang.Thread.run(Thread.java:834)
そしてもう一つ問題が。思ったより表示にかかる時間が長く、気軽に使える感じじゃありません。もしかすると設定値とかを工夫すると何とかなるのかもしれませんが、ちょっとモチベーションがそこまで続きそうもないので、ui4j
は一旦ここまでとしたいと思います。
唐突に終わってすみません。