sifue's blog

プログラマな二児の父の日常

XML編集で返信内容を作れるRubyの TwitterBotスクリプト2

というわけで前回、

http://d.hatena.ne.jp/sifue/20090923/1253697249


で紹介したtwitter4rを使ったXML編集で返信内容を作れるTwitterBotのスクリプトですが、風邪引いて動けないこともあって、改良してみました。
rubyrubygems、twitter4rやcronの設定、設定ファイルの編集法などは前回のエントリーを御覧下さい。

  • 改良点
  1. 返信とつぶやきのスクリプトを分離して、返信は1分以内に返すが、つぶやきは1時間に1回のように頻度を変えて設定できるようにした。
  2. ウェルカムポストと返信のコンテンツにて#user_name#という文字列を入れると、その人の名前(ユーザー名ではない)に置換してくれるようにした。
  3. 返信は、フォロワーのつぶやき捕捉用と自分自身への直接返信捕捉用の二つを用意した。
  4. 前回あったランダム投稿のtrue・falseの設定が反映されない問題を修正した。
  5. 返信したら、つぶやかないという仕様も変更し、返信とつぶやきを切り分け関係なくどちらもするようにした。
  6. フォロー返しするが、外し返しはしないように変更(bot同士のフォローなどのため)


このような改良です。ダウンロードはこちらから。
含まれるファイルは以下7つ。

conf.yml : アカウント情報及び返信インターバル設定ファイル
main_post.rb : ウェルカム&つぶやき用スクリプト
main_reply.rb : 返信用スクリプト
post.xml : つぶやき用のコンテンツ
reply.xml : フォロワーの発言への返信用のコンテンツ
reply_for_me.xml : 自分への返信への返信用のコンテンツ
welcome.xml : ウェルカムポスト用のコンテンツ


となっています。これらをうまく設定することで、かなりインタラクティブbotを制作できるようになるのではと思います。ただ、bot同士でフォローしあったりすると、コンテンツによってはループして短期間で大量のポストをしまくって落ちたりするので、そのへんは自己責任ということで…。


あと、引っかかったキーワード全てを捕捉して返信するようになっていますので、その辺のコンテンツ作りには注意が必要です。


ちなみに、現状このスクリプトで動いているbotは、

  • アイシャ

http://twitter.com/aisha_bot

になります。現在Ubuntu8.04で、つぶやきは1時間に一回、返信は1分に一回のペースで稼働しています。


何かありましたら、このエントリーにコメント頂ければうれしいです。


あと、twitter4rのRDocがあんまりいけてないです。
Twitter::Userのメソッドがこっちにまとまっていて便利でした。

http://d.hatena.ne.jp/deris/20091028/1256744652


以下、設定ファイル及びソースの内容です。リファクタ…した方がいいですね…すみません。動作安定したらクラスにまとめようかなと思います。


conf.yml : アカウント情報及び返信インターバル設定ファイル

## YAML形式なので、タブのインデントや順番、スペースの間隔を崩さないように編集してください。
## loginはユーザー名、post_randomはtrue(ランダムで内容選択)かfalse(回数少ない順)で、
## 返信のインターバルは秒で設定してください(2時間は7200秒、6時間は21600、12時間は43200など)
bot:
  login: aisha_bot
  password: password
  post_random: true
  interval: 60


main_post.rb : ウェルカム&つぶやき用スクリプト

# Twitter Bot スクリプト(ウェルカムポスト&つぶやき用)
# 
# フォローされている人の中で、自分がフォローしていないひとが入ればフォローする。
# はじめての人にはウェルカムポストをし、ひとりごともつぶやくBot。
#
# ・ウェルカムポスト、リプライの際は#user_name#を名前に置換してポスト
# ・コメント選択は、ランダムか、回数少ない順かつ最後に投稿されたもの順のどちらかが選択可
#
# 2009/09/23, updated 2010/01/03
# Copyright(c) 2009- Soichiro YOSHIMURA

