Ruby

capybaraを使ってWebページをスクレイピングする(テストじゃなくて)

capybaraはWebアプリケーションのテスト用のライブラリとして有名ですが、テスト目的でなく、単にスクレイピング用のツールとしても使えます。この記事ではcapybara+selenium+chromeヘッドレスを使用してWebページをスクレイピングするサンプルを紹介します。

動作環境

  • Mac OS High Sierra 10.13
  • ruby 2.5

capybaraのセットアップ

まずはcapybaraのセットアップからです。selenium-webdriver経由でヘッドレスchromeを操れるように設定します。最初に必要なライブラリをhomebrewでインストールしておきます。

📄Gemfile
$ brew install chromedriver

次に必要なgemをインストールするため、Gemfileを作ります。

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'capybara'
gem 'selenium-webdriver'
gem 'launchy'

lauchyというのはスクリーンショットを取ったと同時にプレビュー.appで開くようにしてくれるgemです。いつも通りbundle installしてください。

これでcapybaraを使用する準備ができたので、capybaraの初期設定をします。

📄scrape.rb
require 'capybara'
require 'selenium-webdriver'

Capybara.register_driver :selenium do |app|
  Capybara::Selenium::Driver.new(app,
    browser: :chrome,
    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
      chrome_options: {
        args: %w(headless disable-gpu window-size=1280,800),
      },
    )
  )
end
Capybara.javascript_driver = :selenium

chrome_options の詳しい説明などは こちら で確認してみてください。

ひとまず、これでcapybaraを使用する準備が整いました。

使ってみる

ではgoogleのトップページにアクセスして、スクリーンショットを保存してみましょう。先程のscrape.rbに次のコードを追記します。

📄scrape.rb
...

def start_scraping(url, &block)
  Capybara::Session.new(:selenium).tap { |session|
    session.visit url
    session.instance_eval(&block)
  }
end

start_scraping 'https://www.google.com/' do
  # ここにスクレイピングのコードを書く
  p title #=> "Google"
  save_and_open_screenshot
end

Capybara::Sessionを開始して、Googleのページのスクリーンショットを保存します。ヘッドレスなので実際にChromeは可視化しませんが、ちゃんとアクセスできているのがスクリーンショットを見れば分かると思います。

あとは煮るなる焼くなり好きにして下さい。capybaraの使い方に関しては解説サイトもたくさんあるので、ググってみてください。

github

サンプルプロジェクトをpushしていますので、よかったら使ってください。

itmammoth/capybara-bootstrap

capybaraを使ってWebページをスクレイピングする(テストじゃなくて) Read More »

【factory_girl】子のファクトリ生成時に親の属性をまとめて設定する

タイトル読んでも「ちょっと何言ってるか分かんない」感じだが、要するにhas_oneやhas_manyで親子関係を持つモデルがあったとして、子のファクトリ生成時に親側の属性も設定するという話(その逆もしかり)。

たとえばUserクラスとConfigクラスがあったとして、それぞれ次のような関連を持ってるとする。

📄spec/factories/config.rb
class User < ApplicationRecord
  has_one :config
end

class Config < ApplicationRecord
  belongs_to :user
end

各ユーザーが各々設定を持つというシチュエーションを想定している。で、この場合に親のファクトリをcreate(:config)のような感じで作るときに、まとめてUser側の属性も設定したいということ。

通常の書き方だと、Configのインスタンスを得るにはこんな感じになるはず。(たぶん)

config = create(:user. name: '...', email: '...',
  config: build(:config, attr1: '...', attr2: '...')).config

それを次のように書けるようにする。

config = create(:config, attr1: '...', attr2: '...',
  user: { name: '...', email: '...' })

「なんだ、ほとんど変わんねえじゃねーか」とお思いになるかもしれない。しかし前者は、Configのインスタンスが欲しいんだということがひと目で分からない。また、build :config してるのもいささか冗長だ。後者の方がテストの関心がConfigにあることが一目瞭然だし、タイプ量も少なくてすむ。細かいことだけど、こういうのって結構大事だと思うんだな。

ファクトリ定義

で、肝心のファクトリの定義だが、私はこんな感じに定義している。

FactoryGirl.define do
  factory :config do
    attr1 '...'
    attr2 '...'

    transient do
      user { {} }
    end

    after(:build) do |config, evaluator|
      config.user = build(:user, evaluator.user) if evaluator.user.present?
    end
  end
end

userをtransientにしておきuserの属性(Hash)を渡してもらう。after(:build)の中でuserのファクトリに横流してbuildしているのでuserのファクトリ定義を上書きすることができるというわけ。なお親から子の属性を設定する場合も同様の方法が使えると思う。

これがベストの解かは分からない。もっといい方法があれば教えて欲しいッス。

【factory_girl】子のファクトリ生成時に親の属性をまとめて設定する Read More »

【Rails】リロード時に特定の処理を実行する

railsアプリを開発していると、自作DSLだとか設定ファイルを読み込んで何らかの処理を実行するということがよくある。その際に問題になるのは、設定ファイルを変更するたびにいちいちサーバを再起動しないといけないということ。小さなアプリならまだしも、そこそこの規模になってくると、再起動にも結構時間がかかてしまい、鬱陶しいことこの上ない。そこで特定のファイルを監視しておき、更新されていたら次回リクエスト時に特定の処理を走らせることにする。

