Twitter bot [総集編]

Ruby

こんにちわkazutoです。

今回は、以前、作成した、Twitter bot (天気予報bot)の総集編になります。一つ一つの機能(クラス)を振り返っていきましょう。

Wetaherクラス

Wetaherクラスでは、OpenWeatherMapというサイトのAPIを取得して、天気予報を取得しました。APIKEYの漏洩するのを防ぐために、外部から参照ができない、環境変数をvimというテキストエディタを使って、APIKEYを格納しましたね。

環境変数の値を取得をするためにconfirg.rbファイルを作成しました。

WEATHER_API_KEY       =  ENV['WEATHER_API_KEY']
WEATHER_URL           =  ENV['WEATHER_URL']
require 'date'
require 'csv'

  class Wetaher

    def initialize(citys)
      @citys=citys
    end

    def select_city
      @citys.each do |city|
        puts "#{city['id']}:#{city['city']}"
      end
      puts "天気情報を取得したい都市を選択してください"
      input = gets.to_i-1
      @citys[input]
    end

    def get_weather_forecast(city)
      response = open(WEATHER_URL + "?q=#{city['english']}&appid=#{WEATHER_API_KEY}&units=metric&lang=ja")
      data=JSON.parse(response.read)
      json_to_parse(data['list'])
      datas= {list: data['list'],city_name:data['city']['name']}
    end

    def display_weather_forecast(weathers)
      CSV.open("#{weathers[:city_name]}.csv","w") do |data|
        data<<["id","時間","天気","気温"]
        puts "地区:#{weathers[:city_name]}"
        id = 0
        weathers[:list].each do|weather|
          id+=1
          puts "------------------------------"
          puts "時間#{weather['dt_txt']}"
          puts "天気:#{weather['weather'][0]['description']}"
          puts "気温:#{weather['main']['temp']}℃"
          puts "------------------------------"
          data<<[id,Time.parse(weather['dt_txt']).strftime('%Y年%m月%d日 %H時%M分' ),weather['weather'][0]['description'],weather['main']['temp']]
        end
      end
    end

    def json_to_parse(weathers)
      weather=JSON.pretty_generate(weathers)
      File.open("tenki.json", mode = "w"){|f|
        f.write(weather)
      }
    end

    def display_weather_text(weathers)
      File.open("tenki.txt", mode = "w"){|f|

        f.write(
          weathers[:list].each do|weather|
            puts "------------------------------"
            puts "時間#{weather['dt_txt']}"
            puts "天気:#{weather['weather'][0]['description']}"
            puts "気温:#{weather['main']['temp']}℃"
            puts "------------------------------"
          end
        )
      }
    end

    def current_time_weather(wethers)

        wethers[:list].map{|wether|wether['dt_txt']= Time.parse(wether['dt_txt'])}
        current_weather  = wethers[:list].select{|wether|wether['dt_txt']>=Time.now}.first
        dt_txt = current_weather['dt_txt'].strftime('%Y年%m月%d日 %H時%M分%S秒' )

        data =  {
          city:wethers[:city_name] ,
          dt_txt:  dt_txt,
          description:current_weather['weather'][0]['description'],
          temperature: current_weather['main']['temp'],
          icon:current_weather['weather'][0]['icon']
          }
    end
  end

  def wetah



  er(num)
    citys=CSV.read("./citys.csv",headers: true)
    wether = Wetaher.new(citys)
    city=wether.select_city
    datas=wether.get_weather_forecast(city)
    case num
      when 1
        wether.current_time_weather(datas)
      when 2
        wether.display_weather_forecast(datas)
    end
  end

OpenWeatherMapから取得した情報を

  • 表示
  • データを整形

をしました。

Wetherクラスのゴールは

  • 5日間の天気情報をターミナルで表示
  • 天気予報を取得してTweetクラスに整形済みのデータを渡す

となります。

関連記事:「天気予報APIを用いて5日間の天気情報を取得しよう

Tweetクラス

Tweetクラスでは、

  • アカウント認証
  • ツイート機能
  • プロフィール機能

と上記の3つの機能を実装していきました。その他にも、子クラスの動作を再現するためにいくつかメソッドを作成しました。

