Cómo extraer datos de Instagram usando técnicas de Web scrapping

En este post hablaremos de cómo automatizar la recogida de datos de Instagram. Aplicaremos técnicas de Web Scrapping implementadas con Python para extraer imágenes, comentarios y likes de Instagram buscando por un hashtag.

Se trata por tanto de un artículo técnico, para el que se debe tener nociones básicas de HTML y programación en Python.

Cuando estamos entrenando un algoritmo de Machine Learning o visualizando un problema muchas veces vemos que nos hace falta más información para completar o enriquecer el análisis. Es aquí donde entra en juego las técnicas de recogida de datos de fuentes externas, usando metodologías como el Web Scrapping.

¿Qué es el Web Scrapping?

Antes de comenzar vamos a introducir brevemente este concepto que hemos utilizado en la introducción. Se trata de un tecnicismo que anglosajón cuya traducción literal es «escarbado o raspado web».

Es decir, son técnicas que automatizan la extracción de información de las páginas web. Básicamente se trata de programas, o scripts, que simulan el comportamiento humano navegando por la web.

Por tanto, mediante estas técnicas programamos los pasos que seguiríamos nosotros en la web, de forma que se pueda ejecutar por una máquina mucho más rápido.

Librerías de Web Scrapping

Como hemos comentado usaremos como lenguaje base Python. Además usaremos librerías que nos facilitan la implementación de estas técnicas de Web Scrapping:

  • Selenium: librería que facilita el uso de un Webdriver, el cuál se utiliza para simular acciones de un ser humano en una web, tales como un click.
  • Chromedriver: herramienta de código abierto que proporciona la ejecución de las funciones sobre el navegador Google Chrome. Será necesario descargarnos un ejecutable y guardarlo en una carpeta al que apuntaremos en nuestro programa Python. En este link podéis descargaros dicho ejecutable. Usaremos una variable de entorno para definir ese path, por ejemplo:
chromedriver_path = '/Users/Ignacio/documents/chromedriver'

IMPORTANTE: deberemos tener instalado Google Chrome para que se pueda ejecutar todo correctamente.

Ahora ya sí copio el import de las librerías necesarias:

#Importing all needed libraries
from selenium import webdriver
import time
import os
import time
import requests
from pprint import pprint
import pandas as pd
import json
from lxml import html
import re
import csv
import numpy as np
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import (
    NoSuchElementException,
    TimeoutException,
    WebDriverException,
    )
import pandas as pd

Funciones

En esta sección vamos a detallar las funciones implementadas para la extracción de la información. En todo momento vamos a simular el comportamiento o la interacción de un humano de forma automátizada.

Cargar Instagram

Esta función carga la página de Instagram. Nada más cargar la página nos salta un pop-up preguntándonos si aceptamos las cookies o no, tal y cómo se muestra en la siguiente imagen.

Implementamos o simulamos el click en aceptar con código:

 # Accept cookies
    cookies = WebDriverWait(browser, WAIT_TIME_3).until(
        EC.element_to_be_clickable((By.XPATH,'//button[contains(text(), "Aceptar")]'))).click()

Y esta es la función completa con el código que acepta las cookies incluído:

def load_instagram():
    """
    Function to initialize Instagram and launch it in a browser using Selenium
    """
    # Chrome driver should be in
    executable_path=os.path.join(chromedriver_path)
    options = webdriver.ChromeOptions()
    options.add_argument('--ignore-certificate-errors')
    options.add_argument('--disable-notifications')
    # 1-Allow, 2-Block, 0-default
    preferences = {
        "profile.default_content_setting_values.notifications" : 2,
        "profile.default_content_setting_values.location": 2,
        # We don't need images, only the URLs.
        "profile.managed_default_content_settings.images": 2,
        }
    options.add_experimental_option("prefs", preferences)

    browser = webdriver.Chrome(
        executable_path=executable_path,
        chrome_options=options,
        )
    browser.wait = WebDriverWait(browser, WAIT_TIME)   
    
    #Opening the browser and getting the url
    url = "https://www.instagram.com/"
    browser.get(url)
    
    #wait 5 seconds to load
    time.sleep(WAIT_TIME_2)
    
    # Accept cookies
    cookies = WebDriverWait(browser, WAIT_TIME_3).until(
        EC.element_to_be_clickable((By.XPATH,'//button[contains(text(), "Aceptar")]'))).click()
    
    return browser

