MAGAZINE
ルーターマガジン
Chrome DevTools Protocol(CDP)のDispatchMouseEventを使ってGoogleChrome上のマウスを自動操縦する
マウスの自動操縦は最も人間らしいBOT
GoogleChromeの自動操縦の中でも最も人間に近い動きとなる、マウスの自動操縦を本記事で解説します。 本記事では以下の3つを自動化します。
- マウスカーソル移動
- 左クリック
- スクロール
また、本記事ではrubyのgemであるchrome_remoteを利用します。chrome_remoteの基本的な使い方は以下を参考にしてください。
chrome_remoteという選択(脱Selenium大作戦)
マウスカーソル移動の実装の仕方
以下のように、Input.dispatchMouseEventをChromeに送ります。xとyは移動先の座標です。座標は左上が始点(0,0)になっており、右、下にいくほど大きくなります。一度の送信で指定座標にマウスを一瞬で移動するので、人間らしい動きを実現するためには、これを複数回送信して軌跡を描く必要があります。
require 'chrome_remote'
@chrome = ChromeRemote.client
@chrome.send_cmd(
'Input.dispatchMouseEvent',
type: 'mouseMoved',
x: x,
y: y
)
マウスカーソル移動の実装例
マウスポインタを可視化した上で、ChromeDevtoolsProtocolのInput.dispatchMouseEventでマウスを移動させると、このような動きが実現出来ます。 この動きは以下のコードで実現出来ます。
require 'chrome_remote'
@chrome = ChromeRemote.client
# マウスポインターの可視化
add_pointer_js = "window.addEventListener('pointermove', ev => {
const el = document.createElement('div');
Object.assign(el.style, {
width : '10px',
height : '10px',
backgroundColor : 'red',
borderRadius : '50%',
position : 'fixed',
left : `${ev.x}px`,
top : `${ev.y}px`,
opacity : '1',
transition : 'opacity .3s',
'z-index' : '10000',
'pointer-events': 'None',
});
document.body.appendChild(el);
setTimeout(() => {
el.style.opacity = '0';
setTimeout(() => el.remove(), 300);
}, 500);
});"
@chrome.send_cmd 'Runtime.evaluate', expression: add_pointer_js
# マウスポインターで円を描く
(0..60).cycle do |i|
x = 200 + Math.sin(Math::PI/30*i)*100
y = 200 + Math.cos(Math::PI/30*i)*100
@chrome.send_cmd(
'Input.dispatchMouseEvent',
type: 'mouseMoved',
x: x,
y: y
)
end
マウス左クリックの実装の仕方
マウスの左ボタンを押し込む
Input.dispatchMouseEvent のtypeをmousePressedにしてGoogleChromeに送信すると、マウスのボタンを押し込みます。
def down
@chrome.send_cmd(
'Input.dispatchMouseEvent',
type: 'mousePressed',
button: 'left',
x: @x,
y: @y,
clickCount: 1
)
end
マウスの左ボタンを離す
Input.dispatchMouseEvent のtypeをmouseReleasedにしてGoogleChromeに送信すると、マウスのボタンを離します。
def up
@chrome.send_cmd(
'Input.dispatchMouseEvent',
type: 'mouseReleased',
button: 'left',
x: @x,
y: @y,
)
end
これら2つの処理を組み合わせてクリックする
クリックしたい場所にカーソルを移動させてから左ボタンを押し込んでから一定時間後に離すことでクリックを実現します。
def click(x, y)
move(x, y)
down
sleep(0.002)
up
end
マウス左クリックの実装例
カテゴリ-要素のx,y座標を取得しながらを順にクリックすると、このような動きが実現できます。 コードは長いので末尾に記載します。
スクロールの実装の仕方
Input.synthesizeScrollGesture を使うことでスクロールも実装出来ます。xDistanceが負の値で右方向、yDistanceが負の値で下方向にスクロールします。
def scroll(x,y)
@chrome.send_cmd(
'Input.synthesizeScrollGesture',
x: @x,
y: @y,
xDistance: -x,
yDistance: -y
)
end
スクロールの実装例
このスクロールを実現するコードは以下です。
require 'chrome_remote'
def scroll(x,y)
@chrome.send_cmd(
'Input.synthesizeScrollGesture',
x: @x,
y: @y,
xDistance: -x,
yDistance: -y
)
end
@chrome = ChromeRemote.client
@x = 0
@y = 0
# 株式会社ルーターのニュースに遷移
@chrome.send_cmd 'Page.navigate', url: 'https://rooter.jp/news/'
sleep 3
# スクロール
scroll(0, 3000)
まとめ
カーソル移動、クリック、スクロールまで実装出来るマウスの自動操縦は、最も自由度が高く最も人間らしいBOTと言えるでしょう。弊社での通常のスクレイピング業務ではcurlのようなリクエストの再現のみで実装することが多いですが、リクエストの再現が難しい場合にはGoogleChromeやマウスの自動操縦が候補に上がります。 その自由度の高さ故にコードが冗長になりやすいですが、rubyのgemであるFerrumを使うとある程度は緩和されるでしょう。Ferrumについては以下で解説しているのでぜひお役立てください。
Webブラウザ自動操縦ライブラリchrome_remoteの後継のFerrumの使い方
▼マウス左クリックの実装例のコード
require 'chrome_remote'
require 'nokogiri'
def move(x, y)
steps = [ (x-@x).abs, (y-@y).abs].max / 10
1.upto(steps) do |i|
@chrome.send_cmd(
'Input.dispatchMouseEvent',
type: 'mouseMoved',
button: 'left',
x: @x + (x - @x) * (i / steps.to_f),
y: @y + (y - @y) * (i / steps.to_f)
)
end
@x = x
@y = y
end
def click(x, y)
move(x, y)
down
sleep(0.002)
up
end
def click_selector(selector)
x, y = selector_to_xy(selector)
click(x, y)
end
def down
@chrome.send_cmd(
'Input.dispatchMouseEvent',
type: 'mousePressed',
button: 'left',
x: @x,
y: @y,
clickCount: 1
)
end
def up
@chrome.send_cmd(
'Input.dispatchMouseEvent',
type: 'mouseReleased',
button: 'left',
x: @x,
y: @y,
)
end
def selector_to_xy(selector)
script = <<~JS
var input = document.querySelector("#{selector}");
var box = input.getBoundingClientRect();
JSON.stringify([ box.left, box.right, box.top, box.bottom ]);
JS
result = @chrome.send_cmd("Runtime.evaluate", expression: script)
result_val = result["result"]["value"]
return nil if result_val.nil?
left, right, top, bottom = JSON.parse(result_val)
x = rand(left..right)
y = rand(top..bottom)
[x, y]
end
def visualize_mouse_pointer
add_pointer_js = "window.addEventListener('pointermove', ev => {
const el = document.createElement('div');
Object.assign(el.style, {
width : '10px',
height : '10px',
backgroundColor : 'red',
borderRadius : '50%',
position : 'fixed',
left : `${ev.x}px`,
top : `${ev.y}px`,
opacity : '1',
transition : 'opacity .3s',
'z-index' : '10000',
'pointer-events': 'None',
});
document.body.appendChild(el);
setTimeout(() => {
el.style.opacity = '0';
setTimeout(() => el.remove(), 300);
}, 500);
});"
@chrome.send_cmd 'Runtime.evaluate', expression: add_pointer_js
end
@chrome = ChromeRemote.client
# マウスポインターをインスタンス変数に保持
@x = 0
@y = 0
# 株式会社ルーターのTOPに遷移
@chrome.send_cmd 'Page.navigate', url: 'https://rooter.jp/'
sleep 3
# マウスポインターを可視化
visualize_mouse_pointer()
# HTMLを取得
response = @chrome.send_cmd 'Runtime.evaluate', expression: 'document.documentElement.outerHTML'
html = response['result']['value']
# HTMLをNokogiriでパース
doc = Nokogiri::HTML.parse(html)
# カテゴリごとに繰り返し処理
doc.css('nav.l-header-nav>ul>li>a').each do |category_node|
# Nokogiri::XML::Nodeからcss_pathを取得し、javascriptでx,y 座標に変換
x,y = selector_to_xy(category_node.css_path)
# x,y座標をクリック
click(x, y)
sleep 3
visualize_mouse_pointer()
end
CONTACT
お問い合わせ・ご依頼はこちらから