Selenium+ChromeDriverでスクレイピングをしてみる

はじめに

Nokogiriについて学ぶ \- らせんびよりで、Nokogiriでは情報の取得が難しいサイトがあることを学んだ。

今回は、そういったサイトにも対応できる「Selenium+ChromeDriverでスクレイピングをする方法」について学んでいく。

SeleniumとChromeDriverについて

この二つを組み合わせることで、プログラム上でchromeブラウザを操作することができるようになる。

ためしにseleniumをrubyでやってみたを参考にやってみる。

# selenium-webdriverを取り込む
require 'selenium-webdriver'
require 'webdrivers'

# ブラウザの指定(Chrome)
session = Selenium::WebDriver.for :chrome
# 10秒待っても読み込まれない場合は、エラーが発生する
session.manage.timeouts.implicit_wait = 10
# ページ遷移する
session.navigate.to "<https://google.com/>"

# ページのタイトルを出力する
puts session.title

# 検索フォームの取得(この場合はname属性で取得している)
query = session.find_element(:name, 'q')
# "zenn"を自動入力する
query.send_keys('zenn')

# 送信(検索)
query.submit

# 5秒遅延(処理が早すぎてページ遷移前にスクリーンショットされてしまうため)
sleep(5)

# スクリーンショットをして"zenn.png"で保存する(保存される場所は、コード実行箇所)
if session.save_screenshot('zenn.png')
  # スクリーンショットができたら出力する
  puts "スクリーンショットされました!"
end

# ブラウザを終了
session.quit

とすると、

$ ruby selenium_test.rb 
Google
スクリーンショットされました!

無事動作したようだ。

Selenium+ChromeDriverでのスクレイピング方法

selenium公式のドキュメントFinding web elements \| Seleniumによると、seleniumには要素の取得方法が色々と用意されている。

試しにCSSセレクターを使って要素の取得を試みる。

対象は、nokogiri gemでは取得が難しかった発売スケジュール|任天堂

# selenium-webdriverを取り込む
require 'selenium-webdriver'
require 'webdrivers'

# ブラウザの指定(Chrome)
session = Selenium::WebDriver.for :chrome
# 10秒待っても読み込まれない場合は、エラーが発生する
session.manage.timeouts.implicit_wait = 10
# ページ遷移する
session.navigate.to "<https://www.nintendo.co.jp/schedule/index.html>"

# ページのタイトルを出力する
puts session.title

# 発売予定のゲームタイトルを取得
el = session.find_elements(:css, 'div.local-schedule__listTitle > strong')

el.each do |title|
  puts title.text
end

# ブラウザを終了
session.quit

とすると…

$ ruby selenium_test.rb
発売スケジュール|任天堂
孤独なヴィラ (Lonesome Village)
Missile Command: Recharged
Adrian's Tale
AAAクロック 2
Shatter Remastered Deluxe
すみっコぐらし みんなでリズムパーティ
Timore 5
ドラえもん のび太の牧場物語 大自然の王国とみんなの家
ドラえもん のび太の牧場物語 大自然の王国とみんなの家 デラックスエディション
ドラゴンプラナ
ピクセルぬりえ
Flying Neko Delivery
Mecha Ritz: Steel Rondo
ラン・ボックス・ラン
A列車で行こう ひろがる観光ライン
Aeterna Noctis
Ghost Song
Nintendo Switch(有機ELモデル) スカーレット・バイオレットエディション
HARVESTELLA
It Takes Two
ソニックフロンティア
ソニックフロンティア デジタルデラックス
Sifu
アイ★チュウ
アドベントカレンダー
お絵かき心理テスト ー恋愛も友情も!かんたん診断でアナタの脳内マルハダカ!?-
Orbital Bullet
Cyber Velocity Run
Jurassic World Aftermath Collection
...

発売予定のゲームタイトルを取得することができた!

感想

やっと望みの結果が得られて嬉しい。

最初nokogiriでいくらやってもうまくいかなかったので、「もしかしてスクレイピング対策されてるのかなあ…」と思ったが、やりようはあるものだなあ。

今回の結果を元にrubygemを作っていく予定。

Nokogiriについて学ぶ

