タイトルの通りですが、followersとfollowingを揃えるスクリプトです。
followersとfollowingを取得して、followingに存在するけどfollowersに存在しないユーザーを取得して、削除するかどうかを決めます。削除しないユーザーは削除しますか?と問われた時にNoと答えると、Safe listに追加されて、以後削除対象からはずれます。
元々はcronで定期的に起動して、タイムラインに見える人は全員話しかけてOK状態を自動的にキープする事が目的だったのですが(だから引数にYを付けて起動すると自動的に削除してくれたりする)、Twitter APIのエラーが多すぎるためなるべくエラーが発生しても処理を続けられる様にリトライしたり、ブロッキングするような変更が加わっています。なので、タイマーを使った定期実行は辞めた方がいいです。ずっとリトライしたりブロッキングされてプロセスが増え続けると困りますよね。
というように、色々と完成度は低いので使う場合は(このスクリプトに限りませんが)自己責任で使ってください。あと、friendを削除する処理があるので、スクリプトの趣旨を良く理解して気をつけて使ってください。誤動作しても一切責任持てませんし、穿ちすぎな話ですが、時としてTwitter APIが正しいデータを返さないかもしれませんから。
あと、これを使ってもfollowingとfollowersの数字は必ずしも一致しません。というか、一致した事がありません。なんでかはわかりません。
あと、API制限の関係でfollowerとかが多いと制限に引っかかって処理しきれません。一時間当たり20回制限とかかかってる時は私程度の friend 数でも無理だったりする事があります。
idとパスワードは直接スクリプトに書いてますので、変更してください。マルチユーザー環境の人はパーミッションは0700とか0600(としてrubyの引数に指定して使う)にしましょう。以上です。
あー、あとruby-jsonに依存してます。
#!/usr/bin/env ruby -Ku
require 'rubygems'
require 'json'
require 'open-uri'
Dir.chdir(File.dirname(File.expand_path(__FILE__)))
module Twitter
ID = 'your id'
PASSWORD = 'your password'
SHOW = 'http://twitter.com/users/show/%s.json'
FRIENDS = 'http://twitter.com/statuses/friends.json'
FOLLOWERS = 'http://twitter.com/statuses/followers.json'
DESTROY = 'http://twitter.com/friendships/destroy/%s.json'
end
module Twitter
class Manip
attr_reader :followers, :friends
def initialize(id, password)
@auth_info = [id, password]
@followers = []
@friends = []
end
def get_show(id)
get(Twitter::SHOW % id) do |f|
JSON.parse(f.read)
end
end
def get_friends(page=1, lite=false)
user_data(Twitter::FRIENDS, page, lite) do |friend|
@friends << friend['screen_name']
end
end
def get_followers(page=1, lite=false)
user_data(Twitter::FOLLOWERS, page, lite) do |follower|
@followers << follower['screen_name']
end
end
def friend_destroy(friend_id)
entry = Twitter::DESTROY % friend_id
get(entry) do |f|
JSON.parse(f.read)
end
end
def raise_bar(total, count)
begin
sleep 1
yield
#tc.get_followers(count, true)
print '.'
rescue StandardError => e
puts e.message()
puts 'リトライしますか?'
if gets.upcase.chomp == 'Y'
puts '.' * total
print '.' * count
retry
else
puts '断念!'
exit(1);
end
end
end
private
def user_data(entry, page=1, lite=false)
param = [entry, page]
param << lite.to_s if lite
tmpl = lite ? '%s?page=%d&lite=%s' : '%s?page=%d'
entry = tmpl % param
get(entry) do |f|
JSON.parse(f.read).each do |user|
yield(user)
end
end
end
def get(entry)
begin
open(entry, {:http_basic_authentication => @auth_info}) do |f|
yield(f) if f.status == ['200', 'OK']
end
rescue Timeout::Error => e
puts 'タイムアウト: 10秒間隔を置いてリトライします。'
sleep 10
retry
end
end
end
class SafeList
attr_reader :list
def initialize(path="safe_list.txt")
@path = path
@list = []
end
def load
begin
File.open(@path) do |f|
f.each_line do |line|
@list << line.chomp
end
end
rescue Errno::ENOENT
end
end
def save
File.open(@path, 'w') do |f|
@list.uniq!
@list.each do |line|
f.write line + "\n"
end
end
end
def add(str)
@list << str
@list.uniq!
end
def del(str)
@list.delete(str)
end
end
end
if $0 == __FILE__
begin
tc = Twitter::Manip.new(Twitter::ID, Twitter::PASSWORD)
user = tc.get_show(Twitter::ID)
followers_count = user['followers_count'] / 100 + 1
puts '取得中: followers'
puts '.' * followers_count
STDOUT.sync = true
1.upto(followers_count) do |count|
tc.raise_bar(followers_count, count) do
tc.get_followers(count, true)
end
end
STDOUT.sync = false
friends_count = user['friends_count'] / 100 + 1
puts
puts '取得中: friends'
puts '.' * friends_count
STDOUT.sync = true
1.upto(friends_count) do |count|
tc.raise_bar(friends_count, count) do
tc.get_friends(count, true)
end
end
STDOUT.sync = false
puts
p tc.followers - tc.friends #片想われ
destroy = tc.friends - tc.followers #片想い
sl = Twitter::SafeList.new
sl.load
sl.list.each do |safe_id|
destroy.delete(safe_id)
end
puts "削除対象: %d" % destroy.size
unless destroy.size.zero?
p destroy
destroy.each do |id|
begin
puts "#{id} を削除しますか? [Yn]"
ins = ARGV.first.nil? ? gets.upcase.chomp : ARGV.first.upcase
if ins == "Y"
puts "#{id} を削除します。"
result = tc.friend_destroy(id)
puts result['screen_name'] == id ? "destroy [%s]: Success" % id : "destroy [%s]: Fail" % id
elsif ins == "N"
sl.add(id)
puts "#{id} をセーフリストに加えました。"
else
#pass
end
rescue StandardError
puts 'エラー: リトライ中'
sleep(10)
retry
end
end
sl.save
end
puts '完了。'
rescue OpenURI::HTTPError => e
if e.message =~ /400 Bad Request/
puts 'API制限に引っかかりました。'
end
puts e.message()
e.backtrace.each do |trace|
puts "* #{trace}"
end
end
end