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

Busque atualizações na Web e envie alertas por SMS com Ruby

Publicado em May 18, 2021

Tempo de leitura: 18 minutos

Todos nós já passamos por isso. Você acorda e é um novo dia, e só quer saber a resposta para a pergunta mais urgente: já é fim de semana? Sua semana foi tão agitada, cheia de responsabilidades profissionais e pessoais. Tudo o que você quer é tirar uns dias para sentar e relaxar.

Para responder à pergunta, você poderia, é claro, abrir o aplicativo de calendário no seu celular ou perguntar ao seu assistente digital pessoal favorito. Mas por que fazer isso se você mesmo pode criar um aplicativo que envie uma mensagem de texto para você?

Vamos criar uma aplicação em Ruby on Rails que faça o seguinte:

  • Permite tanto a inscrição de novos assinantes quanto a possibilidade de cancelar a inscrição na lista

  • O site isittheweekend.com traz a resposta à nossa pergunta isittheweekend.com

  • Envia diariamente a resposta, com base nos dados coletados, a todos os destinatários inscritos

Para resumir tudo isso, também criaremos uma tarefa do Rake que executará todas essas tarefas de uma só vez e configuraremos sua execução para ocorrer uma vez a cada 24 horas.

Se preferir, você também pode encontrar uma versão totalmente funcional deste aplicativo no GitHub.

Vamos começar!

Pré-requisitos

Gerar a aplicação Rails

A primeira coisa que precisamos fazer é criar nossa nova aplicação Rails. Na linha de comando, execute o seguinte:

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

Isso cria a estrutura de arquivos necessária para nossa aplicação Rails e define o PostgreSQL como banco de dados padrão.

Feito isso, cd no diretório que foi criado. Antes de instalarmos nossas dependências, vamos adicionar as gems adicionais que nosso aplicativo utilizará.

Abra o código no seu editor de código preferido e vá até o Gemfile. Dentro do arquivo Gemfile , adicione as seguintes gems:

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

Estamos usando o nexmo gem para enviar as atualizações por SMS, a watir e webdrivers gems para fazer a solicitação HTTP a um site com conteúdo dinâmico em JavaScript, o whenever gem para agendar a tarefa do Rake e a dotenv-rails gem para gerenciar as variáveis de ambiente.

Depois de salvar o Gemfile, você estará pronto para executar bundle install a partir da linha de comando.

O próximo passo é criar o esquema do nosso banco de dados e os modelos.

Criar o esquema do banco de dados e os modelos

Agora que nossa aplicação Rails foi criada e suas dependências estão instaladas, a próxima tarefa é criar o esquema de banco de dados adequado para armazenar os dados de que precisaremos para operar nossa aplicação. Precisamos armazenar os seguintes tipos de informações:

  • Destinatários: A lista de assinantes com seus números de telefone

  • DiffStorage: Cópias dos dados do site para comparação, a fim de determinar se houve alguma alteração

Usaremos a ferramenta geradora do Rails para criar os arquivos de migração e, em seguida, editaremos cada um deles.

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

Esses comandos criarão os arquivos de modelo e app/models e arquivos de migração em db/migrate. Antes de confirmar essas alterações em seu aplicativo, assim que as ações do gerador forem concluídas, verifique os arquivos criados em ambos os diretórios para garantir que estejam corretos.

Mais especificamente, nos arquivos de migração, é preciso garantir que cada migração inclua t.timestamps, o que adiciona um created_at e updated_at coluna à tabela. Você também deve verificar se o number e subscribed nas Recipient arquivo de migração, com os tipos definidos como string e boolean, respectivamente. Da mesma forma, você deve ver uma coluna no DiffStorage arquivo de migração para website_data com o tipo definido como text.

Os arquivos de modelo dentro de app/models devem estar vazios, exceto pelas declarações de classe e sua herança de ApplicationRecord.