はじめに

webスクレイピングについて調べたところ、「nokogiri」というgemの情報が出てきた。

そこで、nokogiriに実際に触って理解を深めた。

できること

公式:https://nokogiri.org/

公式のhow toの冒頭に「Nokogiri is a large library, and so it's challenging to briefly summarize it. We've tried to provide long, real-world examples at Tutorials .」とある。

翻訳すると、「Nokogiriは大きなライブラリなので、簡単にまとめるのは難しいです。チュートリアルで長い実例を提供するようにしました。」

どうやらたくさんの機能を備えたライブラリのようだ。

公式の一例

#! /usr/bin/env ruby

require 'nokogiri'
require 'open-uri'

# HTMLを取得し、パースする
doc = Nokogiri::HTML(URI.open('<https://nokogiri.org/tutorials/installing_nokogiri.html>'))

# CSSでノードを検索する
doc.css('nav ul.menu li a', 'article h2').each do |link|
  puts link.content
end

# xpathでノードを検索する
doc.xpath('//nav//ul//li/a', '//article//h2').each do |link|
  puts link.content
end

# 混ぜて使用する
doc.search('nav ul.menu li a', '//article//h2').each do |link|
  puts link.content
end

上の例を手元のirbで試してみる。

HTMLを取得し、パースする

irb(main):004:0> doc = Nokogiri::HTML(URI.open('<https://nokogiri.org/tutorials/installing_nokogiri.html>'))
=> #<Nokogiri::HTML4::Document:0x148d4 name="document" children=[#<Nokogiri::XML::DTD:0x2d0 name="html">, #<Nokogiri::XML::Element:0x148c0 name="html" attributes=[#<Nokogir...
irb(main):005:0>

なんだかよくわからないオブジェクトが返ってきている。

まずこれを知るためにNokogiri::HTMLURI#openについて調べる。

open-uri公式:https://docs.ruby-lang.org/ja/latest/library/open=2duri.html

Nokogiri公式APIドキュメント:https://nokogiri.org/rdoc/index.html

  • URI#open

    • 公式によると、openの引数にhttp:// や https://、ftp:// で始まっている文字列を渡すと、 URI のリソースを取得した上で StringIO オブジェクトまたは Tempfile オブジェクトとして返すらしい。
    irb(main):005:0> doc = URI.open('<https://nokogiri.org/tutorials/installing_nokogiri.html>')
    => #<Tempfile:/var/folders/lr/fmfvf78j5dz5typy3qlwnp6r0000gn/T/open-uri20221101-44816-fouxid>
    
    • たしかにTempfileオブジェクトとして返ってきている。Tempfileオブジェクトについて調べてみると、「Tempfile オブジェクトは**FileクラスへのDelegatorとして定義されており、File**クラスのオブジェクトと同じように使うことができます。」とある。
    • ためしにIO#readを使って中身を文字列で確認してみると
    irb(main):009:0> doc.read
    => "\\n<!doctype html>\\n<html lang=\\"en\\" class=\\"no-js\\">\\n  <head>\\n    \\n      <meta charset=\\"utf-8\\">\\n      <meta name=\\"viewport\\" content=\\"width=device-width,initial-scale=1\\">\\n      \\n        <meta name=\\"description\\" content=\\"The Official Tutorial Archive™ of Nokogiri®\\">\\n      \\n      \\n        <meta name=\\"author\\" content=\\"Mike Dalessio\\">\\n      \\n      \\n        <link rel=\\"canonical\\" href=\\"<https://nokogiri.org/tutorials/installing_nokogiri.html\\>">\\n
    
    長いので以下略
    
    • となっていて、htmlリソースが入っていることがわかる。
  • Nokogiri::HTML

    • HTMLとしてパースし、Nokogiri::HTML4::Documentオブジェクトを返す。(ちなみにNokogiri::HTML.parseとしても同じ結果になる。)
    • Nokogiri::HTML4::Documentオブジェクトに対して様々なメソッドを使うことで、目的のノードを取得したり、ドキュメントのtitleを確認したりできる。
    irb(main):009:0> doc = Nokogiri::HTML(URI.open('<https://nokogiri.org/tutorials/installing_nokogiri.html>'))
    => #<Nokogiri::HTML4::Document:0x3d4dc name="document" children=[#<Nokogiri::XML::DTD:0x28eec name="html">, #<Nokogiri::XML::Element:0x3d4c8 name="html" attributes=[#<Nokog...
    irb(main):010:0> puts doc
    <!DOCTYPE html>
    <html lang="en" class="no-js">
      <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width,initial-scale=1">
          
            <meta name="description" content="The Official Tutorial Archive™ of Nokogiri®">
          
          
            <meta name="author" content="Mike Dalessio">
          
          
            <link rel="canonical" href="<https://nokogiri.org/tutorials/installing_nokogiri.html>">
    
    • Nokogiri::HTML4::Documentオブジェクトにputsを使うと、こんな感じでちゃんとHTMLとしてパースされてるのがわかる。

