JSの設定

2010年5月8日土曜日

ねづっちボットを作る

こういうわけで、7月からtwitterAPIを使うのにベーシック認証が使えなくなるのですね。

マイコミジャーナルではツイッターで記事につけたコメントを集めたまじつぶというサービスをやっとるんですが、「記事にコメントをつける」ところでベーシック認証を使ってる。なので代わりの方法であるOAuth(http://j.mp/arIIzhを参考にオォースと読むことにしてる)に切り替えないといけないんだけど何だかややこしい。ツイッターのボットでも作ってみたらいい演習になるんじゃない?と考えました。

どんなボットがいいかな〜と思って何週間か悩んでいたらテレビにねづっちがでていた。でツイッターに「ねづっちボットってあったら面白そう」と言ったら面白そうですねという反応があった( http://j.mp/9FMSIR http://j.mp/akKdff )のでやる気になりGWに作ることに。
※比較的最近使い始めたdeliciousのrubyタグはねづっちbot作成のための資料がほとんど

◯ねづっち作成方針

ー 大方針 ー
・@nezucchi_bot [お題] のように話しかけてもらい、[お題]でツイッターの世界を検索。全角文字がなるべく多く含まれるツイートを探し、これを[心]とする。[心]を、Yahooの形態素解析サービス( http://j.mp/abW6hx )を使って品詞単位に分割し、名詞だけを集める。この中から「全てひらがなでない・全て半角でない・[お題]とのレーベンシュタイン距離がなるべく遠い」という条件で候補を絞り、最終的に残ったものの中からランダムで[答え]を決定する。

こうして、「ととのいました![お題]とかけまして[答え]とときます、その心は[心]!」というなぞかけを完成させる。

・誰も話しかけてくれない時はお題を自分から探しにいく。はてなキーワードからランダムに1つを選択。

ー 小方針 ー
・一度答えたお題にそれ以降答えないようにするため、お題のツイートIDをDBに保存する。
・DBを簡単に利用するため、また、ActiveSupportの便利なモジュールを利用するため、Railsを使う。
・ボットはウェブアプリケーションではなくバッチで動かすので script/runner を使う。
・長いURLをポストする時は短縮する( bit.ly )

◯環境構築

まだあまり使いこなせていないMacの環境構築から始める。
OSバージョン:10.6.3
Core2 Duo, メモリ4GB
Rails : 2.3.5(初めからインストールされてた)
mysql : 5.1.46 http://j.mp/cOB7ch を参考に。64bit版じゃないと動かない
XCode : Cコンパイラとか。Mac付属のCDに入ってるらしいんだけどDLした( http://j.mp/cNzefy )
※Windowsの場合はVisual Studio 2008 Express Editionあたりをインストールすればよかろう。

・プロジェクトの作成

# rails nezubot --database=mysql
# cd nezubot
# ruby script/generate scaffold Nezubot postid:string user:string tubu:text
# vi config/database.yml -> adapter: mysql
# rake db:create
# rake db:migrate

・OAuthの準備をする
http://j.mp/9TbPyH を参考に4つのキーを取得する。

・短縮URLの準備をする
bitlyのAPIキーを取得しておく。

・Yahoo形態素解析の準備をする
yahooのAPIキーを取得しておく。

◯実装

長いので急所だけ。

#!/usr/bin/env ruby
# coding: utf-8

require 'json'
require 'rest_client'
require 'uri'
require 'oauth'
require 'pp'
require 'leven' # http://j.mp/awOXy6 を外部ファイル化

# URL短縮関数
def shorten(long_url)
 id = 'xxx'
 api_key = 'xxx'
 req = "http://api.bit.ly/v3/shorten?login=#{id}&apiKey=#{api_key}&uri=#{URI.encode(
long_url)}&format=json"

 json = RestClient.get req
 jhash = ActiveSupport::JSON.decode json
 jhash["data"]["url"]
end

# お題[q]から[心]を求める関数
def getBestres(q)
 bestres = []
 uri = "http://search.twitter.com/search.json?q=" + URI.encode(q) + "&locale=ja";

 begin
  json = RestClient.get(uri)
  jhash = ActiveSupport::JSON.decode(json)
 rescue
  abort("rescued in getjson")
 end

 if( jhash["results"] != [])
  jhash["results"].each{ |r|
   if( r["text"].gsub(/[ -~。-゚]*/,"").length > 200 )
    bestres = [r["id"],r["text"],r["from_user"],q ]
    break
   end

   bestres = [r["id"],r["text"],r["from_user"],q ] if bestres==[]
  }
 end

 return bestres
end #getBestres


# [心]から[答え]を決定する関数
def getBestnoun(bestres)
 yuri = "http://jlp.yahooapis.jp/MAService/V1/parse?appid=[API_KEY]&results=ma,uniq&uniq_filter=9|10&sentence="
 nounlist = []

 if( bestres != [] )
  yuri = URI.encode(yuri + bestres[1])
  yhash = Hash.from_xml RestClient.get(yuri)

  words = yhash["ResultSet"]["ma_result"]["word_list"]["word"]
  words.each{ |w|
   nounlist.push w["surface"] if w["pos"]=="名詞"
  }

# 名詞ではなくても、括弧類で囲まれた語は候補に加える
  nounlist.concat bestres[1].scan(/(「.*?」)|(\[.*?\])|(『.*?』)|((.*?))|(\(.*?\))/).
flatten
  nounlist.uniq!

# casecmpは全角半角無視で文字列同士を比べる
  nounlist = nounlist.select{ |i| (i=~/^[^ -~。-゚]*$/)==0 && !(i=~/^[ぁ-ん]*$/) && i.length
> 5 && i.casecmp(bestres[3])!=0 }

(中略)

 return nounlist.rand
end

# 取得した4つのキーを使ってOAuthの下準備をする
begin
consumer = OAuth::Consumer.new(
CONSUMER_KEY,
CONSUMER_SECRET,
:site => 'http://twitter.com'
)
access_token = OAuth::AccessToken.new(
consumer,
ACCESS_TOKEN,
ACCESS_TOKEN_SECRET
)
response = access_token.get('http://twitter.com/statuses/mentions.json')
rescue
abort("rescued in oauth.new")
end

# 出してもらったお題を処理してなぞかけを送信する
JSON.parse(response.body).each do |status|
 news = []
 postid = status['id']
 user = status['user']
 tubu = status['text'].gsub(" "," ")
 next unless (tubu=~/^(@|\.@|@)/)==0

 ActiveRecord::Base.cache do
  news = Spbot.find(:all, :conditions => ["postid = ?", [postid] ])
 end

 if( news==[] )
 begin
  q = tubu.split(' ')[1] || ""
  next if(q=="")
  bestres = getBestres(q)
  bestnoun = getBestnoun( bestres ) || "error:#{q}:できませんでした!(T-T)"

  ans = bestnoun
  ans = ".@#{user['screen_name']} ととのいました! #{q} とかけまして #{bestnoun} とときます、その心は次のツイートで!" if !ans.include? "error"

  if !$DEBUG
   access_token.post('http://twitter.com/statuses/update.json','status'=> ans )
   access_token.post("http://twitter.com/statuses/retweet/#{bestres[0].to_s}.jso
n") if !ans.include? "error"
  end
 rescue
  abort("rescued in postid:#{postid}")
 else
  Spbot.create(:postid => postid, :user => user, :tubu => tubu) if !$DEBUG
 end
end

end if ARGV[0] #JSON.parse

# 一人なぞかけは省略

◯定期実行
0 * * * * ( script/runner nezu.rb) # 一人つぶやき
*/10 * * * * ( script/runner nezu.rb reply) # お題に答える

◯完成品
http://twitter.com/nezucchi_bot

5/26 追記 別のねづっちボット