class Tweet

  attr_accessor :client
  def initialize(current_time_weather)
    @client = Twitter::REST::Client.new do |config|
      config.consumer_key        =  CONSUMER_KEY
      config.consumer_secret     =  CONSUMER_SECRET
      config.access_token        =  ACCESS_TOKEN
      config.access_token_secret =  ACCESS_TOKEN_SECRET
    end
    @current_time_weather =  current_time_weather
  end

  def updata
    text = "#{@current_time_weather[:dt_txt]}\n#{@current_time_weather[:city]}の天気\n天気:#{@current_time_weather[:description]}\n気温:#{@current_time_weather[:temperature]}"
    @client.update(text)
  end

  def account_setting
    puts "アカウント名:#{@client.user.name}"
    puts "プロフィール文\n#{@client.user.description }"
    puts "[1]アカウント名を変更する"
    puts "[2]プロフィール文を変更する"
    puts "[3]閉じる"
    num = gets.to_i

      case num
        when 1
          name =gets.chomp
          @client.update_profile({name:name} )
        when 2
          description = gets.chomp
          @client.update_profile({description:description} )
        when 3
          return
      end
  end

 def search_account_information(user_name,analsis=false)
    puts  @client.user(user_name).screen_name   # アカウントID
    puts  @client.user(user_name).name          # アカウント名
    puts  @client.user(user_name).description   # プロフィール
    puts "こちらのアカウントのでお間違い無いですか?"
    puts "[1]YES"
    puts "[2]NO"
    num = gets.to_i
    case num
     when 1
       if analsis
         nil
       else
         search_target(user_name)
       end
     when 2
       return
     else
       puts"無効な値です"
    end
  end

   def timeline_style(user_name)
    puts "CSVファイルにデータを書き込みますか❓"
    puts "[1]YES"
    puts "[2]NO"
    num = gets.to_i
    case num
      when 1
        timeline_write(user_name)
      when 2
        timeline(user_name)
    else
      puts "無効の値です"
    end
   end

   def timeline(user_name,write=false)
      total_repetition = 0
      likes = 0
      retweets = 0
      data=[] if write
        @client.user_timeline(user_name).each do  |tweet|
          puts "---------------------------------------------"
          # puts tweet.created_at.getlocal
          puts "更新日#{tweet.created_at.getlocal.strftime('%Y年%m月%d日 %H時%M分%S秒' )}"
          puts tweet.full_text
          puts "いいね数:#{tweet.favorite_count}"
          puts "リツイート数:#{tweet.retweet_count}"
          puts "---------------------------------------------"
          likes += tweet.favorite_count
          retweets +=tweet.retweet_count
          total_repetition +=1
          data << [ total_repetition,tweet.created_at.getlocal.strftime('%Y年%m月%d日 %H時%M分%S秒' ),tweet.text,tweet.favorite_count.to_i,tweet.retweet_count] if write
        end
        puts "ツイート数:#{ total_repetition}"
        puts "トータルいいね数:#{likes}"
        puts "トータルリツイート数:#{retweets}"
        data if write
   end

   def timeline_write(user_name)
    puts "ss"
    CSV.open("#{user_name}timeline.csv","w") do |data|
      data << ["id","投稿日","投稿内容","いいね数","リツイート数"]
      tweets=timeline(user_name,true)
      tweets.each do |tweet|
        data<<tweet
      end
    end
  end
   def get_all_tweets(user_name,pages,type )
    total_repetition = 0
    total_like=0
    total_retweeet = 0
    case type[:mode]
    when "WRITE"
      CSV.open("#{user_name}.csv","w") do |data|
          data << ["id","投稿日","投稿内容","いいね数","リツイート数"]
          pages.each do |page|
          @client.user_timeline(user_name,{ count: 200 ,page:page} ).each do |timeline|
            tweet = @client.status(timeline.id)
            puts"--"*100
            puts  tweet.created_at.getlocal.strftime('%Y年%m月%d日 %H時%M分%S秒' )
            puts tweet.text
            puts "いいね数#{tweet.favorite_count}"
            puts "リツート#{tweet.retweet_count}"
            puts "--"*100
            total_repetition+=1
            total_like+=tweet.favorite_count
            total_retweeet+=tweet.retweet_count
            data << [timeline.id,tweet.created_at.getlocal.strftime('%Y年%m月%d日 %H時%M分%S秒' ),tweet.text,tweet.favorite_count,tweet.retweet_count]
          end
        end
        puts "ツイート数#{@client.user(user_name).tweets_count}"
        # puts "繰り返すた回数#{total_repetition}"
        puts "トータルいいね数#{total_like}"
        puts "トータルリツイート#{total_retweeet}"
        data << ["", "",@client.user(user_name).tweets_count,total_like,total_retweeet]
      end
    when "COMPARISON"
      pages.each do |page|
        @client.user_timeline(user_name,{ count: 200 ,page:page} ).each do |timeline|
          tweet = @client.status(timeline.id)
          type[:item] =="like" ? total_like+=tweet.favorite_count : total_retweeet+=tweet.retweet_count
          end
        end
        case type[:item]
          when "like"
            total_like
          when "retweet"
            total_retweeet
        else
          puts "無効の値です"
        end
    else
      puts "無効の値です"
    end
  end


  def number_of_pages_judgment(user_name)
    pages =[]
    if @client.user(user_name).tweets_count < 200
        count = 1
    elsif @client.user(user_name).tweets_count % 200==0
        count = @client.user(user_name).tweets_count / 200
    else
      remainder =  (@client.user(user_name).tweets_count / 200)
      count = remainder+1
      remainder_num=@client.user(user_name).tweets_count-200*remainder
    end
    1.upto(count) do |num|
      pages.push(num)
    end
    pages
  end

