https://a.storyblok.com/f/270183/50025/161969ad4b/dev_web-scraping-app_1200x600.png

Rubyでウェブをスクレイピングして更新情報を取得し、SMSでアラートを送る

最終更新日 May 18, 2021

所要時間:8 分

誰もが経験したことがあるだろう。目を覚ますと新しい一日が始まっていて、最も切実な疑問の答えを知りたくなる。週末はまだか?今週は仕事もプライベートも忙しかった。あなたがしたいことは、2、3日ゆっくりとくつろぐことだ。

その質問に答えるには、もちろん携帯電話のカレンダーアプリを開いたり、お気に入りのパーソナルデジタルホームアシスタントに尋ねることもできる。しかし、代わりにテキストメッセージを送ってくれるアプリを自分で作れるのに、なぜそんなことをするのだろう?

これから、以下のようなRuby on Railsアプリケーションを作成する:

  • 新規購読者とリストからの退会者の両方を許可する。

  • 質問に対する答えを isittheweekend.com

  • スクレイピングされたデータからの回答を、購読しているすべての受信者に毎日送信します。

すべてをまとめるために、これらのタスクを一度に実行するRakeタスクも作成し、24時間に1回実行するように指定する。

もしお望みなら、このアプリケーションの完全動作版を GitHub.

始めよう!

前提条件

Railsアプリケーションを生成する

最初にすべきことは、新しいRailsアプリケーションを作成することです。コマンドラインから以下を実行します:

rails new weekend-checker-app --database=postgresql

これでRailsアプリケーションに必要なファイル構造が作成され、デフォルトのデータベースがPostgreSQLに設定された。

それが終わったら cdを作成したディレクトリに追加します。依存関係をインストールする前に、アプリケーションが使用する追加のgemを追加します。

お好きなコードエディターでコードを開き Gemfile.に移動します。 Gemfileに以下のgemを追加する:

gem 'nexmo'
gem 'watir'
gem 'webdrivers', '~> 4.0'
gem 'whenever', require: false
gem 'dotenv-rails'

私たちは nexmogemを使ってSMSの更新を送信しています。 watirwebdriversgem、動的なJavaScriptコンテンツを含むサイトへのHTTPリクエストを行うための whenevergemはRakeタスクをスケジュールするために、そして dotenv-railsgemで環境変数を管理しています。

を保存したら Gemfileを保存したら、コマンドラインから bundle installをコマンドラインから実行する準備ができた。

次のステップは、データベース・スキーマとモデルの作成です。

データベース・スキーマとモデルの作成

Railsアプリケーションが作成され、依存関係がインストールされたので、次のタスクはアプリケーションの運用に必要なデータを格納する正しいデータベーススキーマを作成することです。以下のタイプの情報を格納する必要があります:

  • 受信者: 購読者のリストと電話番号

  • DiffStorage: 変更があったかどうかを判断するために比較するウェブサイトデータのコピー

Railsジェネレータツールを使ってマイグレーションファイルを作成し、その後で各ファイルを編集します。

rails generate model Recipient number:string subscribed:boolean rails generate model DiffStorage website_data:text

これらのコマンドは app/modelsに、マイグレーションファイルは db/migrate.これらの変更をアプリケーションにコミットする前に、ジェネレーターのアクションが完了したら、両方のディレクトリに作成されたファイルが正しいことを確認します。

具体的には、マイグレーションファイルの中で、各マイグレーションが以下を含むようにしたい。 t.timestampsが含まれるようにします。 created_atupdated_atカラムが追加されます。また numbersubscribedカラムも Recipientマイグレーションファイルの stringbooleanに設定されている。同様に、マイグレーションファイルには DiffStorageマイグレーションファイルの website_dataの列があるはずです。 text.

の中のモデル・ファイルは空でなければならない。 app/modelsからの継承とクラス宣言以外は空でなければなりません。 ApplicationRecord.

