INFRA

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)