end

子クラスの共通するメソッドも作成しているので、かなり記述量が多く、大変でしたが、tweetクラスを実装した事により

  • Searchクラス
  • Analysisクラス

の実装がシンプルになりました。親クラスに共通する処理をまとめる事で、子クラスに重複した処理を記述する必要がなくなります。

関連記事

Searchクラス

Searchクラスでは、TwitterのアカウントIDを入力し、検索するという、検索機能を実装しました。また、検索したユーザと他のユーザを比較したくなった場合に対応できる様に、Analysisクラス(分析機能)と連結しました。と言っても、ただクラス内で、Analysisクラスのインスタンスを生成をし、Analysisクラスのメソッドを使える様に、しただけです。

以下の項目を検索&比較する事ができます。

  • フォロー数
  • フォロワー数
  • いいね数
  • リツイート数
  • ツイート数
require './analysis.rb'
require 'csv'

class Search<Tweet
    def initialize()
      super(client)
    end

    def search()
      puts "ユーザー名を入力してください。"
      input = gets.chomp
      search_account_information(input)
    end

    def search_target(user_name)
      puts "#{@client.user(user_name).name}さんどの情報が知りたいですか?"
      puts "[1]タイムライン"
      puts "[2]フォロー数"
      puts "[3]フォロワー数"
      puts "[4]タイムライン情報全件取得"
      puts "[5]比較する"

      num =  gets.to_i
      case num
        when 1
          timeline_style(user_name)
        when 2
          puts "フォロー数:#{@client.user(user_name).friends_count}"
        when 3
          puts "フォローワー数#{client.user(user_name).followers_count}"
        when 4
          pages=number_of_pages_judgment(user_name)
          get_all_tweets(user_name,pages,{mode: "WRITE"} )
        when 5
          target_user = user_to_compare()
          analysis = Analysis.new()
          analysis.comparison_user_menu(user_name,true,target_user)
      end
    end

    def user_to_compare()
      puts "比較したいユーザのidを入力してください。"
      target_user  = gets.chomp
      search_account_information(target_user,true)
      target_user
    end
end

  def search
    search= Search.new
    search.search()
  end

重い処理をTweetクラスにまとめていたので、記述量が少なく、シンプルな実装になりました。クラスの継承を用いると、親クラスのメソッドも使えるので、便利ですね。

タイムラインという項目を選択した場合は、CSVファイルの有無を選択します。

  • CSVファイルを生成する場合
    →CSVファイルを生成
  • CSVファイルを生成しない場合
    →ターミナルに表示

という2つの挙動を取ります。