Inicio de sesión

Una vez aceptamos las cookies nos sale la página de inicio de sesión donde deberemos introducir nuestras credenciales.

Para ello hemos implementado la siguiente función:

def instagram_login(driver):
    """
    Login to Instagram using username and password.
    """
    usr = driver.find_element_by_name("username")
    usr.send_keys(USERNAME)
    password = driver.find_element_by_name("password")
    password.send_keys(PASSWORD)
    password.send_keys(Keys.RETURN)
    time.sleep(WAIT_TIME_2)

Donde USERNAME y PASSWORD son variables genéricas definidas anteriormente. Debemos especificar nuestro usuario y contraseña en formato string. Un ejemplo:

PASSWORD = '123456'
USERNAME = 'ejemplo@gmail.com'

Búsqueda de información

El objetivo principal es recoger toda la información asociada a un hastag, es decir, imágenes y comentarios, y almacenarlo en local.

Por tanto, para la búsqueda por hashtag deberemos introducirlo en la caja de búsqueda una vez hayamos iniciado sesión. Esta se encuentra en la parte superior:

Como se puede apreciar, aparece un texto Busca en dicha caja. Para saber como encontrarlo es necesario inspeccionar el código fuente HTML de la página. Si hacemos click derecho sobre Busca->Inspeccionar, nos aparecerá el siguiente recuadro lateral:

