A friend recently proposed the idea of creating a script that would inform someone if they were followed/un-followed on instagram – I guess this.

The result was a daily update email:

followers

I decided to use an sqlite database to store a retrieved list of followers and report the differences between the two lists. For example here is a sample run to test it:

run_test(capture_followers = ['bob', 'tony', 'amy'])
run_test(capture_followers = ['bob', 'amy', 'rachel'])
run_test(capture_followers = ['bob', 'tony', 'jo', 'bigd'])

The output is:

ANALYSING
User: bob has started to follow you
User: tony has started to follow you
User: amy has started to follow you
ANALYSING
User: tony has decided to unfollow you
User: rachel has started to follow you
ANALYSING
User: amy has decided to unfollow you
User: rachel has decided to unfollow you
User: tony has started to follow you
User: jo has started to follow you
User: bigd has started to follow you

All I needed to do then was to set-up a task using task-scheduler to execute my Python code every day:

Capture

Capture

Thanks to help from stackoverflow I rewrote my get_followers function to return a generator rather than a list of values which:

also has the advantage of separating data acquisition and handling logic

The best explanation that I found on this was this post:

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

def solve_number_10():
    # She *is* working on Project Euler #10, I knew it!
    total = 2
    for next_prime in get_primes(3):
        if next_prime < 2000000:
            total += next_prime
        else:
            print(total)
            return

Which presents a visualisation of the flow of operation if one wants to deal with large/infinite sequences:

It’s helpful to visualize how the first few elements are created when we call get_primes in solve_number_10’s for loop. When the for loop requests the first value from get_primes, we enter get_primes as we would in a normal function.

We enter the while loop on line 3
The if condition holds (3 is prime)
We yield the value 3 and control to solve_number_10.
Then, back in solve_number_10:

The value 3 is passed back to the for loop
The for loop assigns next_prime to this value
next_prime is added to total
The for loop requests the next element from get_primes
This time, though, instead of entering get_primes back at the top, we resume at line 5, where we left off.

Most importantly, number still has the same value it did when we called yield (i.e. 3). Remember, yield both passes a value to whoever called next(), and saves the “state” of the generator function. Clearly, then, number is incremented to 4, we hit the top of the while loop, and keep incrementing number until we hit the next prime number (5). Again we yield the value of number to the for loop in solve_number_10. This cycle continues until the for loop stops (at the first prime greater than 2,000,000).

Python Script:

import sqlite3
import requests
import json
import time
import smtplib
from email.mime.text import MIMEText


class Indexer(object):
    """Initialise an Indexer Class which will handle all the communication with the sqlite 3 database. It will replace
    the old list of followers in a table with the current list and return the changes"""

    def __init__(self, db, user):
        self.con = sqlite3.connect(db)
        self.tblname = 'followers_%s' % user

    def __del__(self):
        self.con.close()

    def db_commmit(self):
        self.con.commit()

    def is_indexed(self, follower_name):
        """ Check if a user exists already in a table """
        follower = self.con.execute("select rowid from %s where user='%s'" % (self.tblname, follower_name)).fetchone()
        return follower != None

    def create_tables(self):
        """ Create table provided it doesn't already exist """
        if not self.check_table_exists():
            self.con.execute("create table %s(user)" % self.tblname)
            self.db_commmit()

    def add_to_index(self, follower):
        """ Add user to database (if he doesn't exist there already) """
        if self.is_indexed(follower):
            return
        self.con.execute("insert into %s(user) values ('%s')" % (self.tblname, follower))
        self.db_commmit()
        return True

    def remove_from_index(self, follower):
        """ Remove user from table """
        self.con.execute("delete from %s where user='%s'" % (self.tblname, follower))
        self.db_commmit()

    def retrieve_saved(self):
        """ Retrieve stored followers and return as a list """
        followers = self.con.execute("select user from %s" % self.tblname).fetchall()
        return [i[0] for i in followers]

    def check_table_exists(self):
        """ Check if a table exists already in the database and if so return: True """
        try:
            self.con.execute("select count(*) from %s" % self.tblname).fetchone()
            return True
        except sqlite3.OperationalError:
            return False


class followers(object):
    """ Initialise an object to hold the followers captured from instagram API """

    def __init__(self):
        self.followers = []

    def add_follower(self, follower):
        self.followers.append(follower)


def run_analysis(capture_followers,
                 user):
    """
    Initialise the database using Indexer() and then retrieve the snapshot in the database and compare
    against what has been currently captured from instagram to find users who have un-followed (and remove them).
    Then check which new users have appeared and add those in. Return a list of those lost and gained.
    """
    print("ANALYSING")
    indx = Indexer("C:/Users/ikarmanov/PycharmProjects/Insta_Followers/Instagram_Followers.db",
                   user)
    indx.create_tables()
    # Loss
    loss = []
    previous_followers = indx.retrieve_saved()
    for pf in previous_followers:
        if pf not in capture_followers:
            indx.remove_from_index(pf)
            loss.append(pf)
    # Gain
    gain = []
    for cf in capture_followers:
        if indx.add_to_index(cf):
            gain.append(cf)
    # Results
    return [loss, gain]


def get_user_id(username,
                access_token):
    """ Get top matching userid for a given username from instagram """
    response = requests.get(
        'https://api.instagram.com/v1/users/search?q=%s&access_token=%s' % (username,access_token)
    ).text
    response = json.loads(response)
    # Return first-user (best match)
    return response['data'][0]['id']


def get_followers(user_id,
                  access_token,
                  cursor=''):
    """ Create a generator which will be accessed later to get the usernames of the followers"""
    while cursor is not None:
        # fetch data
        url = 'https://api.instagram.com/v1/users/%s/followed-by?access_token=%s&cursor=%s' % (my_user,
                                                                                               access_token,
                                                                                               cursor)
        print(url)
        response = requests.get(url)
        json_out = json.loads(response.text)
        for user in json_out['data']:
            yield user

        # fetch new token
        try:
            cursor = json_out['pagination']['next_url']
        except KeyError:
            print("Finished with %d followers" % len(my_followers.followers))
            cursor = None

if __name__ == '__main__':

    access_token = "secret"
    username = "ilia.uk@gmail.com"
    password = "secret"
    user = 'ilia.uk'      # Searching for

    # Initialise
    my_followers = followers()
    my_user = get_user_id(user, access_token)

    # Retrieve followers
    followers_gen = get_followers(my_user, access_token)  # Get followers
    for usr in followers_gen:
        usrname = usr['username']
        my_followers.add_follower(usrname)
        print("Added: %s" % usrname)
    # Analyse difference to snapshot
    loss, gain = run_analysis(capture_followers=my_followers.followers, user=user)  # Compare to database

    # Create summary message
    msg_l = []
    if len(loss):
        msg_l.append("LOST FOLLOWERS:\n%s" % '\n'.join(loss))
    if len(gain):
        msg_l.append("GAINED FOLLOWERS:\n%s" % '\n'.join(gain))

    if msg_l:
        # There is some change so prepare an email to send-out:
        msg_text = "\n\n".join(msg_l)
        print(msg_text)
        msg = MIMEText(msg_text)
        msg['Subject'] = "Instagram Followers Daily Update"

        # Send Email:
        for em in ["ilia.uk@gmail.com"]:
            msg_to = em
            msg['From'] = "Ilia"
            msg['To'] = msg_to

            s = smtplib.SMTP('smtp.gmail.com:587')
            s.ehlo()
            s.starttls()
            s.login(username, password)
            s.sendmail("Ilia", [msg_to], msg.as_string())
            print("Message Sent")
            s.quit()
    else:
        print("No Change Today!")