ノードを検索する

doc = Nokogiri::HTML(URI.open('<https://utsumate.net>'))
css = doc.css('#wrapper__start > main > div > p.mb-6.top-page-content-line-height')
css.text

=> "このアプリは、そんな思いに応えるために作られました"
  • とれた。

上記のやりかたでは情報を取得できないサイトも

ここまでで学んだやりかたで、ためしに発売スケジュール|任天堂の発売予定のゲームタイトルの情報をとってみようとすると…。なぜかとることができない。

doc = Nokogiri::HTML(URI.open('<https://www.nintendo.co.jp/schedule/index.html>'))
puts doc

として中身を確認するとわかるが、ゲームに関する情報が丸々抜けている。

なぜこんなことが起こるかというと、サイトによっては、javascriptによってindex.htmlとは別のhtmlを取得するリクエストが投げられていたり、コンテンツの一部がJSONから取得されていたりするからだ。

参考:RubyでWebスクレイピング \#5 javascript対応テクニック

こういう場合はこれまでと違ったアプローチを取らなければいけない。

次はそういったアプローチについて学んでいきたい。

感想

webスクレイピングで情報をとれるようになれば、作れるものの幅も広がるし、実生活でも役立つと思うので、引き続き学んでいきたい。

rubygemのインストール先と、requireでどのように呼び出して使っているかを調べた。

概要

rubygemを作って公開しよう!と意気込んでいろいろ情報を集め、さあ作るぞ!ってなったときに、ふと、

「いままでrailsプロジェクトでさんざん使ってきたけど…gemってどこにインストールされて、どういう仕組みで読み込んで使ってるんだ?」と疑問が湧いてきた。

なので調べた。

結論

bundle installでインストールされたgemは、$ gem environmentの出力結果のINSTALLATION DIRECTORY に配置され、requireで組み込み変数$LOAD_PATH内のパスを起点として検索され、呼び出され、使用できるようになっている。

gemのインストール先

bundle installでインストールされたgemは、$ gem environmentの出力結果のINSTALLATION DIRECTORY に配置される。

$ gem environment 
RubyGems Environment:
  - RUBYGEMS VERSION: 3.2.3
  - RUBY VERSION: 3.0.0 (2020-12-25 patchlevel 0) [x86_64-darwin19]
  - INSTALLATION DIRECTORY: /Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0
  - USER INSTALLATION DIRECTORY: /Users/horimotonaomichi/.gem/ruby/3.0.0
  - RUBY EXECUTABLE: /Users/horimotonaomichi/.rbenv/versions/3.0.0/bin/ruby
  - GIT EXECUTABLE: /usr/local/bin/git
  - EXECUTABLE DIRECTORY: /Users/horimotonaomichi/.rbenv/versions/3.0.0/bin
  - SPEC CACHE DIRECTORY: /Users/horimotonaomichi/.gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /Users/horimotonaomichi/.rbenv/versions/3.0.0/etc
  - RUBYGEMS PLATFORMS:
     - ruby
     - x86_64-darwin-19
  - GEM PATHS:
     - /Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0
     - /Users/horimotonaomichi/.gem/ruby/3.0.0
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => false
     - :bulk_threshold => 1000
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /Users/horimotonaomichi/.rbenv/versions/3.0.0/bin
     - /usr/local/Cellar/rbenv/1.2.0/libexec
     - /Users/horimotonaomichi/.rbenv/shims
     - /Users/horimotonaomichi/.rbenv/bin
     - /Users/horimotonaomichi/.nodebrew/current/bin
     - /usr/local/bin
     - /usr/bin
     - /bin
     - /usr/sbin
     - /sbin

