【初心者向け】クローラーでIndeed広告管理画面の指標を取得する(Ruby × selenium × headless-chrome)

#はじめに
##About me
こんにちは｡
｢Livesense not engineers Advent Calendar 2018｣の21日目を担当させて頂きます､shin_aと申します｡

現在新卒2年目で､[転職会議](https://jobtalk.jp/)のプロダクトマネージャー及びマーケターを担当しております｡

｢githubってなんですか?｣状態だった昨年の状態からは脱したものの､｢技術について､人(初心者)に伝えられる程何も学んでない!｣と焦っていた霜月の終わりに@inagakkieさんにお声がけ頂き､｢終わり良ければ全て良し!12月に追い込もう｣という意気込みで､参加させて頂くことにしました｡

##テーマ選択の背景
･webマーケティングは､やりだすとキリがないくらい､いくらでも人員がさける
･でも､マーケティング人材は基本的に人手不足
→｢botに働いて貰うのは不可避では?｣という発想でトライしてみることにしました
(※2018年12月現在、知りうる限り、広告管理用APIはIndeedに存在していない。)

上司に話して見たところ､
｢過去に同様の発想で自動化を試みたものの､メンテナーがいなくなって使われなくなった事例がある｣と言われ､モチベーションが一瞬消えかけましたが､
｢まあ､知らん｡やりたいことやるのがAdvent Calendarだし､その時はその時考えよう｣と気持ちを立て直し､チャレンジすることとしました｡
(上司も｢勉強にはなるだろうからいいんじゃない?｣とは言ってました(念のため))



#概要
##本稿の対象者
● レベル感
　　○ **何となくruby/railsを触ったことある**
　　　(プログラミングスクールなどで短期カリキュラムこなした位)
　　○ **クローラーとは何か､雰囲気は知ってる**
　　　(クローラーについて全くご存知なければ､宜しければ[こちら](https://qiita.com/shin_a/items/3745344f14559d6bc4bf)もご覧頂けたら嬉しいです)

● 読む目的
　　○ **headless-chrome**がどんなものか知りたい
　　○ **selenium**がどんなものか知りたい
　　○ 広告管理画面に表示される指標をbotに取得させたい

##本稿のゴール
- 下記を実行できるクローラーを作成する
    - Indeedの管理画面に自動でログインする
    - 管理画面から､主要な広告指標(表示回数やクリック数など)をキャンペーン単位で取得する

(本当は､取得したデータをDBに突っ込んだり､スプレッドシートにエクスポートしてGASでごにょごにょして...みたいなところまで書きたかったのですが､それはまた別の機会に...)

##注意
｢本稿の対象者｣に書いた人物=記事を書く前の自分です(笑)
調べつつ頑張って記事を書いた程度の理解なので､諸々ツッコミどころがあるかもしれませんが､ご容赦いただけると幸いですmm




#使う技術について
##Seleniumとは?

- 自動でブラウザを操作できるオートメーションツール
- クローリングに必要な要素の取得や解析といった機能を持つ
- ｢WebDriver｣と呼ばれるAPIを利用しクローラとブラウザをつなぐことで､クローリングが可能になる

わかりそうで､何かよくわからないですね｡
**Webdriver**とはなんぞやから記述してみます｡



###Webdriverとは?


- driverはブラウザごとにそれぞれ存在
    - chrome Driver, Firefox Driver, IE Driver など
- driverに対して指定のプロトコルでHTTPリクエストを行うことで､driverを通してブラウザを操作できる
    - =curlコマンドを使って､CLI上から直接WebDriverにHTTPリクエストを送れば操作できる
- とはいえ､実際にSeleniumでクローラーを動かす際は､自分でHTTPリクエストを作成することはなく､
 クライアントライブラリを利用してブラウザを操作する

文字だとよくわからないので､記事等を読んで覚えたイメージが下図です｡
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214121/79118700-c7b7-f03a-04cf-17f67bfdbbae.png)
　　･クローラーを､書きたい言語(今回はRuby)で作成
　　･クライアントライブラリを使うことで､ブラウザ側がそれぞれ用意しているDriver(API)によしなに繋いでくれる
　　･クローラーが実行したいことがDriver(API)を通してブラウザに伝わり､ブラウザ操作が行われる




※あくまで自分の印象なので､より正確な情報は[こちらの記事](https://app.codegrid.net/entry/selenium-1)を御覧ください｡



##Headless-chromeとは?
```
　･2017/6月リリースのChrome59以降にサポートされるようになった機能
　･GUIではなく､CLI上(ターミナルなど)でChromeを実行できる
```

｢目には見えないけど裏側ではブラウザが立ち上がっている状態｣だと理解してます｡
先程の図において､ブラウザをChromeだけにし､調整したものが下図になります｡


![image.png](https://qiita-image-store.s3.amazonaws.com/0/214121/2a6220cf-b9ca-9fdb-cfa7-e74eaa5ec456.png)
故に､**今回の記事の内容の全体像はこの上図**になります｡


##｢Selenium+HeadlessChrome｣を採用した理由
ここで､少し脱線しますが､なぜ他のライブラリではなく｢Selenium+HeadlessChrome｣でのクローラ作成を考えたか記述しておきます｡

- ログインなど､ステートフルなページを扱うため
    - **Anemone**といった､ステートレスなページのクローリングに特化したライブラリは適さないと思いました
- 管理画面が今後アップデートされた際などを想定し､少しでも対応可能性を高めたかったため
    - Javascriptをベースとした技術(React.jsなど)で管理画面がアップデートされた際､レンダリングができない**Mechanize**では対応が難しくなると思いました(Seleniumは実際にブラウザを操作する=レンダリングするため､対応できそう)







#実装手順
さて､ここから､実際にどんな作業をしたのか記述していきます｡
##①CLIから常にChromeを実行できるようにエイリアスを設定する
先程の図でいうと､下の赤部分のあたりを設定するイメージです｡
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214121/73df614a-8bcd-a5ed-6e0a-4aed15467f6a.png)

※Chromeのバージョンが59以前の場合は､最初にChromeを59以降のものにアップデートして下さい
まずは､Chromeのエイリアスを設定します｡
rcファイル上(bashであれば｢.bashrc｣､zshであれば｢.zshrc｣)に､以下のようにエイリアスを設定します｡

```shell:.zshrc
alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
```
これにより､CLI上で ｢chrome｣と入力して実行すれば､現在どのディレクトリにいようと､chromeが立ち上がるようになりました｡

###(蛇足)Seleniumと絡める前に､headless-chromeで遊んで見る
chromeをheadlessモードで立ち上げる方法は簡単で､ `--headless --disable-gpu`をつけるだけです｡
※`--disable-gpu`は､｢暫定的に必要｣という記述を見かけて念のためつけていますが､現在は不要かもしれないです

```
# chromeのブラウザが(目に見えて)立ち上がると思います
$ chrome   

# chromeのブラウザがheadlessモードで立ち上がると思います(CLI上にゴニョゴニョ文字が出るはず)
$ chrome --headless --disable-gpu   
```


もう少し､いろんなフラグを使って遊んでみます｡

```
# 指定したURLのDOM(≒HTML)を取得できる
$ chrome --headless --disable-gpu --dump-dom https://qiita.com/

# 指定したURLのPDFを作成できる
$ chrome --headless --disable-gpu --print-to-pdf https://qiita.com/

# 指定したURLのスクリーンショットを､サイズ指定で撮れる
$ chrome --headless --disable-gpu --screenshot --window-size=1280,1696 https://qiita.com/
```
色々できて楽しいですね!　詳細は[こちらの記事](https://developers.google.com/web/updates/2017/04/headless-chrome)をご確認下さい｡



##②Seleniumが使えるように､必要なライブラリをインストールする
本題に戻ります｡下の赤色部分あたりについて､説明していきます｡
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214121/414093e8-3ef4-0d77-209c-12b99afae9cd.png)
といっても､**chromedriver**(バイナリ)と**selenium-webdriver**(gem)をインストールするだけです｡

```
# chromedriverをインストールする
$ brew install chromedriver

# selenium-webdriverをインストールする
$ gem install selenium-webdriver
```



##③クローラーを作成する
最後に､クローラーについて簡単に記述します｡ここ↓ですね｡
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214121/dfe162d6-5d8f-2082-0072-baf9dc93b853.png)
###簡単なテストを作ってまずは動かしてみる
本稿ゴールの｢Indeedの管理画面にログインし指標を取得する｣クローラーを作る前に､まずは､シンプルなクローラーを作って動かしてみます｡

こんな動きをするクローラーを作ってみます｡
･自動でブラウザが立ち上がり､google.comにアクセスし､"qiita"と入力し検索
･検索結果ページからqiita.comにアクセス
･アクセスしたqiita.comのtitleである"qiita"が取得され､CLI上に出力


```ruby:test_crawler.rb
require "selenium-webdriver"


#クローラーの動きを実現するインスタンス変数を定義
driver = Selenium::WebDriver.for(:chrome)
#googleの検索ページに訪問
driver.navigate.to("http://google.com")

#'q'というnameを持つ要素(=検索窓)を取得
element = driver.find_element(:name, 'q')
# 上記で取得したinput要素に､"qiita"という文字を入力
element.send_keys("qiita")
# submitを実行(=検索する)
element.submit

# 検索結果ページで､qiitaをクリック
driver.find_element(:xpath, "//*[@id='rso']/div[1]/div/div/div/div/div[1]/a/h3").click

# 表示されたページのタイトルをコンソールに出力
puts driver.title

# テストを終了する（ブラウザを終了させる）
driver.quit
```
ファイルを実行した結果がこちらです｡
![sample_crawler.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/214121/81f62a84-153b-625f-8c88-928087f4d25b.gif)

小さくてわかりにくいですが､CLI上でファイルを実行した後､自動で想定の動きがなされているのが確認できると思います｡


###Indeedの管理画面の各種指標を取得するクローラーを作る
さて､Seleniumを使って無事クローラーが動くことが確認できたので､作りたいクローラーを作っていきます｡

####出来上がったもの
先に､今回作成したスクリプトを載せます｡
今回は､ファイルを大きく2つに分けて作成しました｡
･クローラー自体のファイル...`indeed_managementscreen_crawler.rb`
･クローラーの実行ファイル...`execute.rb`

以下が作成したファイルです｡
(見よう見まねで作ったので､ツッコミどころ多いと思いますがご容赦下さいmm)

```ruby:execute.rb
require_relative 'indeed_managementscreen_crawler.rb'
require_relative 'indeed_marketingindex_importer.rb'

crawler = IndeedCrawler.new
crawler.login
crawler.gothrough
result = crawler.fetch
print result
```
```ruby:indeed_managementscreen_crawler.rb
require "selenium-webdriver"
require 'nokogiri'
require 'pry-byebug'

require 'date'


class IndeedCrawler
  #ログイン時に使う名前とパスワードを環境変数から取得
  LOGIN_KEY = {
    "email": ENV['INDEED_EMAIL'],
    "password": ENV['INDEED_PASSWORD']
  }
  #取得データの日付範囲指定(昨日までのデータを取得する)
  TARGET_END_DATE = Date.today.prev_day(1)


 def initialize()
  #クローラーの動きをheadlessで実現するインスタンス変数を定義
   caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {"args" => ["--headless"]})
   @driver = Selenium::WebDriver.for(:chrome, desired_capabilities: caps)
   #ドロップダウン用のインスタンス変数を定義
   @selecter = Selenium::WebDriver::Support::Select

   #各種指標を格納するための配列を準備
   @indices = []
   #キャンペーンの個数
   @campaign_number = nil
 end


 def login
   @driver.navigate.to("https://ads.indeed.com/job/ads")
   ##値を入力しログイン
   window_email = @driver.find_element(:name, '__email')
   window_email.send_keys "#{LOGIN_KEY[:email]}"
   window_password = @driver.find_element(:name, '__password')
   window_password.send_keys "#{LOGIN_KEY[:password]}"

   @driver.find_element(:xpath, "//*[@id='loginform']/button").click
 end


 def gothrough
   #月初来累積の差分を取りたいので､月初を指定
   start_date = {
     year: TARGET_END_DATE.year.to_s,
     month: TARGET_END_DATE.mon.to_s,
     day: "1"
   }
   end_date = {
     year: TARGET_END_DATE.year.to_s,
     month: TARGET_END_DATE.mon.to_s,
     day: TARGET_END_DATE.mday.to_s
   }
   #絞り込みボタンを使える状態にする
   @driver.find_element(:xpath, "//*[@id='filter_options']/div/span").click
   #取得したい日付となるように､範囲のはじめと終わりを入力
   #範囲の終わり(=昨日)
   selection_end_month = @selecter.new(@driver.find_element(:xpath, "//*[@id='month2_0']"))
   selection_end_month.select_by(:text, end_date[:month])
   selection_end_year = @selecter.new(@driver.find_element(:xpath, "//*[@id='year2_0']"))
   selection_end_year.select_by(:text, end_date[:year])
   selection_end_day = @selecter.new(@driver.find_element(:xpath, "//*[@id='day2_0']"))
   selection_end_day.select_by(:text, end_date[:day])
   #範囲の始まり(=当月1日)
   selection_start_month = @selecter.new(@driver.find_element(:xpath, "//*[@id='month1_0']"))
   selection_start_month.select_by(:text, start_date[:month])
   selection_start_year = @selecter.new(@driver.find_element(:xpath, "//*[@id='year1_0']"))
   selection_start_year.select_by(:text, start_date[:year])
   selection_start_day = @selecter.new(@driver.find_element(:xpath, "//*[@id='day1_0']"))
   selection_start_day.select_by(:text, start_date[:day])

   @driver.find_element(:xpath, "//*[@id='filter_options']/div/div/button").click
 end


 def fetch
   (1..campaign_number).each do |current_campaign|
     campaign = {
       campaign_name: @driver.find_element(:xpath, "//*[@id='sjc_table']/tbody/tr[#{current_campaign}]/td[3]/span/span/a").text,
       impressions: @driver.find_element(:xpath, "//*[@id='sjc_table']/tbody/tr[#{current_campaign}]/td[4]").text.delete("^0-9").to_i,
       clicks: @driver.find_element(:xpath, "//*[@id='sjc_table']/tbody/tr[#{current_campaign}]/td[5]").text.delete("^0-9").to_i,
       cost: @driver.find_element(:xpath, "//*[@id='sjc_table']/tbody/tr[#{current_campaign}]/td[9]/div[1]/span").text.delete("^0-9").to_i,
       cpc: @driver.find_element(:xpath, "//*[@id='sjc_table']/tbody/tr[#{current_campaign}]/td[10]/span").text.delete("^0-9").to_i,
       max_cpc: @driver.find_element(:xpath, "//*[@id='sjc_table']/tbody/tr[#{current_campaign}]/td[10]/div").text.delete("^0-9").to_i
     }
    #停止したキャンペーンを｢取得期間においてimpが1回もないもの｣として判断
    if campaign[:impressions] == 0 then
      next
    end
    @indices << campaign
   end
  return @indices
 end


 def campaign_number  #行数を数えてキャンペーン数を取得
   table_low_number = @driver.find_elements(:xpath, "//*[@id='sjc_table']/tbody/tr")
   @campaign_number = table_low_number.count - 2
 end
end
```

####実行方法と実行
環境変数に下記を設定します｡
`INDEED_EMAIL`･･･ログイン時に必要なメールアドレス
`INDEED_PASSWORD`･･･ログイン時に必要なパスワード
※環境変数に関しては､[こちら](https://qiita.com/fuwamaki/items/3d8af42cf7abee760a81)の記事などを参照させて頂きました｡

環境変数設定後`execute.rb`ファイルを実行すると､広告キャンペーン単位のハッシュを格納した配列(result)が､CLI上に表示されているのが確認できると思います｡

無事､｢Indeed管理画面の各種指標をクローラーで取得｣できましたね!**完成!**


####補足
headlessモードだと､実行しても何をしているかわからないので､通常のブラウザ状態でSeleniumを動かしたいと思います｡
`indeed_managementscreen_crawler.rb`の､initializeメソッド内の冒頭2行を､以下のように書き換えて､`execute.rb`を実行してみて下さい｡

```ruby:indeed_managementscreen_crawler.rb
   #消す→　caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {"args" => ["--headless"]})
   @driver = Selenium::WebDriver.for(:chrome)

```

実行すると､下記のような挙動が確認できるはずです(ここでは静止画で､イメージ画像を貼らせて頂きますmm)
①Indeed管理画面のログインページにアクセスし､メールアドレスとパスワードを入力し､ログイン
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214121/a70db931-0330-ff29-9bc5-62f603e8a435.png)


②管理画面右上の日付指定で､当月1日~昨日までに絞り込む
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214121/3de44ed3-8979-c680-01b5-536cfd9800f3.png)




③日付を絞り込んだページで､取得したい要素をスクレイピング
※画像は上記とほぼ同様なので､省略


④CLI上に出力
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214121/6eb14fbd-39f0-a32e-b976-ae007a65907d.png)





