20
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Livesense not engineersAdvent Calendar 2018

Day 21

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

Last updated at Posted at 2018-12-21

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

現在新卒2年目で、転職会議のプロダクトマネージャー及びマーケターを担当しております。

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

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

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

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

● 読む目的
  ○ 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
  ・クローラーを、書きたい言語(今回はRuby)で作成
  ・クライアントライブラリを使うことで、ブラウザ側がそれぞれ用意しているDriver(API)によしなに繋いでくれる
  ・クローラーが実行したいことがDriver(API)を通してブラウザに伝わり、ブラウザ操作が行われる

※あくまで自分の印象なので、より正確な情報はこちらの記事を御覧ください。

##Headless-chromeとは?

 ・2017/6月リリースのChrome59以降にサポートされるようになった機能
 ・GUIではなく、CLI上(ターミナルなど)でChromeを実行できる

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

image.png
故に、今回の記事の内容の全体像はこの上図になります。

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

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

#実装手順
さて、ここから、実際にどんな作業をしたのか記述していきます。
##①CLIから常にChromeを実行できるようにエイリアスを設定する
先程の図でいうと、下の赤部分のあたりを設定するイメージです。
image.png

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

.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/

色々できて楽しいですね! 詳細はこちらの記事をご確認下さい。

##②Seleniumが使えるように、必要なライブラリをインストールする
本題に戻ります。下の赤色部分あたりについて、説明していきます。
image.png
といっても、chromedriver(バイナリ)とselenium-webdriver(gem)をインストールするだけです。

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

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

##③クローラーを作成する
最後に、クローラーについて簡単に記述します。ここ↓ですね。
image.png
###簡単なテストを作ってまずは動かしてみる
本稿ゴールの「Indeedの管理画面にログインし指標を取得する」クローラーを作る前に、まずは、シンプルなクローラーを作って動かしてみます。

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

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

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

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

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

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

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
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・・・ログイン時に必要なパスワード
※環境変数に関しては、こちらの記事などを参照させて頂きました。

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

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

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

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

実行すると、下記のような挙動が確認できるはずです(ここでは静止画で、イメージ画像を貼らせて頂きますmm)
①Indeed管理画面のログインページにアクセスし、メールアドレスとパスワードを入力し、ログイン
image.png

②管理画面右上の日付指定で、当月1日~昨日までに絞り込む
image.png

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

④CLI上に出力
image.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 

20
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?