読み込みの仕組み

rubyrequireの読み込み対象は、rubyの組み込み変数$LOAD_PATH内のパスを起点とし、相対パスでファイルを検索する。

検索の仕組みは単純で、

  1. $LOAD_PATH($:)を順に辿る
  2. 目的のファイルが見つかったら終了
  3. 最後まで来たらLoadError例外

$ ruby -e 'puts $LOAD_PATH'とすると、$LOAD_PATH内に格納されているパスを見ることができる。

$ ruby -e 'puts $LOAD_PATH'
/usr/local/Cellar/rbenv/1.2.0/rbenv.d/exec/gem-rehash
/Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/site_ruby/3.0.0
/Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/site_ruby/3.0.0/x86_64-darwin19
/Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/site_ruby
/Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/vendor_ruby/3.0.0
/Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/vendor_ruby/3.0.0/x86_64-darwin19
/Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/vendor_ruby
/Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/3.0.0
/Users/horimotonaomichi/.rbenv/versions/3.0.0/lib/ruby/3.0.0/x86_64-darwin19

疑問点

bundle install —path vendor/bundleでインストールした場合はどうなる?

プロジェクト内のvendor/bundleディレクトリにインストールされる。これをそのままrequireはできないので、プロジェクト内にインストールしたgemを読み込んで使用するためのBundler.requireメソッドがある。

ただし、Bundler 2.1 以降、 --pathオプションは deprecated(非推奨) となっており、基本的にはgemはグローバルにインストールして使うのが主流っぽい。

参考:

Railsの場合は?

Railsプロジェクトにおいても、bundle installのインストール先は$ gem environmentの出力結果のINSTALLATION DIRECTORY

railsではgemをrequireする必要はない。

理由はconfig/application.rbにGemfile内のgemを一括で読み込む設定がなされているため。

参考:

感想

gemのインストール先を調べていくうちに、requireの仕様まで学ぶことができてよかった。

また、gemはグローバルにおいて使うのが主流ということもわかった。なんとなーく「グローバル汚染するのはよくないのかなあ?」と思っていたのがスッキリした。

(若手?)エンジニアもくもく会 vol.55に参加しました!

作業内容

  • rubygem作成のための情報集め+アイデア出し
  • 就活の情報集め

感想

現役エンジニアのmasciiさんと宮田さんから得難い話を聞けてとても楽しかった。やっぱりエンジニアとして活躍している人たちって、ちゃんとアウトプットしてきてるんだなと実感。

 

雑談で、僕がエンジニアになりたい理由に共感していただけたのが嬉しかった。やっぱり、本音ベースで話すのって大事だなあ。

 

アドバイスを受けて、OSS活動初めてみようかなと思っていたけど、とりあえずは自分の興味のあるものに取り組んでいろいろ作る方向に時間を使おうと思った。

 

来月もあるそうなので、また参加したい。

りんどく.rb #83に参加しました!

学習内容

感想

rubyのパターンマッチの存在を初めて知った。

ただ、なんとな〜く概念は理解できたけど、使い所がまだピンとこない。

とりあえずcase/whenを書くときに「case/in使えるかも??」ぐらいに思っておくと使うときがくる…かもしれない。

りんどく.rbの参加は久しぶりだったのだけど、著者の伊藤さんが参加されててびっくりした。普通に考えてめちゃくちゃ贅沢な輪読会なのでは…。

「elixerみたいな関数型の言語だとパターンマッチは結構メジャーだから、関数型言語の本一冊読むと理解深まるかも。」というアドバイスを覚えておこうと思う。

Amazon RDS+postgreSQLでデプロイしたアプリのデータベースの中身を確認する方法

はじめに

ec2にデプロイしたアプリのデータベースの中身を見てみようと思ったのだが、なかなか思ったようにできなかった。

その忘備録

経緯

