GAEでTwitter botを作った

初めて、Twitter botなるものを作ってみた。
http://twitter.com/zenra_bot
公式ページ:http://zen-ra.appspot.com/

  • フォローするとフォローし返します
  • フォローを外されると同様にフォローを外します
  • フォロワーの発言を拾って、勝手に全裸に書き換えます
    • 例:「@sugyan が全裸で言った: ********」
    • 非公開の発言は拾わないようにしています。
  • たまに独り言もつぶやきます

実装

Google App Engine(Python)で作りました。
ソースコードGitHubにて公開しています。
GitHub - sugyan/Zenra: 全裸にする


基本的にcronでフォロワーのチェック&更新、つぶやきを行っているだけ。
特に外部のライブラリを使わずにGAEのurlfetch APITwitter APIを直接叩いたり。


一番苦労したのがfollowerの更新処理で、

  1. 自分がフォローしているユーザを取得する(friends/ids)
  2. 自分をフォローしているユーザを取得する(followers/ids)
  3. それぞれの差分を調べて、
    1. フォローされているけど自分がまだフォローしていないユーザをフォロー(friendships/create)
    2. フォロー外されているのに自分がまだフォローしているユーザをリムーブ(friendships/destroy)

という処理をすることでfriendsとfollowerを常に等しくできる、と思ったのだけれど、GAEでは30秒以内にレスポンスを返さないといけないのでこれらの処理を一発で全部やろうとするとタイムオーバーになってしまうのでは、と考えた。
ので、1, 2, をそれぞれ独立した処理として行い取得結果をDatastoreに格納し、3.1, 3.2をまた独立した処理でそのDatastoreのデータを読み取って行うことにした。
具体的にはこんなカンジ。

#/usr/bin/env python
# -*- coding: utf-8 -*-

from google.appengine.ext import db


class IDS(db.Model):
    friend   = db.BooleanProperty()
    follower = db.BooleanProperty()
class TwitBot:
...
    # 自分のfriendsのデータを更新する
    def friends(self):
        url = 'http://twitter.com/friends/ids.json'
        result = urlfetch.fetch(
            url     = url,
            headers = self.auth_header,
            )
        if result.status_code == 200:
            keys = ["id:%d" % (id) for id in simplejson.loads(result.content)]
            # 既に登録されているidかどうかをチェックする
            for id in IDS.all().filter('friend =', True):
                key_name = id.key().name()
                # 登録されていれば処理の必要なし
                if key_name in keys:
                    keys.remove(key_name)
                # フォローしている筈だったのが外れている場合
                else:
                    id.friend = False
                    id.put()
            # 新規にフォローすべきidとして登録
            ids = []
            for key in keys:
                id = IDS.get_by_key_name(key)
                if id == None:
                    id = IDS(key_name = key, follower = False)
                id.friend = True
                ids.append(id)
            db.put(ids)

    # 自分のfollowersのデータを更新する
    def followers(self):
        url = 'http://twitter.com/followers/ids.json'
        result = urlfetch.fetch(
            url     = url,
            headers = self.auth_header,
            )
        if result.status_code == 200:
            keys = ["id:%d" % (id) for id in simplejson.loads(result.content)]
            # 既に登録されているidかどうかをチェックする
            for id in IDS.all().filter('follower =', True):
                key_name = id.key().name()
                # 登録されていれば処理の必要なし
                if key_name in keys:
                    keys.remove(key_name)
                # フォローされている筈だったのが外されている場合
                else:
                    id.follower = False
                    id.put()
            # 新規にフォローされたidとして登録
            ids = []
            for key in keys:
                id = IDS.get_by_key_name(key)
                if id == None:
                    id = IDS(key_name = key, friend = False)
                id.follower = True
                ids.append(id)
            db.put(ids)

    # 新たにフォローする
    def create(self):
        # フォローすべきidの抽出
        query = IDS.all()
        query.filter('follower =', True)
        query.filter('friend =',   False)
        id = query.get()
        if id:
            # APIへの送信
            url = 'http://twitter.com/friendships/create/%s.json' % (id.key().name()[3:])
            result = urlfetch.fetch(
                url     = url,
                method  = urlfetch.POST,
                headers = self.auth_header,
                )
            # 内部データの更新
            id.friend = True
            id.put()

    # フォローを外す
    def destroy(self):
        # リムーブすべきidの抽出
        query = IDS.all()
        query.filter('friend =',   True)
        query.filter('follower =', False)
        id = query.get()
        if id:
            # APIへの送信
            url = 'http://twitter.com/friendships/destroy/%s.json' % (id.key().name()[3:])
            result = urlfetch.fetch(
                url     = url,
                method  = urlfetch.POST,
                headers = self.auth_header,
                )
            # 内部データの更新
            id.delete()

で、これらのメソッドをそれぞれcronで5分間隔くらいで叩いて更新するようにしています。
(似たような処理が並んでいるのでそこはあとでリファクタリングするとして…)
これでも取得件数が1000件を超えてしまうと(まず有り得ないだろうけどw)うまく動かなくなってしまう、か。何かもっと良い方法ないかなぁ。


あと発言を全裸にする処理は以前に書いたYahooの形態素解析APIを使った方法で行っています。
Pythonから全裸で形態素解析をする - すぎゃーんメモ
もう少し自然な日本語で全裸にできるよう、このあたりをこれから改良していくつもり。