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

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


##2Capthcaとは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631893/9245da6a-d16b-816e-efbe-dce2da9b2674.png)
CAPTHCA機能を突破するためのサービスで、APIを利用することで認証を自動化することができます。
有料サービスですが、reCAPTCHA v2なら1,000リクエストで$2.99と安いです。
念の為お断りを入れておきますが、私と2Captchaとの間に販促等での金銭のやりとりはございません。

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

##やりたいこと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631893/6c37da63-2bd1-34b1-eed9-096379a1e6ad.png)
[reCAPTCHAのデモページ](https://www.google.com/recaptcha/api2/demo)を突破します。
2Capthcaのアカウントの作成やapiキーの取得については[先人の記事]((https://qiita.com/derodero24/items/7d36f4617a40fbb36b11))をご参照ください。

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

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

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

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

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

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

```ruby:Gemfile
source "https://rubygems.org"
gem 'nokogiri'
gem 'chrome_remote'
```

```
bundle install
```

##rubyプログラム本体です。

```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
```

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