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')

Lo primero que tenemos que sortear es el acceso usando una cuenta válida de Instagram. La creamos e introducimos estos datos en `private_settings.py`:

    #!python
    # private_settings.py
    INSTAGRAM_USER = ''
    INSTAGRAM_PASSWORD = ''

Una vez hemos accedido a la home de Instagram, introducimos los datos y hacemos login:

    #!python
    # Login
    try:
        username_input = browser.find_element_by_css_selector("input[name='username']")
        password_input = browser.find_element_by_css_selector("input[name='password']")
        username_input.send_keys(settings.INSTAGRAM_USER)
        password_input.send_keys(settings.INSTAGRAM_PASSWORD)
        login_button = browser.find_element_by_xpath("//button[@type='submit']")
        login_button.click()
    except:
        print("Already logged-in")

Para ello usamos los selectores de `selenium`, `browser.find_element_by_css_selector`, que sería un análogo a un selector tipo `querySelector`, y `find_element_by_xpath`, que busca elementos descendiendo por el árbol de un nodo y donde '/' equivaldría a bajar un nivel y '//' equivaldría a bajar un número arbitrario de niveles, de modo que `browser.find_element_by_xpath("//button[@type='submit']")` sería equivalente a decir búscame en cualquier nivel de la DOM el primer botón de tipo `submit`.
Una vez sorteado el login tenemos que hacer dismiss sobre los modales habituales con los que Instagram tortura a sus usuarias:

    #!python
    # If notifications modal click on dismiss
    try:
        notifications_button = browser.find_element_by_xpath("//button[contains(text(), 'Not Now')]")
        notifications_button.click()
    except:
        print("Notifications modal not dismissed")

Aquí el selector es algo más complejo porque Instagram como cualquier red social grande está muy protegida contra el escrapeo, para lo que usa una DOM ultra-hermética llena de clases e IDs generados al azar. La única manera es seleccionar el botón por su contenido de texto, para lo que usamos la función `contains`.

Una vez superadas ambos escollos lo que hacemos es parsear la DOM para obtener una lista con las urls de los posts más recientes:

    #!python

    # Visit hashtag
    browser.get('https://www.instagram.com/explore/tags/%s/' % hashtag)
    # Get posts
    most_recent_anchor = "//h2[contains(text(), 'Most recent')]"
    most_recent_links_anchor = most_recent_anchor + "/following-sibling::div//a"
    links = browser.find_elements_by_xpath(most_recent_links_anchor)
    urls = [ l.get_attribute('href') for l in links ]
    print("List of posts: ", urls)
    if limit:
        urls = urls[:limit]

Para ello usamos ahora el selector `/following-sibling::div`, que nos permite seleccionar a un sibling/hermano en la DOM, en este caso del H2 que encabeza la sección 'Most recent'. Una vez seleccionado dicho contenedor simplemente obtenemos todas las URLs que hay dentro, pillando sus elementos `a` y haciendo una lista por compresión de django con los contenidos de sus atributos `href`. Si hemos explicitado un límite lo aplicamos a dicha lista.

Ya por último vamos navegando por esa lista y vamos obteniendo las URLs de las imágenes asociadas a cada post y cerramos.

    #!python
    for url in urls:
        browser.get(url)
        image_anchor = '//article/header/following-sibling::div/following-sibling::div//img'
        image_src = browser.find_element_by_xpath(image_anchor).get_attribute('src')
        print(image_src)
    browser.close()