クローラー memo

ウェブサービスを作るにあたり、どっかからデータを引っ張ってきてそれを反映させたいときにはクローリングを作ることで開発が劇的に効率化する。ただし、むやみにデータを引っ張ってくるのはアウトなので、デリケートに使わなければいけない。

参考書は Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例 Amazon CAPTCHA

取り上げられることが多いですがやはりめっちゃ楽しかったですこれは rubyの文法を一通り理解した人にとっては、クローラーだけではなくRuby文法の理解も一層深まりますのでオススメです。

クローラーの大まかな流れは

  1. 対象ページをダウンロード

  2. そのページを読み込んで解析(-> ソースコードの癖を見抜くなど....)

  3. 必要なデータ部分を文字列処理等で抜き出す

  4. データを加工する

  5. 出力する

以外とシンプル! しかし、3.4がなかなか初心者にとっては難しい所で、正規表現等はを上手く扱える必要があります。

奇跡的な競馬サービス作りたいので出馬表とか、過去のレース成績とかのデーター収集のために勉強していきますー。

2つのカスタムバリデーターを1つのメソッド内で処理する方法

カスタムバリデーターは独自のバリデーションを作れるものです。 今まで一つのバリデータを作るたびに1ファイル作成してたのですが、大変非効率だと思っていた所先輩が教えてくれたので共有します。

モデルは銀行口座の管理を行うものです。bank_codeは銀行コード, bank_branch_codeには銀行支店コードが入る想定です。入力フォームでbank_code, bank_branch_codeのいずれかに不正な番号が入力された際に(そのコードは存在しませんよーという)バリデーションを発動させます。

app/models/control_trading_account.rb

ここではカスタムバリデーターを使いますよという合図を出す
class ControlTradingAccount < ApplicationRecord
  validates :bank_code,          presence: true, exist_association: { key: :code, model: 'Bank' } # このBankクラスをバリデーターメソッドに渡したい
  validates :bank_branch_code,   presence: true, exist_association: { key: :code, model: 'BankBranch' }# このBankBranchをバリデーターメソッドに渡したい
end

app/validators/exist_association_validator.rb

さっきまではこれと同じようなメソッドを2つ書いていました....orz
一つのメソッドで2つの処理を実現させるためには、この中で上のBankBankBranchを受け取る!
class ExistAssociationValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    model = options[:model].safe_constantize  # optionsでパラメータを受け取れるとのこと! safe_constantizeは文字列処理
    unless model.find_by(options[:key] => value)  # model変数にBankとBankBranchがそれぞれ入ります
      record.errors[attribute] << (options[:message] || '正しくありません')
    end
  end
end

バリデーターに限らず、optionでパラメーターを取得するのは今後多用しそうなので備忘録としての投稿でしたー。

ごいすー

rake task の作り方, submoduleについて、バルクインサート(activerecord-import)の使用法 

Rake とは?

  • RakeはRuby-Makeの略で、Rubyで何かを作ったり、定型的な処理をしたいときに役立ってくれるツール。

自分でWebサイトを作っているとして、いつもページを更新するたびにやらないといけない、定型的な処理があるとする。 この類の手作業は、Rubyを使っている以上決まった処理はスクリプトにやらせたい!!! railsならDB定義をしたあとにrake db:migrateとか必ずやりますよね...

普段何気なく使っているあのrake db:migrateもどこかで作られているものだということです。

$ rake -T

上記のコマンドでrake taskの一覧が確認できます。 rake routesなどももちろん入っています。

そこで今回は自作のrake taskを作っていきます。

rake taskの作成

lib/tasks内にhoge.rakeの形でファイルを作成

今回は

$ rake master_data:load:banks_and_branchesとすると処理が実行されるように作っていきます。

サンプルコード lib/tasks/master_data.rake

namespace :master_data do
  namespace :load do
    desc '銀行データの作成と更新'
    task :banks_and_branches => :environment do # :environment はモデルにアクセスするのに必須

      yml = YAML.load_file("#{ Rails.root }/submodules/zengin-code/data/banks.yml") #各自読み込みたいymlファイルのパスを設定してください

      MasterBank.transaction do
        MasterBank.delete_all # データは最初にリセットする

        master_banks = yml.each_with_object([]) do |(_key, val), ary|
          ary << MasterBank.new(code: val['code'], name: val['name'], name_kana: val['kana'])
        end

        MasterBank.import master_banks # バルクインサート 下部で説明しています
      end
    end
  end
end

submodules/zengin-code/data/banks.yml

このymlファイルに1000件近い銀行データが入っている。。

---
'0001':
  code: '0001'
  name: "みずほ"
  kana: "ミズホ"
  hira: "みずほ"  # 今回hiraとromaはDBで定義していないのでcreateする前に省く処理をプログラム側で実行します
  roma: mizuho #
'0005':
  code: '0005'
  name: "三菱東京UFJ"
  kana: "ミツビシトウキヨウUFJ"
  hira: "みつびしとうきようUFJ"
  roma: mitsubishitoukiyouufj
