Rails初心者がNokogiriを使ってbotを作るためにやったこと
タイトルの通りです。
ことの発端は、株主優待の改廃をいち早く知るにはどうしたらいいだろうと悩んだ結果、一番早い情報を得るにはどうしてもスクレイピングが必要だったので自分でbotを作ってしまおうと思ったのがきっかけです。
使ったもの
Railsアプリケーションの作成
sqaleに申し込んでアプリケーションを作成します。 Getting Startedのマニュアルを元にRailsinstallerを使ってruby、Railsのインストール、公開鍵の登録、アプリケーションの作成を済ませます。今回は kabunews というアプリケーション名にしました。
Nokogiriのインストール
Nokogiriはruby製の有名なスクレイピングツールです。 以下の記事が参考になりました。
Nokogiri を使った Rubyスクレイピング [初心者向けチュートリアル]
ローカル環境でrails new .
したあと、Gemfileに以下の1行を追加します。
gem 'nokogiri'
Gemfileに追加したら以下のコマンドを実行すればNokogiriが使えるようになります。
$ bundle install
モデルの作成
スクレイピングして新着情報を検知するためには「前回どこまで調べたか」を保存する必要があります。 今回はRSSの出力も行いたいので、RSSフィードの形式に沿ってテーブルを作成します。
- feedテーブルの定義(案)
id int プライマリキー feed_id str feedを識別するためのユニーク値 title str タイトル description text 本文 link str リンク先URL
テーブル定義を考えたら以下のコマンドでModelを作成します。
$ rails generate model テーブル名 [フィールド名:型] [フィールド名:型]... $ rails generate model feed feed_id:string title:string description:text link:string
Modelを作成するとdb/migrate/
以下にマイグレーションファイルも自動的に作成されます。
class CreateFeeds < ActiveRecord::Migration def change create_table :feeds do |t| t.string :feed_id t.string :title t.text :description t.string :link t.timestamps end end end
アプリケーションのルートでrake db:migrate
と実行すると、マイグレーションファイルを元にテーブルが作成されます。
コントローラーの作成
テーブルが作成できたら次はスクレイピングを実行しDBに保存するためのコントローラーを作成します。 以下のコマンドでコントローラーの雛形を作成します。
$ rails generate controller コントローラー名 [アクション名] $ rails generate controller fetch
次にRailsではどのURLパスにアクセスがきた時どのアクション・コントローラーを起動するかのマッピングをconfig/routes.rb
というファイルで管理しているので、routes.rbに以下の1行を追加しておきます
get ':controller(/:action(/:id(.:format)))'
これでhttp://mysite.com/fetch
にリクエストが来た時にfetchコントローラーのindexアクションが起動するようになります。
あとはapp/controllers/fetch_controller.rb
に処理を書いていく訳ですが、こんな感じに書きました。
NokogiriはCSSセレクタが使えるのでjQueryを書いたことのある人なら何も考えずに使えると思います。
class FetchController < ApplicationController require 'open-uri' # URLアクセス require 'kconv' # 文字コード変換 require 'nokogiri' # スクレイピング def index # スクレイピング先のURL url = 'http://example.com/news/index.html' html = open(url) do |f| f.read # htmlを読み込んで変数htmlに渡す end # htmlをパース(解析)してオブジェクトを生成(utf-8に変換) doc = Nokogiri::HTML.parse(html.toutf8, nil, 'utf-8') latest_id = get_latest_id() # 新着情報ごとにループ doc.css('#contents a').each do |content| # <a name="news123"> の123の部分を取得 feed_id = content["name"].sub(/news/, "").to_i if latest_id < feed_id # DBに未登録の情報があったらDBに保存 title = content.css('h1').to_html description = content.to_html link = url + '#news' + feed_id.to_s insert_feed(feed_id, title, description, link) end end render :text => "Done!" end private # feedsテーブルに1件INSERT def insert_feed(feed_id, title, description, link) feed = Feed.new( :feed_id => feed_id, :title => title, :description => description, :link => link ) feed.save end # DBに保存されている最新のfeed_idを取得 def get_latest_id() row = Feed.order("feed_id desc").first if row.nil? return 0 end latest_id = row["feed_id"].to_i return latest_id end
なお、insert_feed()とget_latest_id()というメソッドの中ではActiveRecordを使ってDB操作を行っていますがActiveRecordについてはググってください。
メールを送信する
新着情報があったらいち早く知りたいので、DBに保存するタイミングで自分にメールを送るようにします。 方法は色々あるようですが、Railsに標準で搭載されているActionMailerという機能を使います。
$ rails generate mailer メーラー名 $ rails generate mailer NoticeMailer
上記コマンドを実行するとapp/mailers/notice_mailer.rb
というファイルが作成されるので、メールを送るためのメソッドを定義します。
# -*- coding: utf-8 -*- class NoticeMailer < ActionMailer::Base default from: "hoge@gmail.com" def send_gmail(to, title, description) @description = description mail(:to => to, :subject => title) end end
宛先と件名をmail()メソッドに渡すとviews/notice_mailer/send_gmail.text.erb
に置かれたテンプレートを元に本文が組み立てられ、メール送信が実行されます。今回は受け取った本文をそのまま送信するのでsend_gmail.text.erbの中身はこれだけです。
<%= @description %>
メールを実際に送信するためにはsmtpサーバーの指定が必要になるので、config/environments/development.rb
に以下の追記を行います。これでgmailのsmtpサーバーを拝借してメール送信ができるようになります。
config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { :enable_starttls_auto => true, :address => 'smtp.gmail.com', :port => 587, :domain => 'gmail.com', :authentication => 'plain', :user_name => 'hoge@gmail.com', :password => 'xxxxxxxxx' }
最後に、作成したsend_gmail()をコントローラーの中で呼び出せば完成です。
# DBに保存 insert_feed(feed_id, title, description, link) + # メール送信 + NoticeMailer.send_gmail('hoge@gmail.com', title, description).deliver
本番デプロイ&cronの設置
ここまでできたらいよいよsqaleにデプロイしていきます。 ローカルに貯めたコミットをsqaleにpushすれば勝手にサーバーへのデプロイからビルドまで行ってくれます。
$ git push origin master
なお初回は本番環境にまだテーブルがないのでdb:migrateを実行してテーブルを作成しておきます。
$ ssh -p 2222 sqale@gateway.sqale.jp Last login: Sun Apr 20 16:57:28 2014 from xxx.xxx.xxx.xxx sqale@kabunews-kazsix-1:~$ cd current/ && bundle exec rake db:migrate
railsアプリケーションを定期実行させるにはwheneverというgemを使う方法やrails runnerを使う方法など色々あるようですが、今回は単純にhttp://mysite.com/fetch
を叩ければよいので以下のようにcrontabを編集しました。
sqale@kabunews-kazsix-1:~$ crontab -e */5 * * * * wget --spider "http://mysite.com/fetch" >/dev/null 2>&1
ちなみにHerokuでは無料枠で実行できるcronは1日1回までですが、sqaleは無制限です。ありがたいですね。
おまけ:RSSフィードを生成する
せっかくなのでお知らせをRSSでも受け取れるようにします。
xmlファイルを定期的に生成して静的ファイルとして設置することも可能ですが、http://mysite.com/rss
にリクエストが来た時に動的にDBの情報からフィードを生成したいと思います。静的ファイル(状態)を持たないことでアプリケーションサーバーをステートレスに保つ事にもなります。
まず以下のコマンドでコントローラーとアクションを作成します。
rails generate controller コントローラー名 [アクション名] rails generate controller fetch index
次に生成されたapp/controllers/rss_controller.rb
に処理を記述していきます。
feedsテーブルから最新5件を取得し、テンプレートを呼び出すだけです。
class RssController < ApplicationController def index @entries = Feed.order('created_at DESC').limit(5).all render :template => 'rss/index.builder', :layout => false end end
テンプレートはviews/rss/index.builder
というファイル名で以下のように作成します。
xml.instruct! :xml, :version => "1.0" xml.rss :version => "2.0" do xml.channel do xml.title "HPタイトル" xml.description "HPの説明" xml.link "http://example.com/news/index.html" # HPのURL for e in @entries xml.item do xml.title e.title # 記事のタイトル xml.description e.description_html # 記事の本文 xml.pubDate e.created_at.to_s(:rfc822) # 記事の作成日 xml.link e.link # 記事のURL xml.guid e.link end end end end
これでhttp://mysite.com/rss
にアクセスすると最新5件のフィード情報がRSS形式で表示されるようになりました。
完成したもの
という訳で作成したアプリケーションがこちらになります。
株主優待新着お知らせ君
http://kabunews-kazsix.sqale.jp/
株主優待新着お知らせ君を利用すると以下の様なメリットがあります。
- 自分の持ってる銘柄が優待廃止・改悪された時、値崩れする前に売れる
- 優待が新設・改良された時、値上がりする前に買える
- 自分的には魅力がない優待でも、キャピタルゲイン狙いで売り買いできる
Railsは学習コストはそれなりに高いけど、覚えてしまえば強力なフレームワークだなーという印象です。 業務アプリだとさらにテストやらチューニングやら考える必要があるけど、自分用のツールとして使う分にはこれで十分かと思います。