『CAPTCHA』を突破するサービス『2Captcha』とRuby+Chrome_Remoteで自動スクレイピング


はじめに

スクレイピングしていると、CAPTCHAが出てプログラムが止まった経験、あると思います。
(そういう方しかこの記事は見ませんよね。)
なんとかCAPTHCAを回避するために、BOTっぽくない動きをさせたり、IP分散という手もありますが、今回は素直にCAPTCHAを解いてやろうと思います。
もちろん、エンジニアなので自分の手で解くより、プログラム上で自動的に解かせたいですよね。
機械学習は学習コストや導入コストが高く、もっと楽したいです。
2Cpathcaというサービスがそれを叶えてくれます。
他にも色々サービスがあるので、自分にあったものを見つけてください。
Pythonの記事はあったのですが、Rubyの記事は見つからなかったので書きました。

2Capthcaとは


CAPTHCA機能を突破するためのサービスで、APIを利用することで認証を自動化することができます。
有料サービスですが、reCAPTCHA v2なら1,000リクエストで$2.99と安いです。
念の為お断りを入れておきますが、私と2Captchaとの間に販促等での金銭のやりとりはございません。

Chrome_Remoteとは

ChromeのインスタンスをRubyから操作できるライブラリです。
詳しい使い方は解説ページリポジトリをご参照ください。
スクレイピングする前提として、そもそもCAPTHCAが出にくいようにしてやる必要があります。
Selenium等と違ってChromeをそのまま動かすChrome_RemoteのほうがBOT判定されにくいと思います。(そのうち違いを検証したい。)

やりたいこと


reCAPTCHAのデモページを突破します。
2Capthcaのアカウントの作成やapiキーの取得については先人の記事をご参照ください。

『2Captcha』とRuby+Chrome_RemoteでreCAPTHCAを突破

2Captchaのapiキーを取得して、ファイル保存しておきます。

key.yaml
---
:2Capthca: 2Captchaのapiキー

Chromeをdebugging-portつきで起動します。

Macの場合
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 &

必要なGemをインストールします.

Gemfile
source "https://rubygems.org"
gem 'nokogiri'
gem 'chrome_remote'
bundle install

rubyプログラム本体です。

crawler.rb

require 'nokogiri'
require 'chrome_remote'
require 'yaml'

class CaptchaDetectedException < StandardError; end

class ChromeController
  def initialize
    @chrome = ChromeRemote.client

    # Enable events
    @chrome.send_cmd "Network.enable"
    @chrome.send_cmd "Page.enable"
  end

  def open(url)
    # ページアクセス
    move_to url
    captcha_detect
  end

  def reload_page
    sleep 1
    @chrome.send_cmd "Page.reload", ignoreCache: false
    wait_event_fired
  end

  def execute_js(js)
      @chrome.send_cmd "Runtime.evaluate", expression: js
  end

  def wait_event_fired
      @chrome.wait_for "Page.loadEventFired"
  end

  # ページ移動
  def move_to(url)
    sleep 1
    @chrome.send_cmd "Page.navigate", url: url
    wait_event_fired
  end

  # HTMLを取得
  def get_html
    response = execute_js 'document.getElementsByTagName("html")[0].innerHTML'
    html = '<html>' + response['result']['value'] + '</html>'
  end

  def captcha_detect
    bot_detect_cnt = 0
    begin
      html = get_html
      raise CaptchaDetectedException, 'captchaが確認されました' if html.include?("captcha")
    rescue CaptchaDetectedException => e
      p e
      bot_detect_cnt += 1
      p "captcha突破試行: #{bot_detect_cnt}回目"
      doc = Nokogiri::HTML.parse(html, nil, 'UTF-8')
      return if captcha_solve(doc) == '解除成功'
      reload_page
      retry if bot_detect_cnt < 3
      p 'captcha突破エラー。Rubyを終了します'
      exit
    end
    p 'captchaはありませんでした'
  end

  def captcha_solve(doc)
    id = request_id(doc).match(/(\d.*)/)[1]
    solution = request_solution(id)
    return false unless solution
    submit_solution(solution)
    p captcha_result
  end

  def request_id(doc)
    # APIキーの読み込み
    @key = YAML.load_file("key.yaml")[:"2Capthca"]
    # data-sitekey属性の値を取得
    googlekey = doc.at_css('#recaptcha-demo')["data-sitekey"]
    method = "userrecaptcha"
    pageurl = execute_js("location.href")['result']['value']
    request_url="https://2captcha.com/in.php?key=#{@key}&method=#{method}&googlekey=#{googlekey}&pageurl=#{pageurl}"
    # captcha解除を依頼
    fetch_url(request_url)
  end

  def request_solution(id)
    action = "get"
    response_url = "https://2captcha.com/res.php?key=#{@key}&action=#{action}&id=#{id}"
    sleep 15
    retry_cnt = 0
    begin
      sleep 5
      # captcha解除コードを取得
      response_str = fetch_url(response_url)
      raise 'captcha解除前' if response_str.include?('CAPCHA_NOT_READY')
    rescue => e
      p e
      retry_cnt += 1
      p "リトライ:#{retry_cnt}回目"
      retry if retry_cnt < 10
      return false
    end
    response_str.slice(/OK\|(.*)/,1)
  end

  def submit_solution(solution)
    # 解除コードを所定のtextareaに入力
    execute_js("document.getElementById('g-recaptcha-response').innerHTML=\"#{solution}\";")
    sleep 1
    # 送信ボタンクリック
    execute_js("document.getElementById('recaptcha-demo-submit').click();")
  end

  def captcha_result
    sleep 1
    html = get_html
    doc = Nokogiri::HTML.parse(html, nil, 'UTF-8')
    doc.at_css('.recaptcha-success') ? '解除成功' : '解除失敗'
  end


  def fetch_url(url)
    sleep 1
    `curl "#{url}"`
  end

end

crawler = ChromeController.new
url = 'https://www.google.com/recaptcha/api2/demo'
crawler.open(url)

プログラムを実行すると、reCAPTCHAのデモページにアクセスして、CAPTCHAの突破を試みます。

bundle exec ruby crawler.rb

最後に

スクレイピングを行う目的や態様、スクレイピングで得たデータの取り扱い方によっては、著作権法、個人情報保護法に抵触してしまう恐れがあります。
皆さんに楽しいスクレイピングライフがあることを祈ります。