edo1z blog

プログラミングなどに関するブログです

twitterの自動フォロワー管理ツールをつくってみた

GAE × Python2.7 × Twitter Api1.1でtwitterの自動フォロワー管理ツールをつくりました。

最初フォロー・フォロー削除共に1日480件という設定にしていたら、アップして数時間で速やかにtwitterからアカウント凍結されました。。まあさすがに1日48件なら大丈夫だろうと思ってるんですが、2回目以降凍結されると復活が困難らしいので、近日中にtwitterで新たな船出を迎えるかもしれません。

機能は下記になります。自分的にはかなり素早く作った方で、仕事終わりに半分寝ながらつくっていたので抜け漏れが結構ある可能性があります。今のところこれで特に問題なく動いているのですが、稼働させたばかりなので根本的なミスをしている可能性もあります。

  • 設定したキーワードをつぶやいている人を自動でフォローするキーワードは3つまで設定できる)
  • 1日当たりのフォロー数も自由に設定可能(様々なリスクがあるので最大100件まで)
  • フォローしてから一定期間経ってもリフォローされない場合は、フォロー解除する
  • フォロー解除までの日数も自由に設定可能
  • 1日当たりのフォロー解除数も自由に設定可能(同じく最大100件まで)(でも100件でも十分凍結対象になりうるらしい。)
  • フォロー解除したくないアカウントは、twitterのリストに登録しておけば、フォロー解除対象から外れる
  • フォローする際は、以前フォロー削除したアカウントかどうかを確認して、以前のフォロー削除から一定期間が経過していない場合はフォローしない

このぐらい。リフォロー機能はまだない。

今回はtweepyという、python用のtwitter API操作モジュールを使いました。ただtwitter API 1.1 にtweepyが対応していなかったのですが、こちらsakitoさんという人がtweepy2というのを作ってくれていたのでこれを使わせていただきました。今回作った限りでは問題なく動いてます。ありがたいです。


ソースコードは下記です。

# -*- coding: utf-8 -*-
import os
import webapp2
import jinja2
import cgi
from google.appengine.ext import db
import re
import math
import datetime
import urllib
import tweepy2

jinja_environment = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)))

CK = 'twitterのコンシューマーキー'
CS = 'twitterのコンシューマーシークレット'
AT = 'twitterのアクセストークン'
ST = 'twitterのシークレットトークン'

#your screen name
yourScreenName = 'twitterの自分のスクリーンネーム'
#1時間当たりのAPI呼び出し回数
callApiOneHour = 1
#1回当たり最大サーチ数(削除時)
maxDeleteSearchCnt = 3
#1回当たり最大サーチ数(フォロー時)
maxFollowSearchCnt = 5
#削除後の再フォローまでの日数
oneMoreFollowTime = 30

#database
class Database(db.Model):
    protects = db.ListProperty(int)
    friendKey1 = db.StringProperty()
    friendKey2 = db.StringProperty()
    friendKey3 = db.StringProperty()
    friendKeyFlag = db.IntegerProperty()
    numOnedayF = db.IntegerProperty()
    numOnedayD = db.IntegerProperty()
    deleteTime = db.IntegerProperty()
    follow1day = db.ListProperty(int)
    follow2day = db.ListProperty(int)
    follow3day = db.ListProperty(int)
    follow4day = db.ListProperty(int)
    follow5day = db.ListProperty(int)
    followTime = db.DateProperty()

class DeleteAccountData(db.Model):
    deleteTime = db.DateProperty()