満足のいく結果が得られたら、いよいよコマンドラインから rake db:migrateをコマンドラインから実行しよう。コマンドは結果をコンソールに出力します。 db/schema.rbファイルを調べると、作成したスキーマがアプリケーション内で初期化されていることがわかります。

最後に MessengerScraperモデルを作成する必要がありますが、これらのマイグレーションは必要ありません。そのためには、Railsジェネレータをもう一度実行し、次のフラグを追加します。 --migration=falseフラグを追加します:

rails generate model Messenger --migration=false rails generate model Scraper --migration=false

いよいよモデル内部のロジックを定義する時が来た。

モデルの定義

上述したように、私たちはアプリケーションのユニークな領域を担当する4つのモデルを持っている:

  • DiffStorage: ウェブサイトのデータに相違がないかチェック

  • Recipient: 購読者の追加と削除を管理

  • Messenger: SMSメッセージの送信管理

  • Scraper: ウェブサイトのデータスクレイピングを担当

DiffStorage モデルの定義

この DiffStorageモデルには2つのクラスモデルが含まれる。1つはスクレイピングするURLを含む。もう1つは、前回ウェブサイトがスクレイピングされたときからの変更をチェックし、条件が満たされたときにアプリケーションの次のステップを呼び出します。

まず、URLを独自のメソッドで定義し、そのメソッドが存在する場所を1つ作って、後で変更する場合に簡単に変更できるようにしよう:

def self.url
  'http://isittheweekend.com'
end

次に、このモデルの大部分は #check_last_recordクラスメソッドの中にあります:

def self.check_last_record
  today_answer = Scraper.call(self.url)
  if DiffStorage.any?
    yesterday_answer = DiffStorage.last
  else
    yesterday_answer = ''
  end
  Messenger.send_update_message(Recipient.all, yesterday_answer, today_answer)
end

上記のメソッドは、まず Scraperクラスのメソッドを呼び出し、最新のスナップショットを取得します。 today_answer.そして、次のステップを ifにレコードがあるかどうかを尋ねる DiffStorage.そこに以前のレコードが保存されている場合、メソッドは最新のものを取得し、それを yesterday_answer.以前のレコードがない場合は、空の文字列が.NETに代入される。 yesterday_answer.最後に、受信者と2つの変数を Messengerモデルに送信します。

スクレーパーモデルの定義

この Scraperからデータを収集する作業は、モデルが担当する。 isittheweekend.comからデータを収集し、それが本当に週末であるかどうかを決定する作業を行います。モデルには4つのクラスメソッドがあり、それぞれをここで定義します:

require 'nokogiri'
require 'webdrivers/chromedriver'
require 'watir'

class Scraper < ApplicationRecord
  def self.call(url)
    self.get_url(url)
  end

  def self.get_url(url)
    doc = HTTParty.get(url)
    browser = Watir::Browser.new :chrome, headless: true
    browser.goto(url)
    parsed_page ||= Nokogiri::HTML.parse(browser.html)
    answer = parsed_page.css('h1#isit').text
    self.check_text(answer)
  end

  def self.check_text(data)
    if data == '' || data == nil
      puts "There was no text received from the web scrape."
      exit
    else
      puts "There was data in the text received from the web scrape."
      self.store_text(data)
    end
  end

  def self.store_text(text)
    record = DiffStorage.new
    record.website_data = text
    if record.save
      puts "Record Updated Successfully"
    end
    return record
  end
end

スクレイピングの各アクションは、それぞれの小さなメソッドとして定義されている。この #callメソッドはクラスの入口です。これは、それ自身の外側の他のメソッドから呼び出されるものです。この #get_urlメソッドは、ChromeブラウザーのリクエストをシミュレートしてHTTPリクエストを作成し Watirライブラリを使ってHTTPリクエストをシミュレートし、それを Nokogiri.で解析する。 #check_textメソッドはデータが取得されたかどうかをチェックします。メソッドは #store_textメソッドはそのデータをデータベースに保存します。

