curl コマンドでブラウザを操作する

## まえがき

ブラウザを操作する方法と言えば，もともと Selenium によって開発された WebDriver と各種プログラミング言語で用意された Selenium のライブラリを使用して行うものが思い浮かびます，ライブラリを使用しているときは WebDriver に実際にどのような操作がされているのかをイメージすることはあまりないと思います．
今回は
- Google Chrome を開く
- Google のトップページを開く
- 検索バーに `selenium` と入力する
- フォームを送信する
- ブラウザを閉じる

という操作を，プログラミング言語を使わず直接 WebDriver にリクエストを送る方法でやってみます．

## 環境

- Google Chrome

ブラウザ右上の縦ポチ 3 つのアイコンから「ヘルプ(H)」→「Google Chrome について(G)」と進むと表示されます．

```text
バージョン: 80.0.3987.122（Official Build） （64 ビット）
```

- ChromeDriver

お手持ちの Google Chrome に合ったものを https://chromedriver.chromium.org/downloads からダウンロードして解凍します．メジャーバージョンが合っていれば大抵動くので問題ありません．

解凍して出てきたディレクトリに移動し，バージョン確認をしてみます．

```text
$ ./chromedriver.exe --version
ChromeDriver 80.0.3987.106 (f68069574609230cf9b635cd784cfb1bf81bb53a-refs/branch-heads/3987@{#882})
```

- ターミナル

curl コマンドが実行できるものをご用意ください．

## からくり

chromedriver.exe を起動すると，下記のようにサーバが立ち上がるので，そこに JSON を送ることでセッションの作成（ブラウザの起動）・ URL へのアクセス・文字入力・ボタンクリックなどを行えます．
下記の例では localhost の 9515 番ポートにサーバが立ち上がっています．

```text
$ ./chromedriver.exe
Starting ChromeDriver 80.0.3987.106 (f68069574609230cf9b635cd784cfb1bf81bb53a-refs/branch-heads/3987@{#882}) on port 9515
Only local connections are allowed.
Please protect ports used by ChromeDriver and related test frameworks to prevent access by malicious code.
```

---

## 実際に操作してみる

前項でサーバを立ち上げた状態でもう一つターミナルを起動し，そちらで操作します．
コマンドは下記のワイヤープロトコルに従って記述します．

https://www.w3.org/TR/webdriver/

### セッションを作成する（Google Chrome を開く）

参考：https://www.w3.org/TR/webdriver/#new-session-0

```sh
curl http://localhost:9515/session \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"capabilities":{"alwaysMatch":{"goog:chromeOptions":{"args": ["--headless"]}}}}'
```

capabilities はブラウザの起動オプションにあたるもので，ヘッドレスで起動したり自己署名証明書の警告を無条件に受け入れる設定などがあります．
今回はヘッドレスで起動してみます．

コマンドを実行するとブラウザが開き，次のような応答が返ってきます．

```json
{
    "value":{
        "capabilities":{
            "acceptInsecureCerts":false,
            "browserName":"chrome",
            "browserVersion":"80.0.3987.122",
            "chrome":{
                "chromedriverVersion":"80.0.3987.106 (f68069574609230cf9b635cd784cfb1bf81bb53a-refs/branch-heads/3987@{#882})",
                "userDataDir":"C:\\Users\\username\\AppData\\Local\\Temp\\scoped_dir9260_2017363770"
            },
            "goog:chromeOptions":{
                "debuggerAddress":"localhost:62291"
            },
            "networkConnectionEnabled":false,
            "pageLoadStrategy":"normal",
            "platformName":"windows",
            "proxy":{},
            "setWindowRect":true,
            "strictFileInteractability":false,
            "timeouts":{
                "implicit":0,
                "pageLoad":300000,
                "script":30000
            },
            "unhandledPromptBehavior":"dismiss and notify"
        },
        "sessionId":"7f26a24e89769a56a60848b8c7e29352"
    }
}
```

一番下の sessionId がこのブラウザを操作するために必要ですので，変数に代入しておきます．

```sh
SESSIONID=7f26a24e89769a56a60848b8c7e29352
```

ヘッドレスで起動していますが， `goog:chromeOptions.debuggerAddress` の値にアクセスすれば実際にブラウザでどのような表示になっているか確認できます．試しに `localhost:62291` をブラウザで開き， `data:,` のリンクをクリックします．

![browser_debug.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/b69fcabc-19af-7f0b-c52f-0ab605c62345.png)

今は空白のページが表示されているということがわかりました．

### Google のトップページを開く

参考：https://www.w3.org/TR/webdriver/#navigate-to

```sh
curl http://localhost:9515/session/$SESSIONID/url \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"url": "https://www.google.co.jp/"}'
```

URL にアクセスするだけなので，応答は null が返ってきます．

```json
{"value":null}
```

デバッガ―では Google の検索ページが表示されています．

![googletop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/6004f96d-e70c-cba2-5302-de5830c9ba4a.png)

### 検索バーに `selenium` と入力する

まずは検索バーを `[name="q"]` という CSS セレクタで取得します．

参考：
https://www.w3.org/TR/webdriver/#find-element
https://www.w3.org/TR/webdriver/#locator-strategies

```sh
curl http://localhost:9515/session/$SESSIONID/element \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"using": "css selector", "value": "[name=\"q\"]"}'
```

次のような応答が返ってきました．

```json
{
    "value":{
        "element-6066-11e4-a52e-4f735466cecf":"b4e179f1-d41b-446f-bcde-c01c2bd61170"
    }
}
```

`b4e179f1-d41b-446f-bcde-c01c2bd61170` の部分が Element ID です．
これを指定して，検索バーに `selenium` という文字列を送ります．

参考：https://www.w3.org/TR/webdriver/#element-send-keys

```sh
curl http://localhost:9515/session/$SESSIONID/element/b4e179f1-d41b-446f-bcde-c01c2bd61170/value \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"text": "selenium"}'
```

ブラウザでも検索バーに selenium という文字列が入力されました．

![searchbarinput.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/0f3b43b2-955d-6d1c-10dc-a7dd7bab20f0.png)

### フォームを送信する

本当は検索ボタンをクリックする動作にしたかったのですが， JSON なしで click のリクエストを送ると `invalid argument` となってしまい断念しました．
WebDriver では直接 Javascript を実行できるので，今回は Javascript を実行する方法でやってみたいと思います．

参考：https://www.w3.org/TR/webdriver/#dfn-execute-script

```sh
curl http://localhost:9515/session/$SESSIONID/execute/sync \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"script":"document.getElementsByName(\"f\")[0].submit()", "args":[]}'
```

検索結果が表示されました．

![searchresult.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/5af8b02b-8516-886a-cad1-ef4ed3b419ee.png)

### ブラウザを閉じる（セッションを終了する）

メソッドに DELETE を使用します．

参考：https://www.w3.org/TR/webdriver/#delete-session

```sh
curl http://localhost:9515/session/$SESSIONID -X DELETE
```

デバッグの接続が閉じられました．

![sessionclosed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/f6aa642c-ad26-9385-1deb-3ef1a083ae4d.png)

---

## あとがき

Selenium のライブラリが内部で WebDriver にどんなリクエストを投げているかがわかり，いい勉強になった気がします．
また， curl コマンドでの方法を知っておくと， Selenium を使用していてバグに当たった時，それがライブラリ由来なのか WebDriver 由来なのかを判断するのに役立ちます．

この記事を書いてから，シェルで操作している方が他にもいらっしゃったことに気づいたのですが，自分の学習のためと思って投稿させていただきました・・・