mike、mikeなるままに…

プログラムに関してぬるま湯のような記事を書きます

JJUG LT大会で話した内容の要点

こんにちわ、みけです。

昨日(2013/08/19)に開催されたJJUGのLT大会でLTをしてきました。

LTの内容ですが、

ネタLTとしては大成功(?)でしたが、

まあJavaのLTとしては疑問符が残るものを発表しました。

とりあえず、JavaFXの部分だけ抽出して書きたいと思います。

要望・仕様

ウケ狙いをしたLTの「冒険者の広場』のページについて、次のような要望・仕様になっています。

  • 定期的にWebの情報を取得したい
  • Web APIが公開されていない
  • リンク先のURLがJavascriptでのクリックにバインドされた関数に記載されている

スクレイピングツールの候補

Javaで利用できるスクレイピングツールおよびブラウザーは次のものが主に挙げられます

  • NekoHTML/Jsoup
  • HtmlUnit
  • Selenium
  • JavaFXのWebEngine

NekoHtml

Java1.3以降から使えるHTMLパーサーです。

古くてかつ最近は更新が遅いようです。

最新のHTMLをパースするのには向いていません。

パースするだけなのでJavascriptの実行はできません。

したがって、採用しませんでした。

Jsoup

HTML5のタグにも対応したHTMLパーサーです。

要素の取得などは非常に便利なのですが、

これもHTMLをパースするだけでJavascriptの実行はできません。

したがって、採用しませんでした。

HtmlUnit

NekoHtmlをHTMLパーサーとして利用したヘッドレスブラウザーです。

Javascriptの実行にRhinoを使っています。

僕自信の目的にはこれで十分な機能だったのですが、

残念ながら『冒険者の広場』を表示しようとしたら、

Javascriptエラーが発生したので、

採用しませんでした。

Selenium

言わずと知れたFireFoxの自動実行ツールです。

ブラウザの操作を覚えさせることも可能なため、

比較的スクリプトを作るのが楽です。

しかし、個人的にはブラウザーがポコポコ立ち上がるのが

好きでないので採用しませんでした。

JavaFXのWebEngine

レンダリングエンジンにWebKitを採用したブラウザーです。

Javascriptの実行にRhinoが使われています。

JavaFXのアプリケーション側からJavascriptの実行も指示することができます。

好みであれば画面を立ち上げることなしに実行できます。

特に文句をつけるところもないので採用しました。

JavaFXをヘッドレスブラウザーとして使う時のやり方

Applicationを継承したクラス

JavaFXのアプリケーションですので、Applicationクラスを継承したクラスを作成します。

DqxWebDriver.java
1
2
3
4
5
6
7
8
9
public class DqxWebDriver extends Application {
    private static WebEngine engine;
    private static final String URL = "…";
    @Override
    public void start(Stage stage) {
        engine = new WebEngine();
        engine.load(URL);
    }
}

ここでのポイントは2つです。

  • startメソッドの中でWebEngineのインスタンスを生成すること
  • 特に画面を表示する必要がないので、stage.show()を実行しないこと

JavaFXの起動と終了

Application.launch(Class<? extends Application>)を実行すればいいのですが、

これを実行するとJavaFXアプリケーションを実行している間は

何もできなくなるため、別のスレッドで実行する必要があります。

Bazaar.java
1
2
3
4
5
6
7
8
9
10
ExecutorService service = Executors.newFixedThreadPool(1);
service.submit(new Runnable(){
    @Override
    public void run() {
        Application.launch(DqxWebDriver.class);
    }
});
// Do some operation
Platform.exit();
service.shutdown();

JavaFXの終了にはPlatform.exit()を実行します。

これによりJavaアプリケーション自体が起動したままになるのを防ぐことができます。

Javascriptの実行

WebEngineexecutScript(String)を実行します。

DqxWebDriver.java
1
2
3
4
5
6
7
8
9
private static WebEngine engine;
public static void executeScript(
        final String script,
        final BlockingQueue<Object> queue) {
    Platform.runLater(new Runnable(){
        Object result = engine.executScript();
        queue.put(result);
    });
}

WebEngineの操作はJavaFXのApplication Threadから実行しなければなりません。

そのために、Platform.runLater(Runnable)を介して実行する必要があります。

引数はRunnableのために、戻り値を利用することができません。

そのために、BlockingQueue<T>を使っています。

WebEngine#executeScript(String)の戻り値をここではObjectとして利用していますが、

戻り値は実際には次のようにマッピングされます。

Javascriptでの型Javaでの型
booleanBoolean
number(整数値)Integer
number(少数値)Double
stringString
objectnetscape.javascript.JSObject
functionnetscape.javascript.JSObject
nullnull

Javascriptでの配列はobjectですので、JSObjectにマッピングされます。

JSObjectのあ使い方ですが、getMember(String)getSlot(int)を用いて値にアクセスします。

getMemberメソッド

{hoge : "hoge", foo : 10}というオブジェクトの場合、

次のようにgetMember(String)メソッドを使用して値を取り出します。

1
2
String hoge = (String)object.getMember("hoge");
int foo = (int)object.getMember("foo");

getSlotメソッド

[1,2,3,4,5]という配列の場合、

次のようにgetSlot(int)メソッドを用いて値を取り出します。

1
2
3
int one = (int)object.getSlot(0);
int two = (int)object.getSlot(1);
int five = (int)object.getSlot(4);

WebEngineの同期

先ほど書いたように、WebEngineへの操作は別スレッドで実行することになりますが、

さらにJavaFX ApplicationスレッドとWebEngineは異なるスレッドで実行されています。

したがって、WebEngineの動作に合わせてアプリケーションの実行をしたいシーンが発生すると思います。

その場合、getLoadWorkerメソッドでWorkerを取得して、getStateにて

WebEngineの状態を確認する必要があります。

DqxWebDriver.java
1
2
3
4
5
6
7
8
9
10
private static WebEngine engine;
public static Worker.State getEngineState() {
    final BlockingQueue<Worker.State> queue = new BlockingQueue<>();
    Platform.runLater(new Runnable(){
        @Override
        public void run() {
            queue.put(engine.getLoadWorker().getState());
        }
    });
}

この結果がWorker.State.RUNNINGの間は、

Javascriptの実行などをしているため、

戻り値を利用するなどのアプリケーション操作はできません。

したがって、Thread.sleep(long)等で停止しておくとよいでしょう。

DqxWebDriver.java
1
2
3
4
5
6
7
public static void waitForEngine() {
    Worker.State state = Worker.State.RUNNING;
    while(state.equals(Worker.State.RUNNING)) {
        Thread.sleep(100l);
        state = getEngineState();
    }
}

おわり

以上が昨日全くもって話しえなかったことです。

スレッド周りが非常に面倒な感じがしますが、

慣れればあとはJavascriptを好きに実行できるので、

WebアプリでのJavascriptのテストなどで活用できたりします。