勉強も兼ねて、お見合いbotを作ってみた。

事の始まりはTwitterでのやりとり

@kin7008 はじまったな。 
 RT @aohasu: お見合いBot…ゴクリ 
  QT @sinsoku_listy: 両方の単語を収集するbotがいたら・・・ 
   RT @aohasu: 彼女欲しいトークもむなしい… 
    QT @miloooks: 彼氏欲しいトークがむなしい…

もっと早く作ろうと思ってたけど、なかなか私生活の方が忙しかったり、
他の事をやってたりで遅くなってしまいましたorz
せっかく作った訳ですし、ブログに書いておきます。

お見合いbot

お見合いbot@omiai_bot

機能

  • 「彼女ほしい」「彼氏ほしい」を検索します。
  • "@user" を "_user"に置換してから非公式RT*1します。
  • http://が含まれていると非公式RTしません。
  • 同意する内容がRT/QT前にないと非公式RTしません。

勉強用ですので、機能はショボいです(´・ω・)ショボーン
他のbot作ってる人はみんな凄い(´・ω・)ッス

ソース

テストコードも書いてませんし、やっつけ仕事になってます・・・
ソース全部を説明するのは面倒なので、簡単に気になった所だけ説明を書いておきます。


ちなみに、ライブラリはtweepyを使用しました。

omiaibot.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from google.appengine.ext import db
import re
import tweepy

class Status(db.Model):
    id   = db.IntegerProperty()
    user = db.StringProperty()
    text = db.TextProperty()

class OmiaiBot(webapp.RequestHandler):
    def get(self):
        self.api = self.authTwitter()
        
        searchwords=[u'彼女ほしい OR 彼氏ほしい']
        tweets = self.search(searchwords)
        tweets = self.filter(tweets)

        self.response.headers['Content-Type'] = 'text/html'
        for tweet in tweets:
            self.response.out.write('RT @' + tweet.from_user + ': ' + tweet.text + '<p>')
            self.post(tweet)
            self.put(tweet)

    def search(self, words):
        ''' wordsの語句を一つずつ検索し、検索結果を一つのリストにして返す。
        '''
        resultlist = []
        for word in words:
            resultlist += self.api.search(word.encode('utf-8'))

        # 重複要素を消去したリストを返す
        return sorted(set(resultlist), key=resultlist.index)

    def _isPosted(self, status):
        ''' DBにstatusのidが保存されていたらTrue, 保存されていなければFalseを返す。
        '''
        query = Status.all()
        query.filter('id =', status.id)
        return query.count() > 0

    def _isAgree(self, text):
        ''' text内の「彼女ほしい」に同意していればTrue, 同意していなければFalseを返す。
        '''
        doui = re.search(u'(ほしい|欲しい)', text)
        rtqt = re.search('(RT|QT)', text)
        return doui != None and rtqt != None and doui.span() < rtqt.span()

    def filter(self, status):
        ''' statusから条件に合わないステータスを消去する。
             * 前回までのcronで既にpostしているステータス
             * リンク(http)がついてるステータス
             * 同意するpostがRT/QTの前にないステータス
        '''
        filterdStatus = []

        for s in status:
            if (not self._isPosted(s)) and (not re.search('http://', s.text)) and self._isAgree(s.text):
                filterdStatus.append(s)

        return filterdStatus

    def post(self, status):
        ''' @userを_userに置換し、リプライが飛ばないように修正してからpostする。また、160字を超えていた場合はpostしない。
        '''
        text = 'RT @' + status.from_user + ': ' + status.text
        cnv_status = re.sub('@', '_', text)
        if len(cnv_status) < 160:
            self.api.update_status(cnv_status)

    def put(self, status):
        ''' postするstatusをデータストアに保存する
        '''
        s = Status()
        s.id = status.id
        s.user = status.from_user
        s.text = status.text
        s.put()

    def authTwitter(self):
        ''' Twitterの認証を行い、tweepy.APIを返す。
        '''
        ckey = ''
        csec = ''
        akey = ''
        asec = ''
        auth = tweepy.OAuthHandler(ckey, csec)
        auth.set_access_token(akey, asec)

        return tweepy.API(auth)

if __name__ == '__main__':
    import doctest
    doctest.testmod()

まず、処理の流れですが、15行目の"def get(self):"から始まります。
普通のプログラムのint main()みたいなものです。

というわけで、16行目から順に説明します。

OAuth認証
16         self.api = self.authTwitter()
~~
83    def authTwitter(self):
84        ''' Twitterの認証を行い、tweepy.APIを返す。
85        '''
86        ckey = ''
87        csec = ''
88        akey = ''
89        asec = ''
90        auth = tweepy.OAuthHandler(ckey, csec)
91        auth.set_access_token(akey, asec)
92
93        return tweepy.API(auth)

ここでtweepy使ってOAuth認証してます。
twitterAPI用pythonライブラリtweepyを使えるようになるまで。 - Number6の「あーあ、俺に狐の嫁さんできねぇかなぁ!!」のエントリが分かりやすいです。

twitterから検索
18        searchwords=[u'彼女ほしい OR 彼氏ほしい']
19        tweets = self.search(searchwords)
~~
28    def search(self, words):
29        ''' wordsの語句を一つずつ検索し、検索結果を一つのリストにして返す。
30        '''
31        resultlist = []
32        for word in words:
33            resultlist += self.api.search(word.encode('utf-8'))
34
35        # 重複要素を消去したリストを返す
36        return sorted(set(resultlist), key=resultlist.index)

ここではリストで渡した単語を順に検索し、検索結果を返しています。*2

フィルタリング
20        tweets = self.filter(tweets)

非公式RTの対象でないつぶやきを除去します。

45    def _isAgree(self, text):
46        ''' text内の「彼女ほしい」に同意していればTrue, 同意していなければFalseを返す。
47        '''
48        doui = re.search(u'(ほしい|欲しい)', text)
49        rtqt = re.search('(RT|QT)', text)
50        return doui != None and rtqt != None and doui.span() < rtqt.span()
~~

RT/QT前に"ほしい"、"欲しい"があるかをチェックしています。
「@hoge 欲しい RT: @piyo 彼女ほしい」とかならTrueになります。
今回、初めてタプルの比較を知りました。

twitter非公式RT
66    def post(self, status):
67        ''' @userを_userに置換し、リプライが飛ばないように修正してからpostする。また、160字を超えていた場合はpostしない。
68        '''
69        text = 'RT @' + status.from_user + ': ' + status.text
70        cnv_status = re.sub('@', '_', text)
71        if len(cnv_status) < 160:
72            self.api.update_status(cnv_status)

非公式RTをするためには、69行目のようにpostする文字列を作る必要があります。
また、160文字を超える可能性もあるので、文字数チェックも必要です。
今回は上記2点に加え、@を_に置換する処理も加えています。

データストアに保存

非公式RTした呟きはデータストアに登録しておきます。
botはcronで動かすので、何回も同じ呟きを非公式RTしても困るので、データストアをチェックして、
今まで非公式RTしていないつぶやきだけを対象に、非公式RTさせます。

GitHub

参考になるか分からないですが、ソースをGitHubに上げておきます。
sinsoku's twitterbot at master - GitHub

*1:正確には非公式RTですら無いですが、postしますより分かりやすいので非公式RTと表現しています

*2:リストにする意味は特にないです。最初は [u'彼女ほしい', u'彼氏ほしい'] にしてたのですが、OR使えたので。