Quando tudo parecer estar correto, é hora de executar rake db:migrate a partir da linha de comando. O comando exibirá os resultados no console e, se você examinar o db/schema.rb arquivo, você poderá ver o esquema que criou inicializado dentro do aplicativo.

Por fim, também precisamos criar Messenger e Scraper modelos, mas não precisamos de uma migração para eles. Para isso, executamos o gerador do Rails novamente e acrescentamos uma --migration=false opção a ele:

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

Chegou a hora de definir a lógica dentro dos modelos.

Definindo os modelos

Conforme mencionado acima, temos quatro modelos responsáveis por áreas específicas do aplicativo:

  • DiffStorage: Verifica se há diferenças nos dados do site

  • Recipient: Gerencia a adição e a remoção de assinantes

  • Messenger: Gerencia ou envio de mensagens SMS

  • Scraper: Responsável por extrair dados do site

Definindo o modelo DiffStorage

O DiffStorage modelo conterá duas classes. Uma conterá a URL que estamos extraindo. A segunda verificará se houve alguma alteração desde a última vez em que o site foi extraído e acionará as próximas etapas do aplicativo quando as condições forem atendidas.

Primeiro, vamos definir a URL em um método próprio, para que haja um único local onde ela exista e possa ser facilmente modificada, caso decidamos fazer isso mais tarde:

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

Em seguida, a maior parte desse modelo ficará dentro do #check_last_record método de classe:

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

O método acima primeiro chama o método na Scraper classe que iniciará a extração do site para obter o instantâneo mais recente e atribui esses dados a today_answer. Em seguida, ele envolve a próxima etapa dentro de uma if instrução que verifica se há algum registro em DiffStorage. Se houver registros anteriores armazenados ali, o método pega o mais recente e o atribui a yesterday_answer. Se não houver registros anteriores, uma string vazia é atribuída a yesterday_answer. Por fim, ele envia os destinatários e as duas variáveis ao Messenger modelo para que ele processe o envio da mensagem.

Definindo o modelo do scraper

O Scraper modelo será responsável por coletar os dados do site isittheweekend.com para determinar se realmente é fim de semana ou não. O modelo terá quatro métodos de classe e definiremos cada um deles aqui:

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

Cada ação do processo de scraping é definida em seu próprio método pequeno, a fim de manter nossas preocupações separadas. O #call método é o ponto de entrada da classe. É ele que é chamado por outros métodos externos a ela. O #get_url método faz a solicitação HTTP simulando uma solicitação do navegador Chrome usando a Watir biblioteca e a analisa com Nokogiri. O #check_text método verifica se algum dado foi obtido. O #store_text método salva esses dados no banco de dados.

Definindo o modelo do mensageiro

Dentro do Messenger modelo estará todo o código responsável pelo envio da atualização diária por SMS aos assinantes. Criaremos um método que enviará a mensagem, um método que elaborará o texto de resposta para o fim de semana, um método que montará a mensagem completa e um método que gerenciará uma mensagem de confirmação caso um assinante envie uma solicitação de cancelamento.

Primeiro, o método para enviar a mensagem de atualização:

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

O valor do text parâmetro acima refere-se a um método de classe chamado #weekend_message. Esse método irá compor a string para a atualização do fim de semana, verificando se hoje é igual a ontem ou não:

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

Em seguida, o método que contém a HEREDOC string com o corpo da mensagem:

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

Por fim, o método para enviar uma mensagem de confirmação de remoção:

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

O último modelo que precisamos definir antes de prosseguirmos para a próxima etapa é o Recipient modelo.

Definição do modelo de destinatário

Este modelo não contém nenhum de seus métodos de classe. A única alteração que faremos neste modelo é adicionar duas validações aos dados dos destinatários. Essas validações servirão como uma medida de segurança ao adicionar novos números de telefone ao banco de dados. Verificaremos se a) um número está de fato sendo fornecido nos dados e b) o número não é uma duplicata de um registro já existente. Para realizar essas validações, adicionamos duas linhas abaixo da definição da classe:

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