class MainHandler(webapp2.RequestHandler):
    def get(self):
        api = createClient() #create twitter client

        [lovesCnt,onlyFriendsCnt,onlyFollowersCnt] = countLoveFriendFollower(api)

        templateValues = {
            'lovesCnt': lovesCnt,
            'onlyFriendsCnt': onlyFriendsCnt,
            'onlyFollowersCnt': onlyFollowersCnt,
        }

        #read database
        data = db.get(db.Key.from_path('Database', 'endo'))
        if data:
            datas = {
                'pro': data.protects,
                'fk1': data.friendKey1,
                'fk2': data.friendKey2,
                'fk3': data.friendKey3,
                'nOF': data.numOnedayF,
                'nOD': data.numOnedayD,
                'dt' : data.deleteTime
            }
            templateValues.update(datas)

        template = jinja_environment.get_template('index.html')
        self.response.out.write(template.render(templateValues))

    def post(self):
        api = createClient() #create twitter client

        fk1 = cgi.escape(self.request.get('fk1'), True)
        fk2 = cgi.escape(self.request.get('fk2'), True)
        fk3 = cgi.escape(self.request.get('fk3'), True)
        nOF = self.request.get('nOF')
        nOD = self.request.get('nOD')
        dt  = self.request.get('dt')

        p = re.compile('^d+$')
        if p.match(nOF) and p.match(nOD) and p.match(dt):
            nOF = int(nOF)
            nOD = int(nOD)
            dt  = int(dt)
            if (nOF >= 1 and nOF <= 100) and (nOD >= 1 and nOD <= 100) and (dt >= 1 and dt <= 5):
                #Input DB
                data = db.get(db.Key.from_path('Database', 'endo'))
                if not data:
                    data = Database(key_name='endo')
                data.friendKey1 = fk1
                data.friendKey2 = fk2
                data.friendKey3 = fk3
                data.numOnedayF = nOF
                data.numOnedayD = nOD
                data.deleteTime = dt
                data.put()
                self.redirect('/')

        errmsg = 'Oooops!!!<br />friends of one day is 1 - 100<br />'
        errmsg += 'Deletes of one day is 1 - 100<br />'
        errmsg += 'Time of Delete is 1 - 5<br />'

        data = db.get(db.Key.from_path('Database', 'endo'))
        pro = ''
        if data:
            pro = data.protects

        [lovesCnt,onlyFriendsCnt,onlyFollowersCnt] = countLoveFriendFollower(api)

        templateValues = {
            'lovesCnt': lovesCnt,
            'onlyFriendsCnt': onlyFriendsCnt,
            'onlyFollowersCnt': onlyFollowersCnt,
            'pro': pro,
            'fk1': fk1,
            'fk2': fk2,
            'fk3': fk3,
            'nOF': nOF,
            'nOD': nOD,
            'dt' : dt,
            'errmsg': errmsg
        }

        template = jinja_environment.get_template('index.html')
        self.response.out.write(template.render(templateValues))

class Follow(webapp2.RequestHandler):
    def get(self):
        api = createClient()

        #エラーの場合のメッセージ
        templateValues = {
            'errmsg': 'ERROR!!! you must put "Friend keyword 1" and "Friends of one day".'
        }

        #データ読み込み
        data = db.get(db.Key.from_path('Database', 'endo'))

        #データがある場合のみ実行する
        if data:
            #friendKey1とnumOnedayFがある場合のみ実行する
            if not (data.friendKey1 == '' or data.numOnedayF == ''):
                #キーワードを読み込む
                fk1 = data.friendKey1
                fk2 = data.friendKey2
                fk3 = data.friendKey3

                #キーワードフラグを読み込む
                flag = data.friendKeyFlag
                if flag == '':
                    flag = 1

                #次のフラグと今回のキーワードを設定する
                if not fk2:
                    nextFlag = 1
                    keyword = fk1
                else:
                    if flag == 1:
                        nextFlag = 2
                        keyword = fk1
                    else:
                        if not fk3:
                            nextFlag = 1
                            keyword = fk2
                        else:
                            if flag == 2:
                                nextFlag = 3
                                keyword = fk2
                            else:
                                nextFlag = 1
                                keyword = fk3

                #最大フォロー件数を算出する
                maxFollowCnt = round(data.numOnedayF / 24 / callApiOneHour)

                #検索
                tweetList = api.search_tweets(urllib.quote(keyword.encode('utf-8')))

                #フォロー数カウンター
                followCnt = 0

                #フォロワーの取得
                followers = api.followers_ids()

                #今日の取得
                today = datetime.date.today()
                #一定期間前の日付の取得
                baseDay = datetime.timedelta(days = oneMoreFollowTime)
                #1日の取得
                oneDay = datetime.timedelta(days = 1)

                for idx in range(0,maxFollowSearchCnt):
                    #すでにフォローしてないか?
                    if not tweetList[idx].user['id'] in followers:
                        #デリート後一定期間が経っているか?(デリートしたことがあるか?)
                        deleteData = db.get(db.Key.from_path('DeleteAccountData', str(tweetList[idx].user['id'])))
                        if (deleteData and (deleteData.deleteTime < (today - baseDay))) or (not deleteData):
                            #フォローする
                            api.create_friendship(tweetList[idx].user['id'])

                            #followTimeがない場合は今日の日付を登録
                            if not data.followTime or data.followTime == 'null':
                                data.followTime = today
                            #ある場合、followTimeが1日経過していないか?
                            else:
                                if today >= data.followTime + oneDay:
                                    #経過している場合は、followリストを移動させる
                                    data.follow5day = data.follow4day
                                    data.follow4day = data.follow3day
                                    data.follow3day = data.follow2day
                                    data.follow2day = data.follow1day

                                    #経過している場合は、followタイムを上書きする
                                    data.followTime = today

                            #フォローデータをfollow1dayに追加
                            data.follow1day += [tweetList[idx].user['id']]

                            #followCntを追加する
                            followCnt += 1

                            #followCntがmaxFollowCntに達した場合終了する
                            if followCnt >= maxFollowCnt:
                                break

                #次のフラグの登録
                data.friendKeyFlag = nextFlag

                #data.putする
                data.put()

                templateValues = {
                    'msg': 'OK! ' + str(followCnt) + ' followed.',
                }

                template = jinja_environment.get_template('follow.html')
                self.response.write(template.render(templateValues))

            else:
                template = jinja_environment.get_template('follow.html')
                self.response.write(template.render(templateValues))

        else:
            template = jinja_environment.get_template('follow.html')
            self.response.write(template.render(templateValues))


