こんにちは。学生エンジニアのhashimotoです。もう平成最後の年末ですね。

今回は、「selenium(セレニウム)を使ってSNSサイトにログインし、タイムラインをスクロールして自動取得する」という流れをご紹介します。マストドンというSNSのQiitadonというインスタンスを例としてプログラムしていきます。

seleniumを使ってログイン

今回はqiitaでマストドンにログイン、qiitaにtwitterでログインという流れでログインしていきます。

まずはgemのrequireやseleniumの設定です。このあたりは過去記事にseleniumの環境構築の記事がありますのでそちらを参照してください。

require 'selenium-webdriver'
require 'nokogiri'

Selenium::WebDriver::Chrome.driver_path = "/mnt/c/chromedriver.exe"
ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36'
caps = Selenium::WebDriver::Remote::Capabilities.chrome('chromeOptions' => { args: ["--user-agent=#{ua}", 'window-size=1280x800', '--incognito'] }) # シークレットモード
client = Selenium::WebDriver::Remote::Http::Default.new
client.read_timeout = 300
driver = Selenium::WebDriver.for :chrome, desired_capabilities: caps
driver.manage.timeouts.implicit_wait = 10

さて、ログインしていく部分ですが、基本的にはfind_elementというメソッドで要素を指定しclickやsend_keysといったアクションをしていくだけです。

username = "username"
password = "password"

# ログインサイトへ飛ぶ
driver.navigate.to"https://qiitadon.com/auth/sign_in"
sleep 1
# qiitaでログインをクリック
driver.find_element(:class, 'button-qiita').click
sleep 1
# twitterでログインをクリック
driver.find_element(:class, 'btn-twitter-inverse').click
# username,passwordの入力欄を指定して入力
username_form = driver.find_element(:name, 'session[username_or_email]')
username_form.send_keys(username)
password_form = driver.find_element(:name, 'session[password]')
password_form.send_keys(password)
sleep 1
# ログイン、許可のボタンを押していく
driver.find_element(:css, 'input.submit').click
sleep 1
driver.find_element(:class, 'btn-success').click

実行結果はこのような感じになります。

ログイン画面

ログイン完了

タイムラインをスクロールしてスクレイピング

ログイン後の画面の真ん中のホームというタイムラインをスクロールしてそれぞれの投稿のテキストを取得していきます。

ここで使用するメソッドがexecute_scriptです。このメソッドは引数のjavascriptを現在開いているブラウザで実行してくれます。ログインの部分もほぼこのメソッドに置き換えることができるほど万能だと思います。

#スクロールする部分を指定
js_scroll_area = "document.getElementsByClassName('scrollable')[0]"
# 10回スクロール
article_texts = []
10.times{
  sleep 2
  driver.execute_script("#{js_scroll_area}.scrollTo(0, #{js_scroll_area}.scrollHeight);")
}

これで自由にスクロールしてpage_sourceメソッドでhtmlを取り出しパースすることでスクレイピングができます。

試しに投稿の文をターミナルに表示させてみました。

まとめ

いかがだったでしょうか。open-uriやmechanizeでスクレイピングすることが難しいサイトでもseleniumを使えばうまくいくことがあると思います。SNSのタイムラインを自動保存してみたい(そんな場面があるのか不明ですが)に参考になれば幸いです。SNSによっては制約が厳しい場合もあるのでよく確認して自己責任でプログラミングしていきましょう。

全体のソース

require 'selenium-webdriver'
require 'nokogiri'

Selenium::WebDriver::Chrome.driver_path = "/mnt/c/chromedriver.exe"
ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36'
caps = Selenium::WebDriver::Remote::Capabilities.chrome('chromeOptions' => { args: ["--user-agent=#{ua}", 'window-size=1280x800', '--incognito'] }) # シークレットモード
client = Selenium::WebDriver::Remote::Http::Default.new
client.read_timeout = 300
driver = Selenium::WebDriver.for :chrome, desired_capabilities: caps
driver.manage.timeouts.implicit_wait = 10

#twitterのログイン情報
username = "username"
password = "password"

# ログインサイトへ飛ぶ
driver.navigate.to"https://qiitadon.com/auth/sign_in"
sleep 1
# qiitaでログインをクリック
driver.find_element(:class, 'button-qiita').click
sleep 1
# twitterでログインをクリック
driver.find_element(:class, 'btn-twitter-inverse').click
# username,passwordの入力欄を指定して入力
username_form = driver.find_element(:name, 'session[username_or_email]')
username_form.send_keys(username)
password_form = driver.find_element(:name, 'session[password]')
password_form.send_keys(password)
sleep 1
# ログイン、連携のボタンを押していく
driver.find_element(:css, 'input.submit').click
sleep 1
driver.find_element(:class, 'btn-success').click

#スクロールする部分を指定
js_scroll_area = "document.getElementsByClassName('scrollable')[0]"
# 10回スクロール
article_texts = []
10.times{
  sleep 2
  # ページのhtmlを保存してパースしていく
  html = driver.page_source
  doc = Nokogiri::HTML(html, nil, 'UTF-8')
  doc.css("article").css("div.status__content").each do |article|
    article_texts << article.text
  end
  driver.execute_script("#{js_scroll_area}.scrollTo(0, #{js_scroll_area}.scrollHeight);")
}
driver.quit

article_texts.uniq!
count = 0
article_texts.each do |text|
  count += 1
  puts count
  puts text
end