require 'rubygems'
gem 'twitter4r', '>=0.3.0'
require 'twitter'
require 'twitter/console'
require 'kconv'
require 'yaml'
require "rexml/document" 

# カレントディレクトリ設定
Dir::chdir(File::dirname(__FILE__))

# 各種設定を読み込む
env = 'bot'
conf_file = 'conf.yml'
conf = YAML::load(File.read(conf_file))
bot_conf = conf[env]
is_random = bot_conf['post_random'] # ランダムポストかどうか

# Twitter4Rのインスタンスを生成する
twitter = Twitter::Client.from_config( conf_file , env )

# 新しいfollowerをフォロー返しす。
follower_names = Array.new()
twitter.my(:followers).each { |user| follower_names << user.screen_name}
friend_names = Array.new()
twitter.my(:friends).each { |user| friend_names << user.screen_name}
add_followers = follower_names - friend_names
add_followers.each { |name|
	begin
	 	twitter.friend(:add, name)
	rescue Twitter::RESTError
	end
 }

# フォローをやめた人をあえて外さないようコメントアウト
#remove_followers = friend_names - follower_names
#remove_followers.each { |name|
#	begin
#		 twitter.friend(:remove, name)
# 	rescue Twitter::RESTError
#	end
# }

###### 新規に登録してくれた人のためのメッセージを返す #####
doc_post = REXML::Document.new(File.read('welcome.xml'))
post = doc_post.root()
comments = post.get_elements('comment')
add_followers.each { |name|
  comment = nil
  if is_random
    comment = comments[rand(comments.length)]
  else
  # ポスト回数・日付順に破壊的ソート
    comments.sort! { |a,b|
      a.attributes.get_attribute('last_time').to_s.to_i <=> b.attributes.get_attribute('last_time').to_s.to_i
    }.sort! { |a,b|
      a.attributes.get_attribute('count').to_s.to_i <=> b.attributes.get_attribute('count').to_s.to_i
    }
    # 最も古くてポスト回数が少ないものを選択
    comment = comments[0]
  end

  # #user_name# を返信を行う名前に置換 (なぜかtwitter.user('mylogin')で得られないため苦肉の策)
  user_name = ''
  twitter.timeline_for(:user, :id => name){| status | user_name = status.user.name}
  name_replaced_comment = comment.attributes.get_attribute('content').to_s
  name_replaced_comment = name_replaced_comment.gsub(/#user_name#/, user_name)
  puts "replace #user_name# to #{user_name}"

  # ポスト!
  twitter.status(:post,
  Kconv.kconv( "@" + name + " " + name_replaced_comment,
     Kconv::UTF8 ) )

  # 属性を更新
  comment_attrs = comment.attributes
  count = comment_attrs.get_attribute('count').to_s.to_i
  comment.delete_attribute('count')
  comment.add_attribute('count', (count + 1).to_s)
  comment.delete_attribute('last_time')
  comment.add_attribute('last_time', Time.now.to_i.to_s)
  puts "post_welcom(" +  name + "): " + comment.to_s
}
# ファイルに保存
io = open('welcome.xml','w')
doc_post.write(io)
io.close

########### ひとりごとをポストする場合のスクリプト ######
doc_post = REXML::Document.new(File.read('post.xml'))
post = doc_post.root()
comments = post.get_elements('comment')

comment = nil
if is_random
  comment = comments[rand(comments.length)]
else
# ポスト回数・日付順に破壊的ソート
  comments.sort! { |a,b|
    a.attributes.get_attribute('last_time').to_s.to_i <=> b.attributes.get_attribute('last_time').to_s.to_i
  }.sort! { |a,b|
    a.attributes.get_attribute('count').to_s.to_i <=> b.attributes.get_attribute('count').to_s.to_i
  }
  # 最も古くてポスト回数が少ないものを選択
  comment = comments[0]
end

# ポスト!
twitter.status(:post,
 Kconv.kconv( comment.attributes.get_attribute('content').to_s,
   Kconv::UTF8 ) )

# 属性を更新
comment_attrs = comment.attributes
count = comment_attrs.get_attribute('count').to_s.to_i
comment.delete_attribute('count')
comment.add_attribute('count', (count + 1).to_s)
comment.delete_attribute('last_time')
comment.add_attribute('last_time', Time.now.to_i.to_s)
puts "post: " + comment.to_s

# ファイルに保存
io = open('post.xml','w')
doc_post.write(io)
io.close


main_reply.rb : 返信用スクリプト

# Twitter Bot スクリプト(返信用)
# 
# フォロワーの誰かがヒットするキーワードを言っていれば、コメントをランダムにリプライし、
# 自分宛の返信にもキーワードに対して返信を行うBot。
#
# ・リプライの際は#user_name#を名前に置換してポスト
# ・コメント選択は、ランダムか、回数少ない順かつ最後に投稿されたもの順のどちらかが選択可
#
# 2009/09/23, updated 2010/01/03
# Copyright(c) 2009- Soichiro YOSHIMURA

require 'rubygems'
gem 'twitter4r', '>=0.3.0'
require 'twitter'
require 'twitter/console'
require 'kconv'
require 'yaml'
require "rexml/document" 

# カレントディレクトリ設定
Dir::chdir(File::dirname(__FILE__))

# 各種設定を読み込む
env = 'bot'
conf_file = 'conf.yml'
conf = YAML::load(File.read(conf_file))
bot_conf = conf[env]
my_name = bot_conf['login'] # ログイン名
is_random = bot_conf['post_random'] # ランダムポストかどうか
interval = bot_conf['interval'].to_i # 間隔の秒数

# Twitter4Rのインスタンスを生成する
twitter = Twitter::Client.from_config( conf_file , env )

###### 返信内容のXMLのDOMを取得し、一つ一つ返信していく ########
doc_rep = REXML::Document.new(File.read('reply.xml'))
reply = doc_rep.root()
twitter.timeline_for( :friends) do | status |
  
  # インターバル時間より前のコメントはパス
  next if status.created_at < Time.at(Time.now.to_i - interval)
  # 自分の投稿はパス
  next if status.user.screen_name == my_name

  reply.elements.each{|k|
   term = k.attributes.get_attribute('term').to_s
   # 一つ一つのキーワードに関して、そのキーワードを含んでいるか
   next if !status.text.include?(term)

   # 含んでいればどのコメントをポストするか判定へ
   comments = k.get_elements('comment')
   comment = nil
   if is_random
    comment = comments[rand(comments.length)]
   else
    # ポスト回数・日付順に破壊的ソート
    comments.sort! { |a,b|
      a.attributes.get_attribute('last_time').to_s.to_i <=> b.attributes.get_attribute('last_time').to_s.to_i
    }.sort! { |a,b|
      a.attributes.get_attribute('count').to_s.to_i <=> b.attributes.get_attribute('count').to_s.to_i
    }
    # 最も古くてポスト回数が少ないものを選択
    comment = comments[0]
   end

  # #user_name# を返信を行う名前に置換
  name_replaced_comment = comment.attributes.get_attribute('content').to_s
  name_replaced_comment = name_replaced_comment.gsub(/#user_name#/, status.user.name)
  puts "replace #user_name# to #{status.user.name}"

   # ポスト!
   twitter.status(:post,
     Kconv.kconv(
       "@" + status.user.screen_name + " " + name_replaced_comment,
       Kconv::UTF8 ) )

   # 属性を更新して、返信フラグを立てる
   comment_attrs = comment.attributes
   count = comment_attrs.get_attribute('count').to_s.to_i
   comment.delete_attribute('count')
   comment.add_attribute('count', (count + 1).to_s)
   comment.delete_attribute('last_time')
   comment.add_attribute('last_time', Time.now.to_i.to_s)
   puts "post_reply(" +  status.user.screen_name + "): " + comment.to_s
  }
end
#XMLの更新を出力
io = open('reply.xml','w')
doc_rep.write(io)
io.close

###### 自分宛の返信内容のXMLのDOMを取得し、一つ一つ返信していく ########
doc_rep = REXML::Document.new(File.read('reply_for_me.xml'))
reply = doc_rep.root()
twitter.timeline_for( :friends) do | status |

  # もし自分あての返信でなければパス
  reg = Regexp.new("^@" + my_name)
  next if reg.match(status.text) == nil

  # インターバル時間より前のコメントはパス
  next if status.created_at < Time.at(Time.now.to_i - interval)
  # 自分の投稿はパス
  next if status.user.screen_name == my_name

  reply.elements.each{|k|
   term = k.attributes.get_attribute('term').to_s
   # 一つ一つのキーワードに関して、そのキーワードを含んでいるか
   next if !status.text.include?(term)

   # 含んでいればどのコメントをポストするか判定へ
   comments = k.get_elements('comment')
   comment = nil
   if is_random
    comment = comments[rand(comments.length)]
   else
    # ポスト回数・日付順に破壊的ソート
    comments.sort! { |a,b|
      a.attributes.get_attribute('last_time').to_s.to_i <=> b.attributes.get_attribute('last_time').to_s.to_i
    }.sort! { |a,b|
      a.attributes.get_attribute('count').to_s.to_i <=> b.attributes.get_attribute('count').to_s.to_i
    }
    # 最も古くてポスト回数が少ないものを選択
    comment = comments[0]
   end

  # #user_name# を返信を行う名前に置換
  name_replaced_comment = comment.attributes.get_attribute('content').to_s
  name_replaced_comment = name_replaced_comment.gsub(/#user_name#/, status.user.name)
  puts "replace #user_name# to #{status.user.name}"

   # ポスト!
   twitter.status(:post,
     Kconv.kconv(
       "@" + status.user.screen_name + " " + name_replaced_comment,
       Kconv::UTF8 ) )

   # 属性を更新して、返信フラグを立てる
   comment_attrs = comment.attributes
   count = comment_attrs.get_attribute('count').to_s.to_i
   comment.delete_attribute('count')
   comment.add_attribute('count', (count + 1).to_s)
   comment.delete_attribute('last_time')
   comment.add_attribute('last_time', Time.now.to_i.to_s)
   puts "post_reply_for_me(" +  status.user.screen_name + "): " + comment.to_s
  }
end
#XMLの更新を出力
io = open('reply_for_me.xml','w')
doc_rep.write(io)
io.close


XML類のフォーマットは前回と一緒。ただし、時期などを見ながら結構コンテンツの中身を変えています。


post.xml : つぶやき用のコンテンツ
welcome.xml : ウェルカムポスト用のコンテンツ

<?xml version='1.0' encoding='UTF-8'?>
<post>
    <comment last_time='1262415850' count='21' content='…ムエ…アメ…カヤキス! カヤキス レビタ!(あなたは…誰…黒! 黒い悪魔!)'/>
    <comment last_time='1262149448' count='20' content='イメ アメ ホオ? メチセノ フユトイ!(みんなどこ? 誰か返事して!)'/>
 </post>


reply.xml : フォロワーの発言への返信用のコンテンツ
reply_for_me.xml : 自分への返信への返信用のコンテンツ

<?xml version='1.0' encoding='UTF-8'?>
<reply>
    <keyword term='アイシャ'>
        <comment last_time='1262502253' content='ありがとう!#user_name#も頑張ってね!' count='6'/>
        <comment last_time='1262498652' content='わたしも頑張るね!#user_name#も頑張ってね!' count='6'/>
    </keyword>
</reply>

以上です。好きなようにいじって使ってもらえればと思います。