環境

  • ruby on rails 4, 5

For Rails 5

まずは rails 5 のコードから。次のコードを適宜編集して、config/initializersの下に置く。

📄config/initializers/reloader_rails_5.rb
unless Rails.env.production?
  reloader = Rails.application.config.file_watcher.new(['path_to_file']) do
    # do_something
  end
  Rails.application.reloaders << reloader

  ActiveSupport::Reloader.to_prepare do
    # reloader.execute <=他のファイルが更新されても実行するならこちら
    reloader.execute_if_updated
  end
end

なにをしているかというと、まずRails.application.config.file_watcherでファイルの更新監視クラスを取ってきている。普通はconfig/environments/development.rbには次のような設定がしてあるはずなので、

📄config/environments/development.rb
config.file_watcher = ActiveSupport::EventedFileUpdateChecker

ActiveSupport::EventedFileUpdateCheckerクラスが取得できるというわけだ。こいつに監視対象のファイルパスを渡し、ブロック内に実施したい処理を書けばよい。

次に、ActiveSupport::Reloader.to_prepare の部分だが、何らかのファイルに更新があった次のリクエスト処理時に、このブロックが実行されることになっている。ここで先ほど生成したreloaderを実行してやればよい。

reloader.execute_if_updatedというのは監視対象のファイルが更新された場合にだけreloaderを実行する。通常はこのメソッドを使えばよいと思う。他のファイルが更新された場合でもreloaderを実行したい場合はreloader.executeだ。

For Rails 4

基本的にはrails 5と同じなのだが、少しだけ使用するクラスが違う。

📄config/initializers/reloader_rails_4
unless Rails.env.production?
  reloader = ActiveSupport::FileUpdateChecker.new(['path_to_file']) do
    # do_something
  end
  Rails.application.reloaders << reloader
 
  ActionDispatch::Reloader.to_prepare do
    reloader.execute_if_updated
  end
end

やってることはrails 5と同じなので、説明は省略。

実例(rails 5)

📄config/initializers/reloader.rb
ymls = Dir.glob("#{Rails.root}/app/dsls/config/search/*.rb")
ymls.each { |yml| load yml }

unless Rails.env.production?
  ymls.each do |yml|
    reloader = Rails.application.config.file_watcher.new([yml]) do
      load yml
    end
    Rails.application.reloaders << reloader

    ActiveSupport::Reloader.to_prepare do
      reloader.execute
    end
  end
end

実際に私が使用している例。自作DSLがyamlファイルを読み込むのだが、yamlを変更する際にサーバを再起動しないでいいように、上のようにしてみた。

もともとは、settingslogicという定数管理用のgemを使用していたのだが、yamlを変更するとサーバを再起動しないといけないのが嫌で、解決策を探してみた。

ちなみに拙作の定数管理gemは、サーバを再起動しなくても、定数を記述するyamlを変更したら自動でリロードするようにしてある。

その中のここのソースでrails4と5を切り替えてreloaderを登録するようにしているので、gemを作る人なんかには参考になるかもしれない。

【Rails】リロード時に特定の処理を実行する Read More »

【Rails 5】ヘルパーを使用しているactive_decoratorをrspecでテストする

active_decoratorにてデコレートされたモデルをrspecでテストする際、デコレータ内でヘルパーメソッド(link_to とかurl_for とか)を使用しているとNoMethodErrorが発生する。これはActiveDecorator::ViewContext にActionView::Context が積まれてないことが原因みたい。rspec専用のgem(mizoR/active_decorator-rspec)もあるみたいだけど、まだrails 5には対応できてない((active_decorator-rspec 0.0.9で対応したようです。))(もしくはrspec 3.5?)ようなので、対応策をメモっておく。

環境

  • rails 5.0.0.rc1
  • rspec 3.5.0.beta3
  • active_decorator 0.7.1

対応方法

まずはrspecようのヘルパーモジュールを書く。私の場合はsupportディレクトリ内に置いて読みこむようにしてるので、spec/support/decorator_spec_helper.rb に書く。

📄spec/support/decorator_spec_helper.rb
module DecoratorSpecHelper
  def self.included(base)
    base.class_eval do
      before do
        controller = ApplicationController.new
        controller.request = ActionDispatch::TestRequest.create
        ActiveDecorator::ViewContext.push controller.view_context
      end
    end
  end
end

テスト用のコントローラとリクエストを生成して、そこからViewContextをガメるという作戦。

続いてrails_helper.rb に以下のコードを追加する。

📄spec/decorator/hoge_decorator_spec.rb
config.include DecoratorSpecHelper, type: :decorator

あとはテストしたいデコレーターにtype: :decorator を忘れずに付けるだけ。

require 'rails_helper'

RSpec.describe HogeDecorator, type: :decorator do
...
    let(:video) do
      ActiveDecorator::Decorator.instance.decorate(Hoge.new)
    end
...

以上。

【Rails 5】ヘルパーを使用しているactive_decoratorをrspecでテストする Read More »