最初、ec2上でpostgreSQLにアクセスしてデータベースを確認しようとしていたのだが、できなかった。

それは当たり前で、「postgreSQLがインストールされているのはec2ではなくてrdsだから」である。

なので、データベースを確認したい場合は、「rdsのpostgreSQLにアクセス」する必要がある。

rdsのアクセス先はエンドポイントとして設定されているので、それを使用すればアクセス可能。

デプロイしたアプリのデータベースの中身の確認方法

  • 接続方法
    • psql -h rdsのエンドポイント -U postgres -d データベース名
    • でいける
  • あとは普通にsql使ってレコードなりを取得すればok
  • rdsのエンドポイントはawsにログインすれば確認できる。

postgreSQLについて理解を深めた

はじめに

これまでにちょいちょいと触ってきたpostgreSQLだけど、その使い方がいまいち理解できていないと感じていた。 なんとなくデータベースを作って、 なんとなくユーザーを作って、 なんとなくデータベースにアクセスして...で使ってきた感覚があり、それがとても気持ち悪かったので理解を深める目的で調べた。

概要:postgreSQL

  • リレーショナルデータベース管理システム
  • 複数のロールによる複数のデータベースの管理ができる
  • どのpostgresqlに、どのロールで、どのデータベースにアクセスするか?の視点が大事
  • postgresqlにアクセスする=データベースのコンソールに入る
  • postgresqlでは、ユーザー+権限=ロール として、ロールのみをあつかう。
  • postgresqlのコンソールに入るには、psql -h ホスト名 -p ポート番号 -U ロール名 -d データベース名 と打つ
  • ターミナルにアクセスする際、ホスト名とポート番号を省略するとローカルのpostgresqlが対象となる

気付き

postgreSQLを使う上で自分に欠けていた大事な視点は、「どのpostgreSQLに、どのロールで、どのデータベースにアクセスするか?」だと気づいた。 これを抑えていれば、postgreSQLのデータベースにアクセスするとき、psql -h ホスト名 -p ポート番号 -U ロール名 -d データベース名 と打つ理由がわかる。

また、Ruby on RailspostgreSQLを扱う際にも、設定ファイルであるdatabase.ymlになんの情報を記述すればいいかがなんとなく見えてきた。 database.ymlについてはまだ理解が浅いので、調べて別記事にしようと思う。

予想と検証

これまでの学習を元に、「postgreSQLRailsで使えるようにするには?」を考えてみる。

予想

おそらく以下を揃え、Railsプロジェクト側でpostgreSQLを受け入れる設定をし、「どのpostgreSQLに、どのロールで、どのデータベースにアクセスするか?」の設定をすれば使えるはず。

  • postgreSQLがインストールされているサーバー
  • Railsプロジェクトで使うデータベースA
  • データベースAにアクセスして中身を変更できる権限を持ったロールB

検証

Rails newするときに、使用するDBにpostgreSQLを指定してあげれば簡単に使えるようになるのは承知の上で、あえて後からsqlite3 -> postgreSQLとDBを変更することをやってみる。

以下手順

  1. Rails new postgres_app
  2. rails db:system:change --to=postgresql #これを打つことでdatabase.ymlとGemfileをpostgreSQLを使用するためのものに書き換えてくれる。
  3. rails g model User name:text address:text age:integer #適当なモデル作成
  4. rails db:migrate

ここでエラーになった。エラー文は、

ActiveRecord::NoDatabaseError: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL: database "postgres_app_development" does not exist

postgreSQLにpostgres_app_developmentっていうデータベースがないよって言われた。 なるほど、rails db:system:change --to=postgresqlpostgreSQLのデータベースまでは作ってくれないらしい。 続き

  1. createdb postgres_app_development #データベース作成
  2. rails db:migrate #再度migrateを試みる。今度は成功。
  3. psql postgres_app_development #データベースコンソールにアクセス
  4. \d users #usersテーブルを確認

ちゃんと作成されている。

感想

postgreSQLについての理解が深まった感触がある。 特に、「どのpostgreSQLに、どのロールで、どのデータベースにアクセスするか?」の視点を持てたのが良かった。

次はdatabase.ymlについて深堀してみたいと思う。