MAGAZINE

ルーターマガジン

RPA

Chrome DevTools Protocol(CDP)のDispatchMouseEventを使ってGoogleChrome上のマウスを自動操縦する

2024.02.21
Pocket

マウスの自動操縦は最も人間らしい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
)

マウスカーソル移動の実装例

mouse_move

マウスポインタを可視化した上で、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

マウス左クリックの実装例

mouse_click

カテゴリ-要素のx,y座標を取得しながらを順にクリックすると、このような動きが実現できます。 コードは長いので末尾に記載します。

スクロールの実装の仕方

Input.synthesizeScrollGesture を使うことでスクロールも実装出来ます。xDistanceが負の値で右方向、yDistanceが負の値で下方向にスクロールします。

def scroll(x,y)
  @chrome.send_cmd(
    'Input.synthesizeScrollGesture',
    x:          @x,
    y:          @y,
    xDistance:  -x,
    yDistance:  -y
  )
end

スクロールの実装例

mouse_scroll

このスクロールを実現するコードは以下です。

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
Pocket

CONTACT

お問い合わせ・ご依頼はこちらから