メッセンジャー・モデルの定義

この Messengerモデルの中に、毎日のSMS更新を購読者に送信するすべてのコードがあります。メッセージを送信するメソッド、週末のレスポンステキストを作成するメソッド、メッセージ全体をまとめるメソッド、購読者が削除リクエストを送信した場合の確認メッセージを管理するメソッドを作成します。

まず、更新メッセージを送信するメソッドだ:

def self.send_update_message(recipients, yesterday, today)
  @client = Nexmo::Client.new(
    api_key: ENV['NEXMO_API_KEY'],
    api_secret: ENV['NEXMO_API_SECRET']
  )
  puts "Sending Message to Each Recipient"
  recipients.each do |recipient|
    if recipient.subscribed == true
      client.sms.send(
        from: ENV['FROM_NUMBER'],
        to: recipient.number,
        text: self.weekend_message(yesterday, today)
      )
      puts "Sent message to #{recipient.number}"
    end
  end
end

上記の textというクラスメソッドを参照しています。 #weekend_message.このメソッドは、今日が昨日と同じかどうかをチェックすることによって、週末の更新のための文字列を構成します:

def self.weekend_message(yesterday, today)
  if today == yesterday
    response = "Today is the same as yesterday, and the answer is #{today}."
  elsif today =! yesterday
    response = "Today is not the same as yesterday, the answer for today is #{today}."
  else
    response = 'Today and yesterday are both neither affirmative or positive. Are we in an alternative dimension of time and space?'
  end
  self.compose_message(response)
end

次に HEREDOC文字列が含まれる:

def self.compose_message(response)
  <<~HEREDOC
  Hello! 
  It is a new day, but is it a weekend day?
  #{response} 
  To be removed from the list please respond with "1".
  HEREDOC
end

最後に、削除確認メッセージを送信する方法です:

def self.send_removal_message(to)
  @client.sms.send(
    from: ENV['FROM_NUMBER'],
    to: to,
    text: 'You have been successfully removed.'
  )
end

次のステップに進む前に、最後のモデルを定義する必要がある。 Recipientモデルです。

レシピエント・モデルの定義

このモデルにはクラスメソッドは含まれていません。このモデルに追加する唯一のことは、受信者のデータに2つのバリデーションを追加することです。これらのバリデーションは、新しい電話番号をデータベースに追加する際のセーフガードとして機能します。a)確かにデータから番号が提供されていること、b)その番号がすでに存在するレコードと重複していないことを確認します。これらのバリデーションを行うために、クラス定義の下に2行追加します:

class Recipient < ApplicationRecord
  validates :number, presence: true
  validates :number, uniqueness: true
end

コントローラーとルートの作成

アプリの構築も終わりに近づいています!次のステップは、アプリケーションの流れを決めるコントローラのアクションを定義することです。まず、コマンドラインからRailsジェネレータを使ってコントローラを生成しましょう:

rails generate controller WeekendChecker

これは app/controllersという weekend_checker_controller.rbという名前の空のコントローラファイルが作成され app/views/weekend_checker.まもなくインデックスビューを追加します。この時点では、コントローラに集中します。

コントローラーは、3つのルートに対応する3つのアクションを必要とする: #index, #create#event.そして #indexルートはデフォルトで、ウェブサイトの唯一のビューになります。そこが、個人がリストを購読できる場所になります。ルートは #createルートは新しいNumbersが処理される場所になります。最後に #eventルートは、SMS APIからWebhookデータを受け取り、それを処理する場所になります。

class WeekendCheckerController < ApplicationController

  def index
  end

  def create
    @recipient = Recipient.new(recipient_params)
    if @recipient.save
      flash[:notice] = "Phone number saved successfully."
    else
      flash[:alert] = "Form did not save. Please fix and try again."
    end
    redirect_to '/'
  end
  
  def event
    if params[:text] == '1'
      recipient = Recipient.find_by(number: params[:msisdn])
      if recipient
        if recipient.update(subscribed: false)
          Messenger.send_removal_message(params[:msisdn])
        end
      end
    end
    puts params

    head :no_content
  end

  private

  def recipient_params
    params.permit(:number, :subscribed)
  end