#まとめ
実は､今回のクローラー作成の直前に社用PCを乗り換えていたのですが､
折角なので､Homebrewを入れたり､シェルをzshにして諸々インストールして使いやすくしたり､からやったりもしました｡
(最初はそっちをカレンダーに書こうかなと思ったり)

そこも含めてなのですが､全体的に点だった知識が少しずつ線になりつつある感覚があり､世の中的にはほんの僅かなんだろうけど､個人にとってはかなり成長できたような気がしており､本当に良い勉強になりました｡
(初期ポケモンが､初めて｢ひのこ｣や｢みずでっぽう｣のような属性技を覚えた的な)

Advent Calendarに参加できて良かったです｡お声掛け頂きありがとうございました!
また来年も是非参加させていただきます!

そして､最後まで読んで下さった皆様､ありがとうございました!





#参考文献
###開発自体
　･selenium+headlessChromeの全体感
　　　https://qiita.com/meguroman/items/41ca17e7dc66d6c88c07
　　　https://qiita.com/orangain/items/6a166a65f5546df72a9d

　･headlessChromeについて
　　　https://developers.google.com/web/updates/2017/04/headless-chrome

　･Seleniumについて
　　　･seleniumについて
　　　　　https://app.codegrid.net/entry/selenium-1
　　　･seleniumを使ってみる
　　　　　https://qiita.com/edo_m18/items/ba7d8a95818e9c0552d9
　　　　　https://qiita.com/tomerun/items/9cb81d7a98150ff22f53
　　　･chromedriverが動かないとき
　　　　　https://github.com/flavorjones/chromedriver-helper/issues/44
　　　･webdriverの持つメソッドとか
　　　　　https://qiita.com/mochio/items/dc9935ee607895420186
　　　　　https://morizyun.github.io/web/selenium-cheat-sheet.html

