.NET CoreとSeleniumでHeadless Chrome操作するよ

プログラムからブラウザを操作したい、まあ、よくある要求ですな。
そのための2018年現在の定番の方法というのが、[Selenium](https://www.seleniumhq.org/)というやつらしいのであります。
他の人は他の言語でやっていることも多いようなのですが、私はC#でやってみることにします。ついでに、いかにもマルチプラットフォームっぽい.NET Coreで。

# 開発環境
* Windows 10 Home 64bit version 1709(Fall Creators Update)
* [Visual Studio Community 2017](https://www.visualstudio.com/ja/downloads/) version 15.6
* [Google Chrome](https://www.google.co.jp/chrome/index.html) version 66

たぶんもっと新しくても大丈夫。より古いとか、32bitとかだとだめかもしれない。

# プロジェクト作成
![新しいプロジェクトの作成画面だ！](https://qiita-image-store.s3.amazonaws.com/0/212471/1578feaf-5d03-9c06-3b69-07cfa545c499.png)
とりあえず、.NET Coreのプロジェクトを作成します。
一覧に出てこないって人は、Visual Studio Installerで「変更」ボタンを押して、下のほうにある「.NET Core クロスプラットフォーム開発」にチェックを入れてインストールしよう！
ここは用途に応じた種類を選べばいいと思うんですが、今回は手っ取り早く試したいので、「コンソール アプリ」を選びます。

デフォルトでなっていると思いますが、一応、ターゲットプラットフォームが「.NET Core 2.0」になっていることを確認しておきます。

# Seleniumを入れる
こっからSeleniumと[ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/)を入れるのですが、Visual Studioではわりかし楽ちんです。

![ソリューションエクスプローラーだ！](https://qiita-image-store.s3.amazonaws.com/0/212471/361cfc8b-582a-c04e-876c-909104c74fcd.png)
依存関係のところを右クリックして「NuGet パッケージの管理」を出して、「Selenium」で検索します。

![Seleniumの検索結果だ！](https://qiita-image-store.s3.amazonaws.com/0/212471/8a1072fb-b9e4-4b22-6064-48e5dde484e7.png)
Selenium関係のやつがずらずらと出てくるので、「Selenium.WebDriver」と「Selenium.WebDriver.ChromeDriver」をインストールします。
最新のブラウザに対応させたいので、Seleniumもなるべく新しいのにしておきます。世間的には1とか2とかあるらしいですが、今回インストールするのは、2018年4月現在の最新版Selenium3.11.2と、ChromeDriver2.38.0.1です。
将来、Google Chrome本体がアップデートするとSeleniumが動かなくなる可能性がありますが、そのときはChromeDriverもアップデートしましょう。

# SeleniumでChromeを操作する

## 普通に開いてみる
基本は、ChromeDriverをnewしてUrlを突っ込んで、最後にQuitします。
ChromeDriverのファイルは出力ディレクトリにコピーされるので、オブジェクト生成時にディレクトリを明示的に指定してやります。パスの通った場所に置いておくというのもありです。
(2018/5/8修正　ディレクトリの指定をカレントディレクトリからアセンブリのディレクトリに変更)

```C#:Program.cs
using System;
using System.IO;
using System.Reflection;
using OpenQA.Selenium.Chrome;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            // ChromeDriverオブジェクトを生成します。
            var chrome = new ChromeDriver(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location));
            // URLに移動します。
            chrome.Url = @"http://www.opoona.com/";
            // すぐ終了しないよう、キーが押されるまで待機します。
            Console.ReadKey();
            // ブラウザを閉じます。
            chrome.Quit();
        }
    }
}
```

![Google Chromeが開いてWii版オプーナの公式サイトが表示された！](https://qiita-image-store.s3.amazonaws.com/0/212471/66d4e61a-387a-f6ea-03ae-6f8865f0bd32.png)

普通に呼び出しただけなので、当然Chromeの画面が出てきます。
何かエラーが出てるし、Flashが無効で見た目も残念ですが、ちゃんと開けてますね。

QuitじゃなくてDisposeでも終了できるからusing句で囲ってもいいとか、Urlプロパティを指定する代わりにNavigate().GoToUrlするとか、書き方はいろいろありそうです。

```C#
using (var chrome = new ChromeDriver(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)))
{
    chrome.Navigate().GoToUrl(@"http://www.opoona.com/");
    Console.ReadKey();
}
```

## Headless――画面表示なし――
さて、画面が出てくると、自動処理、自動テストには都合が悪いですね。
なので、起動オプションに「--headless」を付けて、UIの表示を行わないヘッドレスモードで起動します。

```C#:Program.cs
using System;
using System.IO;
using System.Reflection;
using OpenQA.Selenium.Chrome;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            // ChromeOptionsオブジェクトを生成します。
            var options = new ChromeOptions();
            // --headlessを追加します。
            options.AddArgument("--headless");
            // ChromeOptions付きでChromeDriverオブジェクトを生成します。
            var chrome = new ChromeDriver(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), options);
            // URLに移動します。
            chrome.Url = @"http://www.opoona.com/";
            // タイトルを表示します。
            Console.WriteLine(chrome.Title);
            // すぐ終了しないよう、キーが押されるまで待機します。
            Console.ReadKey();
            // ブラウザを閉じます。
            chrome.Quit();
        }
    }
}
```

![Webページのタイトルが表示された！](https://qiita-image-store.s3.amazonaws.com/0/212471/991bb44e-acf9-2ebb-d9d7-3d43a5510252.png)
画面が出なくて動いてるかどうかわかんなくなるので、とりあえずタイトルも表示しときました。
ヘッドレスモードが登場した当初は[「--disable-gpu」オプションも必須](https://developers.google.com/web/updates/2017/04/headless-chrome?hl=ja)だったらしいのですが、今は必要ないみたいです。いらなくなりましたっていうアナウンスを見たことがないので、うちの環境でたまたまうまくいっただけかもしれませんが。この辺よく知ってる人がいたら教えてください。ちなみにエラーが出てますが、「--disable-gpu」オプションの有無とは関係なく出ているようです。

# Linux環境でもやってみる
さて、ここまでできれば、あとはChromeDriverオブジェクトのメソッドなりプロパティなりを使って自動処理なりテストなり好きにできるかと思うんですが、せっかくの.NET Coreなので、Windows以外の環境でもやってみたいですよね。
幸い、Windows 10には、WSL(Windows Subsystem for Linux)という便利なものがあります。
これを使ってみましょう。

## WSLの有効化とUbuntuのインストール
WSLで使えるLinuxディストリビューションはいくらかあるようですが、ここは深く考えずにUbuntuを使います。

設定のどっかでWSLを有効化して再起動して、ストアアプリで「Ubuntu」を検索して入れときましょう。ずいぶん前にやったことなので、細かいことは忘れました。
ストアのレビューにはおっかないこと書いてる人がいますが、2017年9月までのレビューは全部ベータ版だったころの古いレビューなので、恐れずインストールしましょう。

## .NET Coreのインストール
以下のサイトの手順に従ってインストールします。

* [.NET and C# - Get Started in 10 Minutes](https://www.microsoft.com/net/learn/get-started/linux/ubuntu16-04)

```sh
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main" > /etc/apt/sources.list.d/dotnetdev.list'

sudo apt-get install apt-transport-https
sudo apt-get update
sudo apt-get install dotnet-sdk-2.1.105
```

## Google Chromeのインストール
以下のサイトを参考にインストールしてみます。基本的に2つ目のサイトのコマンドでインストールできるんですが、自動更新に関してはちょっと気を付けたほうがいいかも(よくわかってない)。

* [パソコン版 Chrome](https://www.google.co.jp/chrome/index.html)
* [Install Google Chrome on WSL (Ubuntu) on Windows 10 Pro | 東京大学 佐々木淳 研究室 沿岸環境学 海岸工学 環境水工学 水環境学](http://estuarine.jp/2017/12/install-google-chrome-on-wsl/)

```sh
sudo apt install gdebi
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo gdebi google-chrome-stable_current_amd64.deb
```

なお、今回はHeadlessで動かすので、X Windowとかのインストールは必要ありません。

## さっき作ったプログラムを動かしてみる
さっき作ったやつがあるのでWSLでリビルドして実行してみます。パスは自分で作った場所に応じて変更してください。

```sh
cd /mnt/o/poona/ConsoleApp1/ConsoleApp1
dotnet clean
dotnet run
```

やっている内容としては、プロジェクトのディレクトリに移動して、同じソースからリビルドして実行しているだけです。
プログラム本体はリビルドも必要ないようですが、ChromeドライバーがOSごとに必要になるので、そのためにリビルドが必要になるようです。
(2018/5/8修正　ディレクトリ指定方法変更に伴うコマンドのシンプル化)

# この記事の更新履歴
* 2018/5/3　初版
* 2018/5/8　ディレクトリ指定方法変更