end

これら3つのコントローラのアクションには、次のように定義された3つの対応するルートが必要である。 config/routes.rb:

Rails.application.routes.draw do
  get '/', to: 'weekend_checker#index'
  get '/webhooks/event', to: 'weekend_checker#event'
  post '/recipient/new', to: 'weekend_checker#create'
end

アプリケーションコードのセットアップの最後の項目は /ルートの基本的なビューを作ることです。

ビューの定義

SMSリストに登録するために、登録フォームを含むURLのトップレベルでアクセスできるビューを作成します。

その中に app/views/weekend_checkerフォルダーの中に index.html.erbファイルを追加する。このファイルには以下のコードを記述する:

<h2>Is It The Weekend? Get a Daily Text to Find Out!</h2>
<p>
This is a free service that will analyze <a href="http://isittheweekend.com">isittheweekend.com</a> and check for any updates once a day. If there is an update it will send you a text message at the number you provide. 
</p>
<p>
To remove yourself from the SMS list, reply to the text message you receive with the number "1".
</p>
<p>
SMS messages are sent using the <a href="/home">Nexmo SMS API</a>.
</p>

<% flash.each do |type, msg| %>
  <div>
    <%= msg %>
  </div>
<% end %>

<%= form_with model: @recipient, url: "/recipient/new" do |f| %>
  <%= f.telephone_field :number, :placeholder => '12122222222' %>
  <%= f.hidden_field :subscribed, value: true %>
  <%= f.submit "Add Number" %>
<% end %>

最後のコーディング作業は、このすべてのコードを実行する新しいRakeタスクをセットアップし、1日1回Rakeタスクを実行するようにgemを設定することだ。 whenevergemを設定することだ。

Rakeタスクの作成とスケジュール

もう一度、コマンドラインからRailsジェネレータを使ってRakeタスク用のファイルを作成します。コマンドラインから以下を実行します:

rails generate task scraper check_site_update

上記のタスクは lib/tasksと呼ばれる scraper.rake.このファイルをコード・エディターで開くと、次のようになる:

namespace :scraper do
  desc "TODO"
  task :check_site_update => :environment do
  end
end

を再定義してみよう。 descを再定義しよう: desc "Check Website for Any Updates".次に taskブロックの中に DiffStorage#check_last_recordクラスメソッドを追加する:

namespace :scraper do
  desc "Check Website for Any Updates"
  task :check_site_update => :environment do
    DiffStorage.check_last_record
  end
end

Rakeタスクが定義されたので、最後に whenevergemを初期化し、このタスクを1日1回実行することをgemに知らせる必要がある。そのために、まずgemのイニシャライザーコマンドをコマンドラインから実行する:

bundle exec wheneverize .

上記のコマンドは schedule.rbファイルを config/フォルダ内にファイルを作成する。そのファイルに以下のコードを追加して scraper:check_site_updateタスクを毎日実行する:

every 1.day do
  rake "scraper:check_site_update"
end

スケジュールが作成されたので、マシンのcrontabファイルを更新して、この新しいジョブについて知らせる必要がある。コマンドラインから bundle exec whenever --update-crontabをコマンドラインから実行する。これが完了すると、タスクは完全に初期化され、マシンで1日1回実行されるように設定される。

アプリケーションのコードはすべて整いました。あとは、Nexmoアカウントを作成し、Nexmo API認証情報を取得し、毎日テキストメッセージを送信するための仮想電話番号をプロビジョニングするだけです。これらの情報を入手したら、環境変数としてアプリケーションに追加します。

Nexmo API認証情報と電話番号

アカウントを作成するには Nexmoダッシュボードにアクセスし、登録手続きを行ってください。登録が完了すると、ダッシュボードに入ります。