比較する場合は、シンプルにターミナル に比較結果を表示するだけです。

関連記事:「Twitter bot 検索機能[拡張編]

Analysisクラス

Analysisクラスは、

マイアカウントを分析する、分析機能を実装しました。ややSearchクラスと被る部分がありますが、マイアカウントと他のユーザとで、メニュー欄を分けたかったので、この様な仕様になりました。

分析機能でも、以下の情報を検索&比較する事ができます。

  • タイムライン
  • フォロー数
  • フォロワー数
  • いいね数
  • リツイート数
  • ツイート数

まあ、ほぼ同じ様な内容になります。

class Analysis<Tweet
  def initialize()
    super(client)
  end
  def analysis_menu

    puts "[1]タイムライン"
    puts "[2]フォロー数"
    puts "[3]フォロワー数"
    puts "[4]タイムライン全件取得"
    puts "[5]比較する"

    num = gets.to_i
    case num
        when 1
          timeline_style(@client.user.screen_name)
        when 2
          puts @client.user.friends_count
        when 3
          puts @client.user.followers_count
        when 4
          pages=number_of_pages_judgment(@client.user.screen_name)
          get_all_tweets(nil,pages,{mode: "WRITE"} )
        when 5
          comparison_user_decision()
        else
          puts "無効の値です"
        end
    end

    def comparison_user_decision()
      puts "比較したいユーザのidを入力してください。"
      user_name = gets.chomp
      search_account_information(user_name,true)
      comparison_user_menu(user_name)
    end

    def comparison_user_menu(user_name,search=false,search_target=nil)
      puts "#{@client.user(user_name).name}さん"
      puts "どの項目を比較しますか"
      puts "[1]フォロー数"
      puts "[2]フォロワー数"
      puts "[3]ツイート数"
      puts "[4]いいね数"
      puts "[5]リツイート数"
      num = gets.to_i
      if search
        case num
          when 1
            items={search_target_user:@client.user(search_target).friends_count,comparison_user:@client.user(user_name).friends_count}
            search_target_user ,comparison_user= make_users_information(user_name,"search",items,search_target)
            display_result(search_target_user,comparison_user,"フォロー数")
          when 2
            items={search_target_user:@client.user(search_target).followers_count,comparison_user:@client.user(user_name).followers_count}
            search_target_user,comparison_user = make_users_information(user_name,"search",items,search_target)
            display_result(search_target_user,comparison_user,"フォロワー数")
          when 3
            items={search_target_user:@client.user(search_target).tweets_count,comparison_user:@client.user(user_name).tweets_count}
            search_target_user,comparison_user =  make_users_information(user_name,"search",items,search_target)
            display_result(search_target_user,comparison_user,"ツイート数")
          when 4
            comparison_user_like = get_retweets_or_likes(user_name,"like")
            search_target_user_like = get_retweets_or_likes(search_target,"like")
            items={search_target_user:search_target_user_like,comparison_user:comparison_user_like}
            search_target_user,comparison_user = make_users_information(user_name,"search",items,search_target)
            display_result(search_target_user,comparison_user,"いいね数")
          when 5
            comparison_user_retweet = get_retweets_or_likes(user_name,"retweet")
            search_target_user_retweet = get_retweets_or_likes(search_target,"retweet")
            items={search_target_user: search_target_user_retweet,comparison_user:comparison_user_retweet}
            search_target_user,comparison_user = make_users_information(user_name,"search",items,search_target)
            display_result(search_target_user,comparison_user,"リツイート")
        else
            puts "無効の値です"
        end
      else
        case num
          when 1
            items={myself:@client.user.friends_count,comparison_user:@client.user(user_name).friends_count}
            myself ,comparison_user= make_users_information(user_name,"analysis",items)
            display_result(myself,comparison_user,"フォロー数")
          when 2
            items={myself:@client.user.followers_count,comparison_user:@client.user(user_name).followers_count}
            myself,comparison_user = make_users_information(user_name,"analysis",items)
            display_result(myself,comparison_user,"フォロワー数")
          when 3
            items={myself:@client.user.tweets_count,comparison_user:@client.user(user_name).tweets_count}
            myself,comparison_user =  make_users_information(user_name,"analysis",items)
            display_result(myself,comparison_user,"ツイート数")
          when 4
            comparison_user_like = get_retweets_or_likes(user_name,"like")
            myself_like = get_retweets_or_likes(nil,"like")
            items={myself:myself_like,comparison_user:comparison_user_like}
            myself,comparison_user = make_users_information(user_name,"analysis",items)
            display_result(myself,comparison_user,"いいね数")
          when 5
            comparison_user_retweet = get_retweets_or_likes(user_name,"retweet")
            myself_retweet = get_retweets_or_likes(nil,"retweet")
            items={myself:myself_retweet,comparison_user:comparison_user_retweet}
            myself,comparison_user = make_users_information(user_name,"analysis",items)
            display_result(myself,comparison_user,"リツイート")
        else
          puts "無効の値です"
        end
      end
    end

    def display_result(user,comparison_user,item_name)
          puts "-------------------------------"
          puts "#{item_name}"
          puts "#{user[:name]}:#{ user[:result]}"
          puts "#{comparison_user[:name]}:#{ comparison_user[:result]}"
          puts "-------------------------------"
          result_of_comparison(user,comparison_user)
    end

    def make_users_information(user_name,type,items,search_target=nil)
      case type
        when "analysis"
          myself=
          {
            name:@client.user.name,
            result:items[:myself]
            }
        comparison_user=
          {
            name:@client.user(user_name).name,
            result:items[:comparison_user]
            }
      return myself , comparison_user
        when "search"
          searchtarget=
          {
            name:@client.user(search_target).name,
            result:items[:search_target_user]
            }
        comparison_user=
          {
            name:@client.user(user_name).name,
            result: items[:comparison_user]
            }
          return searchtarget,comparison_user
      else
        puts "無効の値です"
      end
    end

    def result_of_comparison(user,comparison_user)
      puts  user[:result] > comparison_user[:result] ?   "#{ user[:name]}の方が#{user[:result]-comparison_user[:result]}多いです" :  "#{ comparison_user[:name]}の方が#{comparison_user[:result]-user[:result]}多いです"
    end

    def get_retweets_or_likes(user_name,get)
      pages=number_of_pages_judgment(user_name)
      get_all_tweets(user_name,pages,{item: get,mode: 'COMPARISON'})
    end

