全裸botのfollowerが増え過ぎで動かなくなっていたので直した

気がつくと全裸botを作ってから半年以上も経っていた。
GAEでTwitter botを作った - すぎゃーんメモ
当時はこんな下らないbotをfollowするヤツなんてそうそういないだろうと思っていたのだけど、意外と増え続けて、気がつくと1700とかになっている。

これでも取得件数が1000件を超えてしまうと(まず有り得ないだろうけどw)うまく動かなくなってしまう、か。何かもっと良い方法ないかなぁ。

って書いてたのに、本当に1000件超えてしまって、見事に動かなくなってしまったw
ここ最近、CPU Timeが朝くらいにquota制限まで達してしまい、17時にquotaがリセットされるまで動かない、という現象が続いていた。


これまでは、自動follow返しをするために"followers/ids", "friends/ids"から取得したfollowers, friendsのidをdatastoreに保存、更新し、その情報をもとに別のタイミングでcronからfollow返しやfollow外しをするようにしていた。
けど、このfollowers/friendsのid情報を現在保存しているものと照らし合わせながら更新する、という処理が非常に重く、datastoreの取得/更新の制限に引っかかったりしてしまうようだ。errorログをみると

BadRequestError: cannot put more than 500 entities in a single call

というのが出ている。


とにかくこの方法は使えない、ということがわかったので、id:yuroyoroさんに教えていただいた方法を真似することにした。
yuroyoro / yuroyoro-bots / source / lib / webapi / twitter4gae.py — Bitbucket
全裸botの全貌 - すぎゃーんメモ follow/remove の項
cronで5分ごとに"followers/ids", "friends/ids"を確認するときは、返ってくるJSONのテキストデータをそのまま保存し、10分ごとにそれらを元に差分を割り出してfollow/unfollowするように。
変更内容は以下参照。
フォロー返し関係が動かなくなっていたのを修正 · sugyan/Zenra@e0e558d · GitHub


ModelのIDSをこのように変更。

from google.appengine.ext import db

class IDS(db.Model):
    friends = db.TextProperty()
    followers = db.TextProperty()

ここにそれぞれfollowers/friendsのidリストのJSONデータが詰まっていくことになる。単一のエンティティを更新していくだけ。
1エンティティ1MBまでという制限があったと思うけど、follower数が何万とかにならない限り大丈夫なはず。


あとはそこの情報を使ってfollow/unfollowするだけ。

    def friendship(self):
        ids = IDS.get()
        friends   = set(simplejson.loads(ids.friends))
        followers = set(simplejson.loads(ids.followers))
        should_follow   = list(followers - friends)
        should_unfollow = list(friends - followers)
        random.shuffle(should_follow)
        random.shuffle(should_unfollow)
        logging.debug("should follow: %d" % len(should_follow))
        logging.debug("should unfollow: %d" % len(should_unfollow))
        # 繰り返し挑戦するので失敗してもタイムアウトになっても気にしない
        while len(should_follow) > 0 or len(should_unfollow) > 0:
            if len(should_follow) > 0:
                url = 'http://twitter.com/friendships/create/%s.json' % should_follow.pop()
                logging.debug(url)
                result = urlfetch.fetch(
                    url     = url,
                    method  = urlfetch.POST,
                    headers = self.auth_header,
                    )
                if result.status_code != 200:
                    logging.warn(result.content)
            if len(should_unfollow) > 0:
                url = 'http://twitter.com/friendships/destroy/%s.json' % should_unfollow.pop()
                result = urlfetch.fetch(
                    url     = url,
                    method  = urlfetch.POST,
                    headers = self.auth_header,
                    )
                if result.status_code != 200:
                    logging.warn(result.content)

非公開ユーザーはfollow返しが承認してもらえるまで差分として残り続けてしまい、何件もリクエストを送っているうちにタイムアウトになってしまう可能性があるので、途中で失敗して終わることも想定してランダムな順番で行う。


と、まぁこれで大丈夫なんじゃないかなーと思っているんだけど、どうだろうか。
少しこれで動かし続けて様子を見ることにします。