websocket+chrome devtools protocolでスクリーンショットを取得する

# やりたいこと
- プリインストールされたchrome(系ブラウザ)のスクショを取りたい
- webインスペクタを開くためのURLは分かっている
- 諸般の事情でseleniumなどの手段が使えない

要は非Androidのテレビデバイスとか、そういう特殊環境で強引にスクショを取る方法です


# やったこと

## webSocketDebuggerUrlを取得する
> webインスペクタを開くためのURLは分かっている

このURLをまず確認する
※webSocketDebuggerUrlは同時に1セッションしか接続できないので、ブラウザでウェブインスペクタを開いている場合は閉じておく

```shell
$ curl 192.168.1.10:00000/json    # webインスペクタのURLの末尾に '/json' をつける
# ↓ブラウザを起動していないとき blankのタブが常に控えていることがわかる
[ {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=192.168.1.10:00000/devtools/page/049cc469-5488-4932-ab06-daabe219c5a2",
   "id": "049cc469-5488-4932-ab06-daabe219c5a2",
   "title": "about:blank",
   "type": "page",
   "url": "about:blank",
   "webSocketDebuggerUrl": "ws://192.168.1.10:00000/devtools/page/049cc469-5488-4932-ab06-daabe219c5a2"
} ]

# ↓ブラウザを起動したとき blankタブを押しのけて新しいタブが生成される (多分端末によって挙動違うので都度読み替えてください)
[ {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=192.168.1.10:00000/devtools/page/c4a5735e-dee8-4262-9dca-15058f7a821e",
   "id": "c4a5735e-dee8-4262-9dca-15058f7a821e",
   "title": "https://www.youtube.com",
   "type": "page",
   "url": "https://www.youtube.com",
   "webSocketDebuggerUrl": "ws://192.168.1.10:00000/devtools/page/c4a5735e-dee8-4262-9dca-15058f7a821e"
}, {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=192.168.1.10:00000/devtools/page/4fb84849-f023-4014-ba14-d76bd2d9c4bf",
   "id": "4fb84849-f023-4014-ba14-d76bd2d9c4bf",
   "title": "about:blank",
   "type": "page",
   "url": "about:blank",
   "webSocketDebuggerUrl": "ws://192.168.1.10:00000/devtools/page/4fb84849-f023-4014-ba14-d76bd2d9c4bf"
} ]

```

#### キーの説明
- `devtoolsFrontendUrl` webインスペクタを開くためのURL デバイスのアドレスと合体させて `http://192.168.11.11:10485/devtools/inspector.html?ws= 192.168.1.10:00000/devtools/page/c4a5735e-dee8-4262-9dca-15058f7a821e` みたいにすることでアクセスできる
今回は使わない
- `id` セッションごとにランダムに生成される文字列 タブ1つずつに生成される
- `title` ウェブページのタイトル
- `type` `page`以外見たことがないので詳細は知らん
- `url` 現在開いているページのURL
- `webSocketDebuggerUrl` 本命 websocketでchrome devtools protocolを叩くためのURL


というわけでwebSocketDebuggerUrlを取得することができた


#### pythonで実装

```shell
$ pip install websocket-client requests
```
たまにある事例ではあるが、pipとpython内でパッケージの名前が異なるので一応注意


```ws.py
import websocket
import requests
from requests.exceptions import Timeout


DEVICE_ADDRESS = '192.168.1.10:00000'
TIMEOUT = 10.0    # こういう端末は得てしてリソースカツカツなのでタイムアウト大目に見る

try:
    response = requests.get(f'http://{DEVICE_ADDRESS}/json', timeout=TIMEOUT)
    ws_url = response.json()[0]['webSocketDebuggerUrl']
    print('ws_url: ', ws_url)

except requests.exceptions.Timeout as err:
    print(err)
```

```shell
$ python ws.py
ws_url:  ws://192.168.1.10:00000/devtools/page/c4a5735e-dee8-4262-9dca-15058f7a821e
```


## websocketで接続する


こんなかんじでやれそう

```ws.py
import websocket
import requests
from requests.exceptions import Timeout

import json


DEVICE_ADDRESS = '192.168.1.10:00000'
TIMEOUT = 10.0    # こういう端末は得てしてリソースカツカツなのでタイムアウト大目に見る

def get_ws_url():
    try:
        response = requests.get(f'http://{DEVICE_ADDRESS}/json', timeout=TIMEOUT)
        ws_url = response.json()[0]['webSocketDebuggerUrl']
        print('ws_url: ', ws_url)

    except requests.exceptions.Timeout as err:
        print(err)


def send(message):
    ws_url = get_ws_url(DEVICE_ADDRESS)

    ws = websocket.create_connection(ws_url)        # websocketでの接続を確立する
    ws.send(json.dumps(message))                    # 何らかのメッセージを送りつける
    receive = ws.recv()                             # 何らかのメッセージが帰ってくる
    ws.close()                                      # 接続を終了する

    return receive
```

#### websocketで端末に送りつけるための書式を調べる