end


def analysis
  analysis= Analysis.new
  analysis.analysis_menu
end

検索機能と連結している処理の部分が重いですが、実装している内容は、シンプルですので、難しく考える必要はないです。

  1. データを作成
  2. データを渡す
  3. データーを加工
  4. データーを元に比較
  5. ターミナルに結果を表示

を繰り返しているだけです。

タイムラインという項目を選択した場合は、CSVファイルの有無を選択します。

  • CSVファイルを生成する場合
    →CSVファイルを生成
  • CSVファイルを生成しない場合
    →ターミナルに表示

という2つの挙動を取ります。

比較する場合は、シンプルにターミナル に比較結果を表示するだけです。

関連記事:Twitter bot 分析機能[拡張編]


まとめ:Twitter bot [総集編]

今回は、Twitter bot シリーズのまとめ記事を書いてみました。まとめ記事を書いた経緯は、アウトプット量を増やすためです。

人は忘れる生き物です。

  • 英単語を100個、暗記をした
  • 難しい方程式の公式を暗記した

と一時的に覚えられたとしても、継続的にアウトプットを繰り返さないと、いつか忘れてしまいます。なので僕は、「忘れる事を前提」としてプログラミング学習を行っております。忘れるという問題点を解決するのは、物理的に不可能です。したがって、覚えるという行為(インプット)よりも、思い出す(アウトプット)という行為を効率的に行える

  • 仕組み
  • 構造

を作るのに時間を割いた方が合理的です。要するに、いかに覚えるかではなく、いかに速く思い出せるかの方が優先度が高いという事です。
なので僕は、kazugrammingというプログラミング特化サイトを開設をし、学んだ事をWEBテキストに定期的にまとめ、いつでもググれる様な仕組みを作りました。手書きノートよりもWEBテキストの方が機動力が高く、パソコンの恩恵を感じるので、ぜひ活用してみてください。

以上、kazutoでした。

関連記事