0009:
  code: 0009
  name: "三井住友"
  kana: "ミツイスミトモ"
  hira: "みついすみとも"
  roma: mitsuisumitomo

$ rake mater_data:load:banks_and_branches

一応確認してみる

$rails db

# select * from master_banks;
 id | code |      name      |        name_kana         |         created_at         |         updated_at
----+------+----------------+--------------------------+----------------------------+----------------------------
 16 | 0001 | みずほ         | ミズホ                   | 2016-08-04 10:25:00.742385 | 2016-08-04 10:25:00.742385
 17 | 0005 | 三菱東京UFJ | ミツビシトウキヨウUFJ | 2016-08-04 10:25:00.782744 | 2016-08-04 10:25:00.782744
 18 | 0009 | 三井住友       | ミツイスミトモ           | 2016-08-04 10:25:00.79938  | 2016-08-04 10:25:00.79938
(3 rows)

入ってますね!!! これはいろいろ応用できそう!。


サブモジュールとは?

https://git-scm.com/book/ja/v1/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E3%82%B5%E3%83%96%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB

http://qiita.com/go_astrayer/items/8667140aef8875742a36

あるプロジェクトで作業をしているときに、プロジェクト内で別のプロジェクトを使わなければならなくなることがよくあります。サードパーティが開発しているライブラリや、自身が別途開発していて複数の親プロジェクトから利用しているライブラリなどがそれにあたります。こういったときに出てくるのが「ふたつのプロジェクトはそれぞれ別のものとして管理したい。だけど、一方を他方の一部としても使いたい」という問題です。

例を考えてみましょう。ウェブサイトを制作しているあなたは、Atom フィードを作成することになりました。Atom 生成コードを自前で書くのではなく、ライブラリを使うことに決めました。この場合、CPAN や gem などの共有ライブラリからコードをインクルードするか、ソースコードそのものをプロジェクトのツリーに取り込むかのいずれかが必要となります。ライブラリをインクルードする方式の問題は、ライブラリのカスタマイズが困難であることと配布が面倒になるということです。すべてのクライアントにそのライブラリを導入させなければなりません。コードをツリーに取り込む方式の問題は、手元でコードに手を加えてしまうと本家の更新に追従しにくくなるということです。

Git では、サブモジュールを使ってこの問題に対応します。サブモジュールを使うと、ある Git リポジトリを別の Git リポジトリのサブディレクトリとして扱うことができるようになります。これで、別のリポジトリをプロジェクト内にクローンしても自分のコミットは別管理とすることができるようになります。

=> 今回はzengin-code(日本の銀行データが入ってる)というgemをsubmoduleとして登録しました。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

[応用編] 多数レコードをDBに登録する際にはバルクインサートがめちゃ使える

上で、ymlファイルからDBにレコード登録する方法を説明しましたが、多数レコードになる場合はバルクインサートが効率的で良さげです。 端的に言うと、eachによって一回ずつinsert文を実行していたのを、一度のinsert文でクエリをまとめて、DBに流しちゃうというものです。 バルクインサートの方が実行スピードがだいぶ速くなるので、オススメです

activerecord-importというgemを用います。 Gemfile

gem  'activerecord-import'
$ bundle

参考) とてもわかりやすいので参照してみてください。 blog.livedoor.jp

qiita.com

railsでgithubログインを実装する

  1. githubにapplicationを追加する (githubにアクセス-> settings -> Oauth applications-> developer applicationのタブ内のresister new applicationをクリック)

application名とurlとAuthorization callback URLにそれぞれ記入し作成(この時にのちに使うclient-idとclient-secretが生成される)

urlはlocalhost://3000でおk

Authorization callback URLも http://localhost:3000/auth/github/callback とかで大丈夫です

gem 'omniauth-github'  

$ bundle install

userテーブルに新しく3つのカラムを追加していく $ rails g migration add_columns_to_user provider:string uid:string image:string

$ rake db:migrate

app/model/user.rbに書いていく ちなみにuserモデルはdeviseを使っていますのでその前提で書いています>_<。

class User < ActiveRecord::Base

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :omniauthable #最後の:omniauthableのみを追記しました。

def self.find_for_github_oauth(access_token, signed_in_resource = nil)
    data = access_token['info']
    user = User.where(:provider => access_token['provider'], :uid => access_token['uid']).first

    if user
      return user
    else
      registered_user = User.where(:email => data['email']).first
      if registered_user
        return registered_user
      else

        if data['name'].nil?
          name = data['nickname']
        else
          name = data['name']
        end

        user = User.create(
          name: name,
          provider: access_token['provider'],
          email: data['email'],
          uid: access_token['uid'],
          image: data['image'],
          password: Devise.friendly_token[0, 20],
          )
      end
    end
  end
end

続いてinitializers/devise.rbをいじる