chrome devtoolsは単なるwebインスペクタではなく、外部から操作できるプロトコルが存在するらしい
それがコレ
[chrome devtools protocol](https://chromedevtools.github.io/devtools-protocol/)
>The Chrome DevTools Protocol allows for tools to instrument, inspect, debug and profile Chromium, Chrome and other Blink-based browsers. Many existing projects currently use the protocol. The Chrome DevTools uses this protocol and the team maintains its API.

deeplによる翻訳:
>Chrome DevTools Protocolは、Chromium、Chrome、その他のBlinkベースのブラウザの計測、検査、デバッグ、プロファイルを行うためのツールを提供します。現在、多くの既存のプロジェクトがこのプロトコルを使用しています。Chrome DevToolsはこのプロトコルを使用しており、チームはそのAPIを維持しています。

なるほどね
ちなみに端末内のchromeバージョンは要確認
`chrome://version` に端末でアクセスし、対応するchrome devtools protocolのバージョンを確認する
(こちらの環境はchrome65だったので stable RC(1.3)というバージョンまでの機能が使えました)


テンプレはこんなかんじ

```python
message = {
    'id': 1,    # どういう意味があるのか分かってないのでとりあえず数字入れている 固定値でもなんも怒られないので謎
    'method':'chrome devtools protocolのメソッド',
    'params': {'パラメータのkey': 'パラメータのvalue'}
}
send(message)
```


たとえばjavascriptを実行するやつ
https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-evaluate

```python
def evaluate_js(script):
    message = {
        "id": 1,
        "method": "Runtime.evaluate",        # Runtime.evaluate を指定
        "params": {"expression": script}     # パラメータexpressionにjavascriptを直接代入する
    }
    send(message)
```
↑コレの引数に `location.reload()`などを文字列として渡せば動く


使用頻度の高そうなやつを追加するとこんなかんじ

```ws.py
import websocket
import requests
import json

from requests.exceptions.Timeout

import base64   # エンコードされた画像の処理

DEVICE_ADDRESS = '192.168.1.10:00000'
TIMEOUT = 10.0    # こういう端末は得てしてリソースカツカツなのでタイムアウト大目に見る

def get_ws_url():
    try:
        response = requests.get(f'http://{DEVICE_ADDRESS}/json', timeout=TIMEOUT)
        ws_url = response.json()[0]['webSocketDebuggerUrl']
        print('ws_url: ', ws_url)

    except requests.exceptions.Timeout as err:
        print(err)


def send(message):
    ws_url = get_ws_url(DEVICE_ADDRESS)

    ws = websocket.create_connection(ws_url)        # websocketでの接続を確立する
    ws.send(json.dumps(message))                    # 何らかのメッセージを送りつける
    receive = ws.recv()                             # 何らかのメッセージが帰ってくる
    ws.close()                                      # 接続を終了する

    return receive

def reload():
    """
    リロード
    """
    message = {
        'id': 1,
        'method': 'Runtime.evaluate',
        'params': {'expression': 'location.reload()'}
    }
    send(message)

def clear():
    """
    localStorageの消去
    """
    message = {
        'id': 1,
        'method': 'Runtime.evaluate',
        'params': {'expression': 'localStorage.clear()'}
    }
    send(message)

def evaluate_js(script):
    """
    javascriptの実行
    """
    message = {
        'id': 1,
        'method': 'Runtime.evaluate',
        'params': {'expression': script}
    }
    send(message)

def transition(url):
    """
    絶対URLで直遷移
    """
    message = {
        'id': 1,
        'method': 'Runtime.evaluate',
        'params': {'expression': f'location.href="{url}"'}
    }
    send(message)

def get_screenshot(image_path):
    """
    スクショ取得
    """
    message = {
        'id': 1,
        'method': 'Page.captureScreenshot',
        'params': {'format': 'png'}
    }
    receive = send(message)

    # レスポンスにbase64でエンコードされた画像が帰ってくる
    image_base64 = json.loads(receive)['result']['data']
    image = base64.b64decode(image_base64)
    with open(image_path, 'bw') as f:
        f.write(image)

def get_url():
    """
    現在のURL取得
    """
    response = requests.get(f'http://{DEVICE_ADDRESS}/json')
    url = response.json()[0]['url']
    return url

def get_element_text(xpath):
    """
    要素のテキストを取得
    """
    evaluate = f'document.evaluate("{xpath}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.textContent;'

    message = {
        'id': 1,
        'method': 'Runtime.evaluate',
        'params': {'expression': evaluate}
    }

    receive = send(message)

    print('ws get_element: receive', receive)
    try:
        text_element = json.loads(receive)['result']['result']['value']
    except KeyError:
        text_element = ''

    return text_element
```

```python
# 対話モード
>>>import ws
>>>
>>>ws.get_url()
>>>https://www.youtube.com
>>>
>>>ws.reload()    # 端末実機のページが更新される
>>>
>>>ws.get_screenshot('test.png')    # 端末のSSがカレントディレクトリに保存される
```


# まとめ
- websocket経由でchromeに接続し、スクショを保存することができた
- javascriptの実行やログの取得など、webインスペクタでサポートされている一通りの機能が呼び出せることが分かった
- ドキュメントを見る感じパフォーマンス計測なども一応できるっぽい