Esta es una necesidad del proyecto Mapa de Afectos, de Zemos98 y Jartura. Se trataría de poder leer posts de Instagram con un determinado hashtag e importar automáticamente dichos contenidos a un Django.

## Funcionamiento básico

A grandes rasgos la manera de hacer esto es mediante comandos de gestión de django que usen Selenium para hacer la interacción y que sean lanzados por un script de cron cada x tiempo, por ejemplo 10 minutos. Si se quiere hacer un poco más fancy en cliente, se puede hacer un script de long polling en JS que actualice el frontend mágicamente cada vez que se produzca una importación.

## Instalar selenium

`Selenium` es un framework de automatización web. Se instala como cualquier otro paquete de python:

    #!python
    . ../env/bin/activate
    pip install selenium

Tiene como dependencia los motores de navegación que se quieran emplear. Vamos a emplear por ejemplo `firefox/gecko`. Para ello visitamos https://github.com/mozilla/geckodriver/releases y descargamos la última versión. Le damos permisos de ejecución y la movemos a cualquier ubicación en el PATH de sistema:

    #!bash
    cd ~/Descargas
    tar -xvzf geckodriver-v0.27.0-linux64.tar.gz
    sudo chmod +x geckodriver
    sudo mv /usr/bin/geckodriver


## Crear un comando de gestión de django

Los comandos de gestión de django se albergan en apps que han de estar en la lista de `INSTALLED_APPS` de settings.py. Creamos por ejemplo una app `connectors` y dentro de ella creamos una carpeta `management` con un `__init__.py` y otra carpeta dentro, `commands`:

    #!bash
    . ../env/bin/activate
    cd apps
    ../manage.py startapp connectors
    mkdir -p connectors/management/commands && touch connectors/management/__init__.py

Dentro de esta creamos el comando en cuestión, por ejemplo `instagram_import.py`. Un comando es un objeto de la clase `Command` y tiene que tener un método `handle`, que es la secuencia lineal que ejecuta el mismo. También le podemos implementar un método `add_arguments`, de modo que pueda aceptar parámetros de ejecución. Le añadimos un par de ellos, que nos permitan especificar un hashtag y un número máximo de posts a ser importados:

    #!python
    from selenium import webdriver
    # django
    from django.core.management.base import (
        BaseCommand,
        CommandError
    )
    # project
    from django.conf import settings

    """
    A manage.py command to read posts from a Instagram hashtag
    """
    class Command(BaseCommand):

        """
        Adds the path to project resource
        """
        def add_arguments(self, parser):
            parser.add_argument(
                '--hashtag',
                type=str,
                help='A valid hashtag',
            )
            parser.add_argument(
                '--limit',
                type=int,
                help='Maximum number of posts to be imported',
            )

        """
        Imports posts from Instagram
        """
        def handle(self, *args, **options): 
            pass

Ahora es cuestión de definir el comando que _escrapea_ Instagram. Para ello usamos selenium, especificando que lo corremos correr de manera _headless_, es decir, sin interfaz gráfica:

    #!python
    def handle(self, *args, **options): 
            hashtag = options['hashtag']
            limit   = options['limit']

            if hashtag:
                try:
                    options = webdriver.FirefoxOptions()
                    options.set_headless()
                    browser = webdriver.Firefox(firefox_options=options)
                    browser.get('https://www.instagram.com/')
                except:
                    print("There was a problem connecting to instagram in headless mode. Abort.")
                    browser.close()
            else:
                raise CommandError('You have to provide a valid hashtag')

Una vez nos podemos conectar lo que vamos a hacer es leer el grafo de GraphQL asociado al hashtag, de manera que no necesitaremos un usuario registrado (ideal para que no nos bloqueen inadvertidamente la cuenta en algún momento y nos bloqueen el invento). Sólo tenemos que leer la URL del hashtag a la que añadiremos un parámtro GET para que nos devuelva el JSON asociado y lo parsearemos para crear un objeto `post` por cada nodo contenido en dicho grafo.

    #!python
    browser.get('https://www.instagram.com/explore/tags/%s/?__a=1' % hashtag)
    data = browser.find_element_by_xpath("//div[@id='json']").text
    posts_data = json.loads(data)['graphql']['hashtag']['edge_hashtag_to_media']['edges']
    if limit:
        posts_data = posts_data[:limit]
    for post_data in posts_data:
        item = post_data['node']
        post = {
            'id'      : item['id'],
            'slug'    : item['shortcode'],
            'img'     : item['thumbnail_src'],
            'caption' : item['edge_media_to_caption']['edges'][0]['node']['text'],
            'date'    : item['taken_at_timestamp'],
        }

La única dificultad es extraer la ubicación, para lo que necesitaremos un poco de regex. Primero extraeremos de la `accessibility caption`, el bloque que traduce a texto la imagen (comienza por «Image may contain...») y después comprobaremos si la imagen etiqueta a gente, en cuyo caso eliminaremos también dicho bloque (empieza por « with...»):

    #!python
    if item['accessibility_caption']:
        caption = re.search(
            re.compile( r'(.+) in (.+). Image may' ),
            item['accessibility_caption']
        )
        if caption:
            place = caption.group(2)
            place = re.sub(' with(.+)', '', place)
            post['place'] = place

Quedaría implementar una comprobación de IDs para no crear contenido redundante.