Selenium+ChromeDriverでスクレイピングをしてみる
はじめに
Nokogiriについて学ぶ \- らせんびよりで、Nokogiriでは情報の取得が難しいサイトがあることを学んだ。
今回は、そういったサイトにも対応できる「Selenium+ChromeDriverでスクレイピングをする方法」について学んでいく。
SeleniumとChromeDriverについて
- Selenium
- Webブラウザで行うクリック操作やキーボード入力などをプログラム上から操作できるようにするライブラリ
- rubyではselenium-webdriver gemを使うことで使用可能
- Seleniumチートシート \[Ruby\] \| 酒と涙とRubyとRailsと
- ChomeDriver
この二つを組み合わせることで、プログラム上で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には要素の取得方法が色々と用意されている。
対象は、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に実際に触って理解を深めた。
できること
公式の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::HTML
とURI#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としてパースされてるのがわかる。
ノードを検索する
- https://nokogiri.org/tutorials/searching_a_xml_html_document.html
- xpathやCSSのクエリを使って検索する。
- ためしに自作サービスのうつメイトのトップページから文言を抜き出してみる
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
読み込みの仕組み
rubyのrequire
の読み込み対象は、rubyの組み込み変数$LOAD_PATH
内のパスを起点とし、相対パスでファイルを検索する。
検索の仕組みは単純で、
- $LOAD_PATH($:)を順に辿る
- 目的のファイルが見つかったら終了
- 最後まで来たら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はグローバルにインストールして使うのが主流っぽい。
参考:
- https://qiita.com/jnchito/items/62adbea043abf72fa7cc
- https://qiita.com/jnchito/items/99b1dbea1767a5095d85
Railsの場合は?
Railsプロジェクトにおいても、bundle installのインストール先は$ gem environment
の出力結果のINSTALLATION DIRECTORY
。
railsではgemをrequireする必要はない。
理由はconfig/application.rb
にGemfile内のgemを一括で読み込む設定がなされているため。
参考:
- https://zenn.dev/toshi023/articles/d99b00510aafb3
- https://qiita.com/kaishuu0123/items/2a91495e7daa8c7783ed
感想
gemのインストール先を調べていくうちに、requireの仕様まで学ぶことができてよかった。
また、gemはグローバルにおいて使うのが主流ということもわかった。なんとなーく「グローバル汚染するのはよくないのかなあ?」と思っていたのがスッキリした。
Amazon RDS+postgreSQLでデプロイしたアプリのデータベースの中身を確認する方法
はじめに
ec2にデプロイしたアプリのデータベースの中身を見てみようと思ったのだが、なかなか思ったようにできなかった。
その忘備録
経緯
最初、ec2上でpostgreSQLにアクセスしてデータベースを確認しようとしていたのだが、できなかった。
それは当たり前で、「postgreSQLがインストールされているのはec2ではなくてrdsだから」である。
なので、データベースを確認したい場合は、「rdsのpostgreSQLにアクセス」する必要がある。
rdsのアクセス先はエンドポイントとして設定されているので、それを使用すればアクセス可能。
デプロイしたアプリのデータベースの中身の確認方法
postgreSQLについて理解を深めた
はじめに
これまでにちょいちょいと触ってきたpostgreSQLだけど、その使い方がいまいち理解できていないと感じていた。 なんとなくデータベースを作って、 なんとなくユーザーを作って、 なんとなくデータベースにアクセスして...で使ってきた感覚があり、それがとても気持ち悪かったので理解を深める目的で調べた。
概要:postgreSQL
- リレーショナルデータベース管理システム
- 複数のロールによる複数のデータベースの管理ができる
- どのpostgresqlに、どのロールで、どのデータベースにアクセスするか?の視点が大事
- postgresqlにアクセスする=データベースのコンソールに入る
- postgresqlでは、ユーザー+権限=ロール として、ロールのみをあつかう。
- postgresqlのコンソールに入るには、
psql -h ホスト名 -p ポート番号 -U ロール名 -d データベース名
と打つ - ターミナルにアクセスする際、ホスト名とポート番号を省略するとローカルのpostgresqlが対象となる
気付き
postgreSQLを使う上で自分に欠けていた大事な視点は、「どのpostgreSQLに、どのロールで、どのデータベースにアクセスするか?」だと気づいた。
これを抑えていれば、postgreSQLのデータベースにアクセスするとき、psql -h ホスト名 -p ポート番号 -U ロール名 -d データベース名
と打つ理由がわかる。
また、Ruby on RailsでpostgreSQLを扱う際にも、設定ファイルであるdatabase.yml
になんの情報を記述すればいいかがなんとなく見えてきた。
database.yml
についてはまだ理解が浅いので、調べて別記事にしようと思う。
予想と検証
これまでの学習を元に、「postgreSQLをRailsで使えるようにするには?」を考えてみる。
予想
おそらく以下を揃え、Railsプロジェクト側でpostgreSQLを受け入れる設定をし、「どのpostgreSQLに、どのロールで、どのデータベースにアクセスするか?」の設定をすれば使えるはず。
- postgreSQLがインストールされているサーバー
- Railsプロジェクトで使うデータベースA
- データベースAにアクセスして中身を変更できる権限を持ったロールB
検証
Rails newするときに、使用するDBにpostgreSQLを指定してあげれば簡単に使えるようになるのは承知の上で、あえて後からsqlite3 -> postgreSQLとDBを変更することをやってみる。
以下手順
Rails new postgres_app
rails db:system:change --to=postgresql
#これを打つことでdatabase.ymlとGemfileをpostgreSQLを使用するためのものに書き換えてくれる。rails g model User name:text address:text age:integer
#適当なモデル作成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=postgresql
はpostgreSQLのデータベースまでは作ってくれないらしい。
続き
createdb postgres_app_development
#データベース作成rails db:migrate
#再度migrateを試みる。今度は成功。psql postgres_app_development
#データベースコンソールにアクセス\d users
#usersテーブルを確認
ちゃんと作成されている。
感想
postgreSQLについての理解が深まった感触がある。 特に、「どのpostgreSQLに、どのロールで、どのデータベースにアクセスするか?」の視点を持てたのが良かった。
次はdatabase.ymlについて深堀してみたいと思う。