Criar o controlador e as rotas

Estamos quase terminando a construção do nosso aplicativo! O próximo passo é definir as ações do controlador que irão determinar o fluxo do aplicativo. Primeiro, vamos gerar o controlador usando o gerador do Rails a partir da linha de comando:

rails generate controller WeekendChecker

Isso criará um novo arquivo de controlador vazio em app/controllers chamado weekend_checker_controller.rb e arquivos de visualização complementares em app/views/weekend_checker. Adicionaremos uma visualização de índice em breve. Neste momento, vamos nos concentrar no controlador.

O controlador precisa de três ações para corresponder a três rotas: #index, #create e #event. A #index rota será a visualização padrão e única do nosso site. Esse será o local onde as pessoas poderão se inscrever na lista. A #create rota será onde os novos números serão processados. Por fim, a #event rota será onde o aplicativo receberá dados de webhook da SMS API, incluindo solicitações de remoção, e os processará.

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

Essas três ações do controlador exigem que três rotas correspondentes sejam definidas em 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

O penúltimo passo na configuração do código do nosso aplicativo é criar uma visualização básica para a / rota.

Definindo a visualização

Para se inscrever na lista de SMS, criaremos uma visualização acessível no nível superior da URL que conterá um formulário de inscrição.

Dentro da app/views/weekend_checker pasta, adicione um index.html.erb arquivo. Ele conterá o seguinte código:

<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 %>

A última tarefa de programação que precisamos realizar é configurar nossa nova tarefa do Rake, que executará todo esse código e configurará o whenever gem para executar a tarefa do Rake uma vez por dia.

Crie a tarefa do Rake e programe-a

Mais uma vez, vamos usar um gerador do Rails a partir da linha de comando para criar o arquivo da nossa tarefa do Rake. Na linha de comando, execute o seguinte:

rails generate task scraper check_site_update

A tarefa acima criará um arquivo em lib/tasks chamado scraper.rake. Quando o abrirmos em nosso editor de código, ele terá a seguinte aparência:

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

Vamos redefinir o desc com uma breve descrição do que essa tarefa fará: desc "Check Website for Any Updates". Em seguida, dentro do task bloco, adicione o DiffStorage#check_last_record método de classe, que é o ponto de entrada para todo o trabalho que criamos anteriormente:

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

Agora que nossa tarefa do Rake está definida, precisamos, por fim, inicializar a whenever gem e informar a ela que queremos que essa tarefa seja executada uma vez por dia. Para isso, primeiro executamos o comando de inicialização da gem na linha de comando:

bundle exec wheneverize .

O comando acima cria um schedule.rb arquivo dentro da config/ pasta. Adicione o código a seguir a esse arquivo para executar a scraper:check_site_update tarefa diariamente:

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

Agora que a programação foi criada, precisamos atualizar o arquivo crontab em nossa máquina para que ele reconheça essa nova tarefa. Fazemos isso executando bundle exec whenever --update-crontab na linha de comando. Feito isso, a tarefa estará totalmente inicializada e configurada para ser executada uma vez por dia em nossa máquina.

O código do nosso aplicativo já está pronto. A única coisa que falta agora é criar nosso Account no Nexmo, obter nossas credenciais da API do Nexmo e provisionar um número de telefone virtual para enviar as mensagens de texto diárias. Assim que tivermos essas informações, vamos adicioná-las ao nosso aplicativo como variáveis de ambiente.

Credenciais da API da Nexmo e número de telefone

Para criar um account, acesse o Painel da Nexmo e siga as etapas de cadastro. Assim que concluir o cadastro, você será direcionado ao seu Painel.

Se você ainda não tiver feito isso, crie um .env arquivo no diretório raiz do seu aplicativo e adicione seu NEXMO_API_KEY e NEXMO_API_SECRET a ele. Os valores para esses campos podem ser encontrados na parte superior da página do painel, sob o Your API credentials cabeçalho.