もしそうしていなければ、アプリケーションのトップレベル・ディレクトリに .envファイルを作成し、アプリケーションのトップレベル・ディレクトリに NEXMO_API_KEYNEXMO_API_SECRETを追加してください。これらの値はダッシュボードページの一番上の Your API credentialsヘッダーの下にあります。

NEXMO_API_KEY=
NEXMO_API_SECRET=

ダッシュボードの中で次にやるべきことは、電話番号のプロビジョニングだ。サイドバーナビゲーションで Numbersリンクをクリックすると、ドロップダウンメニューが表示されます。その中から Buy numbersオプションを選択し Searchボタンをクリックすると、取得可能な電話番号のリストが表示されます。

機能、国、タイプからNumbersを検索する場合は、ユーザーの居住国を選択することをお勧めします、 SMS機能 Mobileを選択することをお勧めします。

オレンジ色の Buy購入したい番号のオレンジ色のボタンをクリックしたら、その番号を .envという新しい変数として FROM_NUMBER:

NEXMO_API_KEY=
NEXMO_API_SECRET=
FROM_NUMBER=

ダッシュボード内で行う最後の項目は、電話番号のイベントウェブフックとして外部からアクセス可能なURLを提供することです。開発用に ngrokは良いツールです。 このガイドを参考にしてください。

ダッシュボード Numbersサイドバーのナビゲーションドロップダウンから Your numbersを選択すると、新しくプロビジョニングされた電話番号がリスト表示されます。歯車のアイコンをクリックしてプロパティを管理すると、設定ダイアログメニューが表示されます。

上記のスクリーンショットの例では Inbound Webhook URLテキストフィールドを /webhooks/event.

以上です!コードはすべて完成し、Nexmoの認証情報もすべて設定された。この時点で、アプリケーションを実行するかどうかの選択を迫られる。ローカルで実行するか、Herokuのような外部のホスティング・プロバイダーにデプロイするかです。最後のステップでは、ローカルで実行する方法について説明します。

より長期的なソリューションとして導入することに興味がある方は GitHub リポジトリにアクセスし Deploy to Herokuボタンをクリックして、そのプロセスを開始することができる。

アプリケーションの実行

これで真新しいアプリケーションを実行する準備ができました!ローカルで実行するには、RailsイベントWebhookをローカル環境以外の外部からアクセスできるようにする必要があります。たとえば、このガイドに従ってngrokを使用している場合 このガイドを使用する場合は、Railsアプリケーションとngrokの両方を同時に実行する必要があります。

Railsアプリケーションを起動するには、コマンドラインから以下を実行する:

bundle exec rails server

その後、お好きなブラウザで localhost:3000.あなたが作成した登録フォームが表示されます。あなたの電話番号を記入して送信してください。Rakeタスクが実行されると、週末かどうか、今日が昨日と違うかどうかを知らせるSMSが届くはずです!

次のステップ

私たちが作ったアプリケーションは、ウェブスクレイピングとSMSを活用して、購読者に最新情報を配信するアプリケーションを作る可能性を示している。このようなアプリケーションの潜在的な使用例は無数にある。あなたがこのシナリオを正確に再現することに興味があるにせよ、あなた自身のユースケースのためにコードを移植することに興味があるにせよ、このトピックについて探求することはさらにたくさんある。

その他のSMSの可能性については、以下の資料をご覧ください:

シェア:

https://a.storyblok.com/f/270183/384x384/e5480d2945/ben-greenberg.png
Ben Greenbergヴォネージの卒業生

ベンはセカンドキャリアの開発者で、以前は成人教育、コミュニティ組織化、非営利団体運営の分野で10年を過ごした。彼はVonageの開発者支援者として働いていた。コミュニティ開発とテクノロジーの交差点について定期的に執筆している。南カリフォルニア出身で、長年ニューヨークに住んでいたが、現在はイスラエルのテルアビブ近郊に在住。