こんにちは。アルバイトのtakataです。初めての投稿ですがよろしくお願いいたします。今回は8割程度のところまでスクロールするとウェブページの更新が行われ、かつ、更新回数が2度以上あるサイトのスクレイピングをSeleniumを使用して行う方法についてご紹介します。

スクレイピングを行っているとスクロールに伴ってページが更新されるというウェブサイトにしばしば遭遇します。一般的にページ下までスクロールしないと商品や文章が表示されないというウェブページでは、対象のウェブページの最大の高さよりも十分に大きい値を設定しSeleniumでスクロールさせることで画面更新後のHTMLを取得できます。

しかし、今回扱うシチュエーションでは一度に下までスクロールすると更新されないという点と、更新が一度でないという点の2点からこの方法は使えません。したがってスクロールの高さを更新が行われるように制限し、かつ更新が行われなくなったことを確かめながらスクロールする必要があります。結論から言うと、複雑なことはせずとも単純なループ処理で実現できます。それでは詳細を説明します。

Seleniumを使用したスクレイピング

本題に入る前に一気にスクロールする方法を説明します。Selenium自体の使い方については過去の記事(環境構築Seleniumの使い方)がわかりやすいと思います。使用言語はRubyですが、SeleniumはJava, Python, C#, Ruby, JavaScript, Kotlinに対応しているようですので慣れている言語でトライするのもありかもしれません。Selenium Webdriver ドライバー要件

初めにコードを載せます。


require 'selenium-webdriver'

# Seleniumを使用するための設定群
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'.freeze

def initialize_driver
  option_args = [
    "--headless",
    "--no-sandbox",
    "--disable-setuid-sandbox",
    "--disable-gpu",
    "--user-agent=#{USER_AGENT}",
    'window-size=1280x800'
  ]
  # for windows
  driver_for_win = "/mnt/c/driver/chromedriver.exe"
  Selenium::WebDriver::Chrome::Service.driver_path = driver_for_win if File.exist?(driver_for_win)
  client = Selenium::WebDriver::Remote::Http::Default.new
  caps = Selenium::WebDriver::Remote::Capabilities.chrome('chromeOptions' => {args: option_args, w3c: false})
  @driver = Selenium::WebDriver.for :chrome, desired_capabilities: caps, http_client: client
  @driver.manage.timeouts.implicit_wait = 10
end

def quit_driver
  @driver.quit
end

url = 'https://www.yahoo.com/'

initialize_driver
sleep(1)
@driver.navigate.to(url) # 対象のページに遷移する
sleep(3)
10.times do # 一度だけはスクロールできないかもしれないので10回繰り返す
  sleep(1)
  @driver.execute_script('window.scroll(0,1000000);') # スクロールを実行
end
sleep(10)
@driver.page_source
quit_driver

初めにSeleniumを使うための設定を書いています。初期化や終了時に行うことは関数にまとめています。頻繁に使用するため関数にまとめていますが、べた書きでも使用には問題ありません。本記事ではヘッドレスモードで実行していますが、オプションから除外すれば実際の実行状況を目で確認することができます。自分もデバッグ時には目視で最後までスクロールが行われたことを確認しました。

スクレイピングサンプルとしてYahoo!を選択しました。下までスクロールすると画面の更新が行われるのでSeleniumを使う練習としてよいと思います。ちなみに執筆にあたり調査していませんが、スクロールしてもしなくてもHTMLに変化がない場合はSeleniumを使わずともNokogiriでスクレイピングできるのでそこはケースバイケースです。

まず、@driver.navigate.to(url)で対象サイトにアクセスしています。その後、@driver.execute_script('window.scroll(0,1000000);') でスクロールを実行しています。一度ではスクロールしきれない場合に備えて10回繰り返すようにしていますが、しなくても十分なら必要ないと思います。

そしてスクロールが終わった後は@driver.page_sourceでHTMLの取得を行っています。本記事では説明のため、これ以上の操作は行っていませんが、実務ではRDBに保存するなどしています。最後にquit_driverでSeleniumを止めるのを忘れないように。

通常の場合はスクロール操作を行うだけでよいので簡単ですね。それでは本題に入っていきましょう。

ゆっくりとスクロールする場合

まずソースコードを示します。


require 'selenium-webdriver'

USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'.freeze

def initialize_driver
  option_args = [
    "--headless",
    "--no-sandbox",
    "--disable-setuid-sandbox",
    "--disable-gpu",
    "--user-agent=#{USER_AGENT}",
    'window-size=1280x800'
  ]
  # for windows
  driver_for_win = "/mnt/c/driver/chromedriver.exe"
  Selenium::WebDriver::Chrome::Service.driver_path = driver_for_win if File.exist?(driver_for_win)
  client = Selenium::WebDriver::Remote::Http::Default.new
  caps = Selenium::WebDriver::Remote::Capabilities.chrome('chromeOptions' => {args: option_args, w3c: false})
  @driver = Selenium::WebDriver.for :chrome, desired_capabilities: caps, http_client: client
  @driver.manage.timeouts.implicit_wait = 10
end

def quit_driver
  @driver.quit
end

url = 'https://www.yahoo.com/'

initialize_driver
sleep(1)
@driver.navigate.to(url)
sleep(3)
last_height = @driver.execute_script("return document.body.scrollHeight")
while true
  1.step(last_height, last_height / 10).each do |height|
    sleep(1)
    @driver.execute_script("window.scrollTo(0, #{height})")
  end
  sleep(1)
  new_height = @driver.execute_script("return document.body.scrollHeight")
  if new_height == last_height
    break
  end
  last_height = new_height
end
sleep(10)
@driver.page_source
quit_driver

設定部分、HTMLの取得、Seleniumの停止箇所のコードは先ほどと同じです。変更したのはスクロールのロジックのみです。もともとの目的は

  • 一度に下までスクロールせずに段階的にスクロールを行う。
  • ウェブページのの更新が行われなくなったらスクロールを停止する。
の二点です。1点目の段階的なスクロールはページの高さlast_heightを取得したうえで10分割し、1/10, 2/10, ..., 10/10の高さに移動するようにしました。これでいずれかの実行時に更新が行われるようになりました。分割数は10としましたが、粗くても問題ない場合はそのほうが実行時間が短くなるのでよいと思います。また、前半部分はなくてもいいですし8割程度のところに一度だけスクロールするようにしてもいいと思います。


1.step(last_height, last_height / 10).each do |height|
    sleep(1)
    @driver.execute_script("window.scrollTo(0, #{height})")
  end

2点目のスクロールが下まで完了したか確認するために、サイトの高さlast_heightを取得しています。スクロールが終わった後にも高さnew_heightを取得して、両者が一致したら終了するようにしました。

さいごに

かなり簡単にスクロールの調整ができるということがわかったのではないでしょうか。 中身はループ処理なので大したことはしていませんが、頭の片隅に置いておくとスクレイピングで助かることがあるかもしれません。
Pocket