#見やすくするために不要なコメント行は全て削除しています
Devise.setup do |config|

  require 'devise/orm/active_record'

  config.case_insensitive_keys = [:email]
  config.strip_whitespace_keys = [:email]
  config.skip_session_storage = [:http_auth]
  config.stretches = Rails.env.test? ? 1 : 10

  config.reconfirmable = true
  config.expire_all_remember_me_on_sign_out = true
  config.password_length = 8..128
  config.reset_password_within = 6.hours
  config.sign_out_via = :delete

  require 'omniauth-github'
  config.omniauth :github, 'Client-id', 'Client-Secret', scope: 'user:email'
 #'Client-id', 'Client-Secret'はgithubでapplicationを登録した時に生成されたものをそれぞれクオーテーション内に貼り付けてください

end

続いてcontrollerを新規で作成 app/controllers/omniauth_callbacks_controller.rbで作成しました

class OmniauthCallbacksController < Devise::OmniauthCallbacksController

  def github
    @user = User.find_for_github_oauth(request.env["omniauth.auth"], current_user)
    if @user.persisted?
      flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Github"
      sign_in_and_redirect @user, :event => :authentication
    else
      session["devise.user_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end

end

ルーティング設定

routes.rb


devise_for :users, :path => '', :path_names => { :sign_in => 'login', :sign_out => 'logout', :edit => 'profile' }, :controllers => { :omniauth_callbacks => 'omniauth_callbacks' }

あとはviewで新規登録(ログイン)リンクを生成するだけです

views/devise/registrations/new.html.erb

<%= link_to "sign up with Github", user_omniauth_authorize_path(:github) %>

装飾は割愛してますのでお好きなようにどうぞ!

以上!! facebookもにこんな感じで実装できちゃうので試してください。 f:id:snsn19910803:20160724134703p:plain

gemのfriendly_idを使ってみた

friendly_idを使うとurlが変わる

例えばrails特有の /projects/1 が/projects/homeworkとかに変わってくれる。

https://rubygems.org/gems/friendly_id/versions/5.1.0 gemfile

gem 'friendly_id', '~> 5.1'

$ rails g friendly_id

$ rails g migration add_slug_to_project slug:string:uniq

app/model/project.rbで


  extend FriendlyId
  friendly_id :name, use: [:slugged, :finders] #projectのname部分がidの代わりのurlとして代用される

を追加

こうするとprojectのname部分がidの代わりとして代用されるようになります before => /projects/1 after => /projects/homework

是非お試しください

カスタムMarkdown機能を実装する方法

gemを使って簡単にmarkdown機能を実装していきます。

まずはgemfileに2つのgemを追加

markdownのgem redcarpet | RubyGems.org | your community gem host

シンタックスハイライトのgem coderay | RubyGems.org | your community gem host

Gemfile

gem 'redcarpet', '~> 3.3', '>= 3.3.4'
gem 'coderay', '~> 1.1'

app/helpers/application_helper.rbにメソッドを書いていきます。(コピペで大丈夫)

module ApplicationHelper

  class CodeRayify < Redcarpet::Render::HTML
    def block_code(code, language)
      CodeRay.scan(code, language).div(:line_numbers => :table)
    end
  end

  def markdown(text)
    coderayified = CodeRayify.new(:filter_html => true, :hard_wrap => true)
    options = {
      :fenced_code_blocks => true, #ここのoptionは任意で好きなようにカスタマイズしてok
      :no_intra_emphasis  => true,
      :autolink           => true,
      :strikethrough      => true,
      :lax_html_blocks    => true,
      :superscript        => true
     }
     markdown_to_html = Redcarpet::Markdown.new(coderayified, options)
     markdown_to_html.render(text).html_safe
  end
end

viewのmarkdownにしたいところでメソッドを呼ぶだけ

app/views/tasks/show.html.erb

   <%= markdown(@task.note)  %>

ここまでで実装は終了です。 フォームのtext_field内で実際に試してみましょう。

f:id:snsn19910803:20160724114202p:plain

viewの画面で確認 f:id:snsn19910803:20160724114210p:plain

できたああああああ

なお ActionView::Template::Error (Symbol or String expected, but NilClass given.のエラーが途中出てきた場合は Block code starting with 4 spaces cannot be rendered when used with CodeRayify · Issue #308 · vmg/redcarpet · GitHub ここを参考にしてみてください。

備忘録

プレイスホルダー

プレイスホルダー → パラメーターの置き場所。条件式に対して、実行時に任意のパラメーターを引き渡すことができる。

例)
scope :active, -> { where('event_date >= ?', Date.today) }
設定されたイベント日を過ぎてしまったら、そのイベントは表示させないようにしたい

?にDate.todayを引き渡している。 またカンマ区切りで複数つなげることもできるし、params[:id]などの引数も取れる。

devise便利メソッド

user_signed_in? => booleanが返り値

current_user => userが返り値(ログインユーザ情報を取得できる)

とりあえずここまで