Se puede apreciar como la etiqueta input contiene un atributo placeholder=»Busca». Esto lo usaremos para encontrar con código dicha caja de búsqueda, implementándolo de la siguiente forma:

 # Get the search box
    searchbox = WebDriverWait(browser,WAIT_TIME).until(
        EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Busca']")))
    
    searchbox.clear()

Una vez estamos situados y hemos limpiado el texto, podemos proceder a escribir el hashtag objetivo, que lo denominamos keyword:

   # Search by tag
    searchbox.send_keys(keyword)
    time.sleep(WAIT_TIME_3)
    searchbox.send_keys(Keys.ENTER)
    time.sleep(WAIT_TIME_3)
    searchbox.send_keys(Keys.ENTER)
    time.sleep(WAIT_TIME_3)

NOTA: se ha implementado dos veces el click sobre enter ya que la primera vez posiciona el cursor en la primera lista del desplegable y el segundo ejecuta la búsqueda en sí.

Imágenes

Una vez procedemos a buscar, nos aparecen numerosas imágenes como se muestra en la siguiente figura. Además hemos pulsado click derecho->inspeccionar sobre una imagen para analizar los atributos.

Se puede apreciar como la etiqueta <img> tiene un atributo llamado src, que contiene la url de la imagen en cuestión. Por tanto con el siguiente código recuperamos todas las url de las imágenes que aparecen en pantalla.

images = browser.find_elements_by_tag_name('img')
images = [image.get_attribute('src') for image in images]
images = images[1:-2] #slicing-off first photo, IG logo and Profile picture

Número de likes

Una vez disponemos de todas las urls de las imágenes, tenemos que ir pinchando foto a foto para que aparezcan el número de likes. Si pinchamos en una imagen cualquiera se abre la siguiente ventana:

Para ejecutar cada click con código usaremos el siguiente código, donde image será cada url:

# Click and open posts 
browser.execute_script("arguments[0].click();",
                        browser.find_element_by_xpath('//img[@src="'+str(image)+'"]'))
time.sleep(WAIT_TIME_5)

Al igual que en el caso anterior, vamos a inspeccionar para ver que etiquetas podemos usar como referencia en el scrapping:

Vemos como el caso más habitual se trata de una etiqueta <div> con clase Nm9Fw con una etiqueta <span> por debajo. Por tanto usaremos dicha clase de referencia mediante llamadas con estilo CSS:

try:
        el_likes = browser.find_element_by_css_selector(".Nm9Fw > * > span").text
                      
except Exception as e:
        try:
            el_likes = browser.find_element_by_css_selector(".Nm9Fw > button").text
        
        except Exception as e2:
            try:
                el_likes = browser.find_element_by_css_selector(".vcOH2").text
            except Exception as e3:
                print(f"ERROR - Could not fetch like  {e3}")  

NOTA: hemos comprobado que en algunos casos en lugar de etiqueta span, aparece un button. También en casos muy puntuales hemos visto que los likes aparecen directamente en una etiqueta de clase vcOH2.

Una vez extraídos los likes, es posible que la imagen no tenga ningún like, o que en lugar de imagen sea un vídeo y aparezcan número de reproducciones. Por tanto, es necesario una limpieza de datos ex-post para quedarnos únicamente con el número:

# Transform the text when there are no Likes
if el_likes == "indicar que te gusta esto":
        el_likes = '0'
        
# Clean the info to only retrieve numbers instead of text    
if "Me gusta" in str(el_likes) or "reprodu" in str(el_likes):
        el_likes = el_likes[:1]
        

Comentarios

Al igual que con los likes es necesario pulsar en cada imagen usando el siguiente código:

# Click and open posts 
browser.execute_script("arguments[0].click();",
                        browser.find_element_by_xpath('//img[@src="'+str(image)+'"]'))
time.sleep(WAIT_TIME_5)

Inspeccionamos la imagen:

Vemos que sobre la clase C4VMK cuelgan diferentes etiquetas <span > con los comentarios. Por tanto usamos el siguiente código que nos devuelve una lista con todos los comentarios:

try:
        comment_elements = browser.find_elements_by_css_selector(".eo2As .gElp9 .C4VMK")
        comment = [element.find_elements_by_tag_name('span')[1].text for element in comment_elements]
      
except Exception as e:  
        print(f"ERROR - Could not fetch comment  {e}") 
        

Procesamiento de la información

En este punto ya hemos obtenido todos los likes, urls de las imágenes y comentarios. Por tanto, estamos en disposición de procesar la información para almacenarla en local.

Básicamente vamos a estructurar la información en un Pandas Dataframe, de forma que el Título del post sea el primer comentario. Además separamos los hashtags principales en otra columna. En la siguiente imagen, mostramos lo que hemos considerado como hashtags principales:

La función implementada para llevar a cabo todo este procesado es la siguiente:

def process_info(insta_info):
    """
    Function that process the info retrieved from posts
    
    """
    
    df = pd.DataFrame(insta_info)
    
    # The first comment is considered as a title
    df["Title"]    = [i[0] for i in list(df["Comments"])] 
    df["Comments"] = [i[1:] for i in list(df["Comments"])]
    
    # Get all the hashtags from the title
    df["Principal Hashtags"] = [ re.findall("#(\w+)", title)  for title in list(df["Title"])]
    
    return df

El dataframe resultado final tiene la siguiente forma:

Descarga de la información

Finalmente vamos a proceder a descargar en local toda la información obtenida. Por un lado las imágenes en formato .jpg y por otro el dataframe con comentarios likes, etc anterior en formato excel.

df["Download name"] = download_images(keyword,list(df["Image URL"]))
df.to_csv(str(keyword)+'.csv',index=False, header=True)

La función download_images es la siguiente:

def download_images(keyword,images):
    """
    Function used to download  images
    """
    fpath = os.getcwd()
    fpath = os.path.join(fpath, keyword[1:])
  
    if(not os.path.isdir(fpath)):
        os.mkdir(fpath)
    
    #download images
    counter = 0
    image_name_lst = []
    for image in images:
        persist_image(fpath,image,counter,image_name_lst)
        counter += 1
    return image_name_lst
def persist_image(folder_path:str,url:str, counter,image_name_lst):
    """
    Function used to persist physically in localhost an image from an url
    """
    try:
        image_content = requests.get(url).content
    except Exception as e:
        print(f"ERROR - Could not download {url} - {e}")
    try:
        img_name =  'jpg' + "_" + str(counter) + ".jpg"
        f = open(os.path.join(folder_path,img_name), 'wb')
        f.write(image_content)
        f.close()
        print(f"SUCCESS - saved {url} - as {folder_path}")
        image_name_lst.append(img_name)
    except Exception as e:
        print(f"ERROR - Could not save {url} - {e}")

Espero que os haya gustado! Si tenéis cualquier duda o necesitáis alguna explicación del código más en detalle, no dudéis en poner un comentario!