Rails初心者がNokogiriを使ってbotを作るためにやったこと

タイトルの通りです。

ことの発端は、株主優待の改廃をいち早く知るにはどうしたらいいだろうと悩んだ結果、一番早い情報を得るにはどうしてもスクレイピングが必要だったので自分でbotを作ってしまおうと思ったのがきっかけです。

使ったもの

Railsアプリケーションの作成

sqaleに申し込んでアプリケーションを作成します。 Getting Startedのマニュアルを元にRailsinstallerを使ってrubyRailsのインストール、公開鍵の登録、アプリケーションの作成を済ませます。今回は 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に以下の追記を行います。これでgmailsmtpサーバーを拝借してメール送信ができるようになります。

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は学習コストはそれなりに高いけど、覚えてしまえば強力なフレームワークだなーという印象です。 業務アプリだとさらにテストやらチューニングやら考える必要があるけど、自分用のツールとして使う分にはこれで十分かと思います。