　･XpathとCSSセレクタの対応表
　　　https://qiita.com/niusounds/items/8932790c77d781aa8993

###前提知識･周辺知識
　･前提知識
  　　･Linuxとは?カーネルとは?
　　　　https://wa3.i-3-i.info/word15502.html
　　  ･pathを通すとは?環境変数とは?
　　　　https://qiita.com/fuwamaki/items/3d8af42cf7abee760a81
  　　･profileとは?rcとは?
　　　　https://qiita.com/shyamahira/items/260862743e4c9794b5d2
  　　･Homebrewのcaskやtapとは?
　　　　http://tweeeety.hateblo.jp/entry/2018/05/04/181928
  　　･｢Bundler｣と｢gemfile｣と｢gemfile.lock｣とについて
　　　　https://qiita.com/tsubasakat/items/169833d4c8baf79e1b52

　･周辺知識
  　　･｢ChromeがStableになった｣とは?
　　　　https://support.google.com/chromebook/answer/1086915?hl=ja
  　　･PhantomJSとは?
　　　　https://jser.info/2018/06/11/phantomjs-ended/


###rubyの基礎知識
　･クラスとモジュール
　　　https://qiita.com/fukumone/items/2dd4d2d1ce6ed05928de
　･インスタンス変数/クラス変数/クラスインスタンス変数
　　　https://qiita.com/mogulla3/items/cd4d6e188c34c6819709#3--クラスインスタンス変数
　･ハッシュの操作
　　　https://uxmilk.jp/43303
　･日付の扱い方
　　　https://qiita.com/prgseek/items/c0fc2ffc8e1736348486#-n日前n日後--n月前n月後--n年前n年後
　　　https://www.javadrive.jp/ruby/date_class/index3.html


###(蛇足)シェルの設定
　･zshを入れてprezto入れた
　　　https://qiita.com/taktakfu/items/ce228762c9078466c71f
　･ターミナルをカスタマイズした
  　　･全体
　　　　https://qiita.com/kinchiki/items/57e9391128d07819c321
  　　･個別
  　　  　　･色合いを変えた
　　　　　　https://cocopon.me/blog/2014/04/iceberg-for-terminalapp/?p=4862
  　　  　　･プロンプトを変えた(pureをいれた)
　　　　　　https://suin.io/565
  　　  　　･補完機能とハイライト機能を入れた(記事の最後の方)
　　　　　　https://qiita.com/kinchiki/items/57e9391128d07819c321