#onlyFriendsの中でListに登録されておらず、規定時間内にフォローしてもいない
#アカウントのフォローを削除する
class Delete(webapp2.RequestHandler):
    def get(self):
        api = createClient()

        #データの読み込み
        data = db.get(db.Key.from_path('Database', 'endo'))

        #エラーの場合のメッセージ
        templateValues = {
            'errmsg': 'ERROR!!! you must put "Deletes of one day" and "Time of delete".'
        }

        #データがある場合のみ実行
        if data:
            #1日当たり削除件数と削除までの規定日数が登録されているか場合のみ実行
            if not (data.numOnedayD == '' or data.deleteTime == ''):
                #最大削除数(規定値)の算出
                maxDeleteCnt = round(data.numOnedayD / 24 / callApiOneHour)

                #onlyFriendsのidリストの取得
                onlyFriends = getOnlyFriendsList(api)

                #onlyFriendsからprotectsメンバーを削除
                onlyFriendsSet =  set(onlyFriends)
                protectsSet = set(data.protects)
                nonProtectsSet =  onlyFriendsSet.difference(protectsSet)

                #削除規定日を取得して、規定日に応じて日別フォローリストを読み込み結合する
                if data.deleteTime == 1:
                    followList = data.follow1day
                elif data.deleteTime == 2:
                    followList = data.follow1day + data.follow2day
                elif data.deleteTime == 3:
                    followList = data.follow1day + data.follow2day + data.follow3day
                elif data.deleteTime == 4:
                    followList = data.follow1day + data.follow2day + data.follow3day + data.follow4day
                else:
                    followList = data.follow1day + data.follow2day + data.follow3day + data.follow4day + data.follow5day

                #nonProtectsSetから規定日内フォローリストを削除
                canDeleteList = list(nonProtectsSet.difference(set(followList)))

                #今日の取得
                today = datetime.date.today()

                deleteCnt = 0
                for idx in range(0,maxDeleteSearchCnt):
                    #Listメンバーか否かを確認
                    user = api.is_list_member(yourScreenName,'hoge',canDeleteList[idx])

                    #リスト登録されていない場合
                    if not user:
                        #削除
                        api.destroy_friendship(canDeleteList[idx])
                        #DeleteDataにdeleteTimeを登録
                        deleteData = DeleteAccountData(key_name = str(canDeleteList[idx]))
                        deleteData.deleteTime = today
                        deleteData.put()
                        #削除カウント+1
                        deleteCnt += 1
                        if deleteCnt >= maxDeleteCnt:
                            break

                    #リスト登録されているのでprotectに登録する
                    else:
                        #protectsへのuserIDの追加
                        data.protects += [canDeleteList[idx]]

                #データ登録
                data.put()

                templateValues = {
                    'msg': 'OK! ' + str(deleteCnt) + ' deleted.',
                }
                template = jinja_environment.get_template('delete.html')
                self.response.write(template.render(templateValues))
            else:
                template = jinja_environment.get_template('delete.html')
                self.response.write(template.render(templateValues))
        else:
            template = jinja_environment.get_template('delete.html')
            self.response.write(template.render(templateValues))

def countLoveFriendFollower(api):
    followers = set(api.followers_ids()) #get follower
    friends = set(api.friends_ids()) #get friends

    loves = followers.intersection(friends) #friend &amp;&amp; follower
    onlyFriends = friends.difference(followers) #only friend
    onlyFollowers = followers.difference(friends) #only follower

    return [len(loves),len(onlyFriends),len(onlyFollowers)]

def createClient():
    auth = tweepy2.OAuthHandler(CK, CS)
    auth.set_access_token(AT, ST)
    return tweepy2.API(auth_handler=auth)

#onlyFriendsのidリストを抽出しソートする
def getOnlyFriendsList(api):
    followers = set(api.followers_ids())
    friends = set(api.friends_ids())
    onlyFriends = list(friends.difference(followers))
    onlyFriends.sort()
    return onlyFriends

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/delete',Delete),
    ('/follow',Follow),
    ('/test',Test),
], debug=True)