ツイッターに地震情報を投稿する Bot の簡単な作り方を紹介します。Ruby での実装例です。

紹介するコードで動作している Bot のアカウントはこちらです:地震速報(@earthquake_twt)

下記のような地震速報を投稿する Bot です。

2013年10月23日 1時50分ごろ、地震が発生しました。震源地は宮城県沖、マグニチュード2.8です。この地震による津波の心配はありません。

用意するもの

  • Ruby と cron を実行できる環境
  • Nokogiri (鋸)
    • Yahoo の地震情報ページの HTML ソースをパースするのに使います。
  • A Ruby interface to the Twitter API
    • Twitter に地震情報を投稿するために使います。Twitter API v1.1 に対応しています。

作り方

  1. 地震速報 Bot 用の Twitter アカウントを作ります。
  2. https://dev.twitter.com/apps/new に、作成した地震速報 Bot 用のアカウントでログインして、API キーを取得します。なお、"Application type" は "Read and Write" を選択してください。
  3. 下記の 75 行から成るソースコードをコピーして、適当な名前をつけてスクリプトファイルとして保存します。
  4. コード 36 - 39 行目の API キーをあなたのものに置き換えます。
  5. Ruby スクリプトを cron で定期的(例えば 1 分おき)に実行するように設定します。

以上で、地震速報 Bot のできあがりです。

地震速報 Bot のソースコード

# encoding: utf-8
require "open-uri"
require "uri"
require "rubygems"
require "nokogiri"
require "twitter"

class HttpClient
  User_Agent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
  def self.request(url)
    begin
      response = open(url, "User-Agent" => User_Agent)
      doc = Nokogiri::HTML(response)
    rescue OpenURI::HTTPError => err
      puts err.message
    end
  end
end

class Parser
  def self.parse(doc)
    info = {}
    doc.css(".yjw_table").first.css("tr").each do |tr|
      info_name,info_value = tr.text.split("\n")
      info[info_name] = info_value.strip
    end
    return info
  end
end

class TwitterClient
  require 'openssl'
  # Avoid "SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed"
  OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE

  CONSUMER_KEY = 'your_consumer_key'
  CONSUMER_SECRET = 'your_consumer_secret'
  OAUTH_TOKEN = 'your_oauth_token'
  OAUTH_TOKEN_SECRET = 'your_oauth_token_secret'

  def self.tweet(message)
    Twitter::Client.new(
      :consumer_key => CONSUMER_KEY,
      :consumer_secret => CONSUMER_SECRET,
      :oauth_token => OAUTH_TOKEN,
      :oauth_token_secret => OAUTH_TOKEN_SECRET
    ).update(message)
  end
end

begin
  info = Parser::parse(HttpClient::request("http://typhoon.yahoo.co.jp/weather/jp/earthquake/"))

  prev_datetime = nil

  if (File.exists?("earthquake.log"))
    File.open("earthquake.log", "r") do |file|
      prev_datetime = file.read
      file.close
    end
  end

  exit if prev_datetime == info["発生時刻"]

  TwitterClient::tweet(
    info["発生時刻"] + "、地震が発生しました。震源地は" + info["震源地"] + "、マグニチュード" + info["マグニチュード"] + "です。" + info["情報"]
  )

  File.open("earthquake.log", "w") do |file|
    file.write(info["発生時刻"])
    file.close
  end
rescue
  puts $!
end

コード解説

HttpClient クラス

class HttpClient
  User_Agent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
  def self.request(url)
    begin
      response = open(url, "User-Agent" => User_Agent)
      doc = Nokogiri::HTML(response)
    rescue OpenURI::HTTPError => err
      puts err.message
    end
  end
end

このクラスは、Yahoo の地震情報ページ を取得して、それを Nokogiri::HTML オブジェクトとして返すためのクラスです。

Parser クラス

class Parser
  def self.parse(doc)
    info = {}
    doc.css(".yjw_table").first.css("tr").each do |tr|
      info_name,info_value = tr.text.split("\n")
      info[info_name] = info_value.strip
    end
    return info
  end
end

このクラスは、Yahoo の地震情報ページ内にある下記のような地震情報テーブルから情報抽出して、それをハッシュに格納して返すためのクラスです。テーブルの 1 列目の各項目を info_name キー、2 列目を info_value 値として info ハッシュに格納しています。

情報発表時刻2013年10月23日 1時54分
発生時刻2013年10月23日 1時50分ごろ
震源地宮城県沖
緯度北緯38.7度
経度東経141.8度
深さ50km
マグニチュード2.8
情報この地震による津波の心配はありません。

TwitterClient クラス

class TwitterClient
  require 'openssl'
  # Avoid "SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed"
  OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
 
  CONSUMER_KEY = 'your_consumer_key'
  CONSUMER_SECRET = 'your_consumer_secret'
  OAUTH_TOKEN = 'your_oauth_token'
  OAUTH_TOKEN_SECRET = 'your_oauth_token_secret'
 
  def self.tweet(message)
    Twitter::Client.new(
      :consumer_key => CONSUMER_KEY,
      :consumer_secret => CONSUMER_SECRET,
      :oauth_token => OAUTH_TOKEN,
      :oauth_token_secret => OAUTH_TOKEN_SECRET
    ).update(message)
  end
end

このクラスは、ツイッターに地震情報を投稿するためのクラスです。

  CONSUMER_KEY = 'your_consumer_key'
  CONSUMER_SECRET = 'your_consumer_secret'
  OAUTH_TOKEN = 'your_oauth_token'
  OAUTH_TOKEN_SECRET = 'your_oauth_token_secret'

の部分は、あなたのキーに置き換えてください。

また、

  require 'openssl'
  # Avoid "SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed"
  OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE

ですが、私の環境では、47 行目の update(message) を実行すると、"SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed" とエラーが発生したため、OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE を付け加えてこのエラーを抑制し(ごまかし)ました。ちゃんとした対処法の詳細については、http://stackoverflow.com/questions/4528101/ssl-connect-returned-1-errno-0-state-sslv3-read-server-certificate-b-certificat を参照してください。

実行部分

begin
  info = Parser::parse(HttpClient::request("http://typhoon.yahoo.co.jp/weather/jp/earthquake/"))
 
  prev_datetime = nil
 
  if (File.exists?("earthquake.log"))
    File.open("earthquake.log", "r") do |file|
      prev_datetime = file.read
      file.close
    end
  end
 
  exit if prev_datetime == info["発生時刻"]
 
  TwitterClient::tweet(
    info["発生時刻"] + "、地震が発生しました。震源地は" + info["震源地"] + "、マグニチュード" + info["マグニチュード"] + "です。" + info["情報"]
  )
 
  File.open("earthquake.log", "w") do |file|
    file.write(info["発生時刻"])
    file.close
  end
rescue
  puts $!
end

残りは、実行制御を組み立てるメインの部分です。 cron による定期的な実行で重複した地震情報を投稿をしないように、63 行目 exit if prev_datetime == info["発生時刻"] の処理を入れて、発生時刻が前回取得時と同じなら、ツイッターに投稿せずに Bot を終了させます。発生時刻の情報は、69 行目から始まる部分にあるようにファイルに保存して、次回実行時に参照できるようにしています。