だるろぐ

明日できることは、今日しない。

WPF + PhantomJS で Web ページの内容を取得してみる

「AngularJS で作られた Web サイトの内容がとれないよ……」って Twitter で泣いてたら、@nakaji 先生が「PhantomJS 使えばええやろ」的なことを言っていた気がするので、ちょっと試してみました。

PhantomJS とは

ぶっちゃけあんまりよくわかってないのですが、“Google Chrome のユーザーインターフェイスがない*1やつ”だと思えばだいたい合ってるみたいです。JavaScript で動的にデータをとってくるタイプの Web サイトの DOM をわちゃわちゃしたり、スクリーンショットをとって保存したり、ユーザーインターフェイスの操作を自動化してテストしたり……みたいな感じに使えるっぽいですね。

スタンドアロンのバイナリになっているので、C# からはそれを叩いて、あらかじめ用意しておいたスクリプトを処理してもらう感じになるようです。なので、任意のプロセスを叩けない UWP で使うのは難しそうですね。今回は WPF でサンプルを作りましたが、WPF 要素は皆無です。

使い方

まず NuGet で PhantomJS をとってきます。

f:id:daruyanagi:20170321192456p:plain

すると phantomjs.exe というのがソリューションに追加されます。これはコンパイル時に出力フォルダーにコピーされます。

f:id:daruyanagi:20170321193813p:plain

次に、JavaScript を用意します。今回はソリューションフォルダーのルートに Hello.js を作成。

console.log('Hello, world!');
phantom.exit();

最初なので、動作確認をするだけです。これも phantomjs.exe と同様、コンパイル時に出力フォルダーへコピーされるようにしておけばいいと思います。

次は、これを呼ぶための C# コードを書きます。標準出力でやり取りする感じにしてみました。

public MainWindow()
{
    InitializeComponent();

    Loaded += MainWindow_Loaded;
}

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    var result = ProcessScript("hello.js");

    System.Diagnostics.Debug.WriteLine(result);
}

private string ProcessScript(string script, params string[] args)
{
    using (var process = new System.Diagnostics.Process())
    {
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardInput = false;
        process.StartInfo.FileName = "phantomjs.exe";
        process.StartInfo.Arguments = $"{script} {string.Join(" ", args)}";

        process.Start();
        var result = process.StandardOutput.ReadToEnd();
        process.WaitForExit();

        System.Diagnostics.Debug.WriteLine($"ProcessScript() -> Code {process.ExitCode}: {process.ExitTime - process.StartTime} has elapsed.");

        return result;
    }
}

本当はパスの管理とかもう少し何とかしないとだめかもしれませんけど、まぁ、いいや。

とりあえず、実行してみましょう。

f:id:daruyanagi:20170321194934p:plain

> ProcessScript() -> Code 0: 00:00:01.7818442 has elapsed.
> Hello, world!

いい感じになってる気がします。今度はちょっと複雑にしてみます。といっても、a.href の値を配列で受け取るだけです。

// MainWindow.cs

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    var result = ProcessScript("hello.js", "なんか URL");

    System.Diagnostics.Debug.WriteLine(result);
}
// hello.js

var system = require('system');

// 引数のチェック
switch(system.args.length) {
    case 2:
        var url = system.args[1];
        break;
    default:
        console.log('error: invalid argument');
        phantom.exit(1);
        break;
}

var page = require('webpage').create();

// Web Page を開く
page.open(url, function (status) {
    switch (status) {
        case 'success':
            var links = page.evaluate(function () {
                return [].map.call(document.querySelectorAll('a'), function (link) { return link.getAttribute('href'); });
            });
            console.log(JSON.stringify(links));
            phantom.exit(0);
            break;
        default:
            console.log('error: page.open() ' + status);
            phantom.exit(1);
            break;
    }
});

f:id:daruyanagi:20170321195239p:plain

とりあえず動いている気がします。JavaScript がよくわからないのが困ったちゃんですが、まぁ、おいおいマスターしていけばいいよね。これでいろんなことに使える気がしてきました。

*1:ヘッドレスっていうらしい