NEXMO_API_KEY=
NEXMO_API_SECRET=

A próxima tarefa que precisamos realizar no painel é provisionar um número de telefone. Depois de clicar no Numbers link na barra de navegação lateral, um menu suspenso será exibido. Depois de selecionar a Buy numbers opção e clicar no Search botão, você verá uma lista de números disponíveis para aquisição.

Ao pesquisar Numbers por recurso, país e tipo, recomenda-se selecionar o país onde seus usuários estarão localizados, SMS para características e Mobile para o tipo.

Depois de clicar no botão laranja Buy para o número que você deseja comprar, você pode adicionar esse número ao seu .env arquivo como uma nova variável chamada FROM_NUMBER:

NEXMO_API_KEY=
NEXMO_API_SECRET=
FROM_NUMBER=

A última coisa que precisamos fazer em nosso painel é fornecer uma URL acessível externamente como o webhook de eventos para o número de telefone. Para fins de desenvolvimento, o ngrok é uma boa ferramenta para se usar, e você pode seguir este guia para saber como começar a usá-la.

No painel Numbers menu suspenso da barra lateral de navegação, ao selecionar Your numbers você verá seu número de telefone recém-provisionado em uma lista. Ao clicar no ícone de engrenagem para gerenciar suas propriedades, uma janela de diálogo de configurações será exibida.

No exemplo da captura de tela acima, você deve substituir o Inbound Webhook URL campo de texto pela sua própria URL, terminando com /webhooks/event.

É isso aí! Nosso código está totalmente finalizado e nossas credenciais do Nexmo já estão configuradas. Neste ponto, você precisa decidir como executar seu aplicativo. Você pode executá-lo localmente ou implantá-lo em um provedor de hospedagem externo, como o Heroku. Na etapa final, vamos explicar como executá-lo localmente.

Se você estiver interessado em implementá-lo como uma solução de longo prazo, pode acessar o repositório do GitHub e clicar no Deploy to Heroku botão na parte superior do arquivo README para iniciar esse processo.

Executando o aplicativo

Agora estamos prontos para executar nosso aplicativo novinho em folha! Para executá-lo localmente, o webhook de eventos do Rails precisa estar acessível ao mundo externo, fora do seu ambiente local. Por exemplo, se você estiver usando o ngrok após seguir este guia , tanto a aplicação Rails quanto o ngrok precisam estar em execução simultaneamente.

Para iniciar o aplicativo Rails, execute o seguinte na linha de comando:

bundle exec rails server

Em seguida, você pode acessar no navegador de sua preferência localhost:3000. Você verá o formulário de cadastro que criou. Preencha-o com seu número de telefone e envie-o. Agora, assim que a tarefa do Rake for executada, você deve receber um SMS informando se é fim de semana e se hoje é diferente de ontem!

Próximos passos

O aplicativo que criamos, embora seja um pouco lúdico, demonstra o potencial de utilizar web scraping e SMS para criar um aplicativo que forneça atualizações aos assinantes. Existem inúmeros casos de uso possíveis para um aplicativo como esse. Se você estiver interessado em reproduzir exatamente esse cenário ou em adaptar o código para o seu próprio caso de uso, há ainda muito mais a ser explorado sobre esse assunto.

Para explorar mais a fundo outras possibilidades do SMS, confira os seguintes recursos:

Compartilhar:

https://a.storyblok.com/f/270183/384x384/e5480d2945/ben-greenberg.png
Ben GreenbergEx-funcionários da Vonage

Ben é um desenvolvedor que mudou de carreira, tendo atuado anteriormente por uma década nas áreas de educação de adultos, organização comunitária e gestão de organizações sem fins lucrativos. Ele trabalhou como representante de desenvolvedores na Vonage. Escreve regularmente sobre a interseção entre desenvolvimento comunitário e tecnologia. Natural do sul da Califórnia e morador de longa data da cidade de Nova York, Ben reside atualmente perto de Tel Aviv, em Israel.