やりたいこと
- プリインストールされたchrome(系ブラウザ)のスクショを取りたい
- webインスペクタを開くためのURLは分かっている
- 諸般の事情でseleniumなどの手段が使えない
要は非Androidのテレビデバイスとか、そういう特殊環境で強引にスクショを取る方法です
やったこと
webSocketDebuggerUrlを取得する
webインスペクタを開くためのURLは分かっている
このURLをまず確認する
※webSocketDebuggerUrlは同時に1セッションしか接続できないので、ブラウザでウェブインスペクタを開いている場合は閉じておく
$ 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で実装
$ pip install websocket-client requests
たまにある事例ではあるが、pipとpython内でパッケージの名前が異なるので一応注意
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)
$ python ws.py
ws_url: ws://192.168.1.10:00000/devtools/page/c4a5735e-dee8-4262-9dca-15058f7a821e
websocketで接続する
こんなかんじでやれそう
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
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)というバージョンまでの機能が使えました)
テンプレはこんなかんじ
message = {
'id': 1, # どういう意味があるのか分かってないのでとりあえず数字入れている 固定値でもなんも怒られないので謎
'method':'chrome devtools protocolのメソッド',
'params': {'パラメータのkey': 'パラメータのvalue'}
}
send(message)
たとえばjavascriptを実行するやつ
https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-evaluate
def evaluate_js(script):
message = {
"id": 1,
"method": "Runtime.evaluate", # Runtime.evaluate を指定
"params": {"expression": script} # パラメータexpressionにjavascriptを直接代入する
}
send(message)
↑コレの引数に location.reload()
などを文字列として渡せば動く
使用頻度の高そうなやつを追加するとこんなかんじ
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
# 対話モード
>>>import ws
>>>
>>>ws.get_url()
>>>https://www.youtube.com
>>>
>>>ws.reload() # 端末実機のページが更新される
>>>
>>>ws.get_screenshot('test.png') # 端末のSSがカレントディレクトリに保存される
まとめ
- websocket経由でchromeに接続し、スクショを保存することができた
- javascriptの実行やログの取得など、webインスペクタでサポートされている一通りの機能が呼び出せることが分かった
- ドキュメントを見る感じパフォーマンス計測なども一応できるっぽい