Concurrencia vs Paralelismo: Diferencias significativas para Web Scraping

Raspando, Las diferencias, Jan-17-20225 minutos de lectura

Cuando se habla de concurrencia frente a paralelismo, puede parecer que se refieren a los mismos conceptos en ejecuciones de programas informáticos en un entorno multihilo. Bueno, después de mirar sus definiciones en el diccionario Oxford, puedes inclinarte a pensar que sí. Sin embargo, cuando se profundiza en estas nociones con respecto a

Cuando se habla de concurrencia frente a paralelismo, puede parecer que se refieren a los mismos conceptos en ejecuciones de programas informáticos en un entorno multihilo. Bueno, después de mirar sus definiciones en el diccionario Oxford, puedes inclinarte a pensar que sí. Sin embargo, cuando profundices en estas nociones con respecto a cómo la CPU ejecuta las instrucciones del programa, te darás cuenta de que concurrencia y paralelismo son dos conceptos distintos. 

Este artículo profundiza en la concurrencia y el paralelismo, cómo varían y cómo trabajan juntos para mejorar la productividad en la ejecución de programas. Por último, se discutirá qué dos estrategias son las más adecuadas para el web scraping. Así que empecemos.

¿Qué es la ejecución concurrente?

En primer lugar, para simplificar las cosas, empezaremos con la concurrencia en una única aplicación ejecutada en un único procesador. Dictionary.com define la concurrencia como una acción o esfuerzo combinado y la ocurrencia de eventos simultáneos. Sin embargo, se podría decir lo mismo de la ejecución paralela, ya que las ejecuciones coinciden, por lo que esta definición es algo engañosa en el mundo de la programación informática.

En la vida cotidiana, tendrás ejecuciones concurrentes en tu ordenador. Por ejemplo, puedes leer un artículo de blog en tu navegador mientras escuchas música en tu Windows Media Player. Habría otro proceso en ejecución: descargando un archivo PDF de otra página web-todos estos ejemplos son procesos separados.

Antes de la invención de las aplicaciones de ejecución concurrente, las CPU ejecutaban los programas secuencialmente. Esto implicaba que las instrucciones de un programa tenían que completar su ejecución antes de que la CPU pasara al siguiente.

Por el contrario, la ejecución concurrente alterna un poco de cada proceso hasta que todos están completos.

En un entorno de ejecución multihilo con un único procesador, un programa se ejecuta cuando otro está bloqueado por la entrada del usuario. Ahora te preguntarás qué es un entorno multihilo. Es una colección de hilos que se ejecutan independientemente unos de otros-más sobre hilos en la próxima sección.

La concurrencia no debe confundirse con la ejecución paralela

Ahora es más fácil confundir la concurrencia con el paralelismo. Lo que queríamos decir con concurrencia en los ejemplos anteriores es que los procesos no se ejecutan en paralelo. 

En cambio, digamos que un proceso requiere completar una operación de Entrada/Salida, entonces el Sistema Operativo asignaría la CPU a otro proceso mientras completa su operación de E/S. Este procedimiento continuaría hasta que todos los procesos completen su ejecución.

Sin embargo, como la conmutación de las tareas por parte del sistema operativo se produce en un nano o microsegundo, a un usuario le parecería que los procesos se ejecutan en paralelo, 

¿Qué es un hilo?

A diferencia de la ejecución secuencial, la CPU no puede ejecutar todo el proceso/programa a la vez con las arquitecturas actuales. En su lugar, la mayoría de los ordenadores pueden dividir todo el proceso en varios componentes ligeros que se ejecutan independientemente unos de otros en un orden arbitrario. Estos componentes ligeros se denominan hilos.

Por ejemplo, Google Docs puede tener varios subprocesos que funcionan simultáneamente. Mientras un subproceso guarda automáticamente tu trabajo, otro puede ejecutarse en segundo plano para comprobar la ortografía y la gramática.  

El sistema operativo determina el orden y qué hilos priorizar, lo cual depende del sistema.

¿Qué es la ejecución paralela?

Ahora ya conoce la ejecución de programas informáticos en un entorno con una única CPU. En cambio, los ordenadores modernos ejecutan muchos procesos simultáneamente en varias CPU, lo que se conoce como ejecución en paralelo. La mayoría de las arquitecturas actuales tienen múltiples CPU.

Como puede ver en el diagrama siguiente, la CPU ejecuta cada hilo perteneciente a un proceso de forma paralela entre sí.  

En el paralelismo, el Sistema Operativo conmuta los hilos hacia y desde la CPU en fracciones de macro o microsegundos según la arquitectura del sistema. Para que el Sistema Operativo consiga la ejecución paralela, los programadores informáticos utilizan el concepto conocido como programación paralela. En la programación paralela, los programadores desarrollan código para aprovechar al máximo las múltiples CPU. 

Cómo la concurrencia podría acelerar el web scraping

Con tantos dominios que utilizan el web scraping para extraer datos de sitios web, un inconveniente importante es el tiempo que lleva extraer grandes cantidades de datos. Si no eres un desarrollador experimentado, puedes acabar perdiendo mucho tiempo experimentando con técnicas específicas antes de ejecutar el código sin errores y a la perfección.

A continuación se exponen algunas de las razones por las que el web scraping es lento.

¿Razones importantes por las que el web scraping es lento?

En primer lugar, el scraper tiene que navegar hasta el sitio web de destino en el web scraping. A continuación, tendría que extraer y recuperar las entidades de las etiquetas HTML que desea raspar. Por último, en la mayoría de los casos, deberá guardar los datos en un archivo externo, como el formato CSV.  

Como puede ver, la mayoría de las tareas anteriores requieren una operación de E/S muy pesada, como extraer datos de sitios web y guardarlos en archivos externos. Navegar a los sitios web de destino a menudo depende de factores externos como la velocidad de la red o esperar a que una red esté disponible.

Como puede ver en la siguiente figura, este consumo de tiempo extremadamente lento puede dificultar aún más el proceso de scraping cuando tiene que scrapear tres o más sitios web. Se supone que la operación de scraping se realiza de forma secuencial.

Por lo tanto, de una forma u otra, tendrá que aplicar concurrencia o paralelismo a sus operaciones de scraping. Veremos el paralelismo primero en la siguiente sección.

Concurrencia en el web scraping con Python

Estoy seguro de que ya tienes una visión general de la concurrencia y el paralelismo. Esta sección se centrará en la concurrencia en el web scraping con un sencillo ejemplo de codificación en Python.

Un ejemplo sencillo de demostración sin ejecución concurrente

En este ejemplo, extraeremos de Wikipedia la URL de los países por una lista de capitales basada en la población. El programa guardaría los enlaces y luego iría a cada una de las 240 páginas y guardaría el HTML de esas páginas localmente.

 Para demostrar los efectos de la concurrencia, mostraremos dos programas: uno con ejecución secuencial y otro concurrente con multihilos.

Aquí está el código:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import time

def get_countries():
    countries = 'https://en.wikipedia.org/wiki/List_of_national_capitals_by_population'
    all_countries = []
    response = requests.get(countries)
    soup = BeautifulSoup(response.text, "html.parser")
    countries_pl = soup.select('th .flagicon+ a')
    for link_pl in countries_pl:
        link = link_pl.get("href")
        link = urljoin(countries, link)
        
        all_countries.append(link)
    return all_countries
  
def fetch(link):
    res = requests.get(link)
    with open(link.split("/")[-1]+".html", "wb") as f:
        f.write(res.content)
  

        
def main():
    clinks = get_countries()
    print(f"Total pages: {len(clinks)}")
    start_time = time.time()
    for link in clinks:
        fetch(link)
 
    duration = time.time() - start_time
    print(f"Downloaded {len(links)} links in {duration} seconds")
main()

Explicación del código

En primer lugar, importamos las bibliotecas, incluida BeautifulSoap, para extraer los datos HTML. Las otras librerías incluyen request para acceder al sitio web,urllib para unir las URLs como descubrirás, y la librería time para averiguar el tiempo total de ejecución del programa.

importar peticiones
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import tiempo

El programa comienza con el módulo principal, que llama a la función get_countries(). A continuación, la función accede a la URL de Wikipedia especificada en la variable countries a través de la instancia BeautifulSoup mediante el analizador HTML.

A continuación, busca la URL de la lista de países de la tabla extrayendo el valor del atributo href de la etiqueta anchor.

Los enlaces que recupera son enlaces relativos. La función urljoin los convertirá en enlaces absolutos. Estos enlaces se añaden a la matriz all_countries, que devuelve a la función principal 

A continuación, la función fetch guarda el contenido HTML de cada enlace como un archivo HTML. Es lo que hacen estos trozos de código:

def fetch(enlace):
    res = requests.get(link)
    con open(link.split("/")[-1]+".html", "wb") como f:
        f.write(res.content)

Por último, la función principal imprime el tiempo que se tardó en guardar los archivos en formato HTML. En nuestro PC, tardó 131,22 segundos.

Bien, este tiempo podría ciertamente hacerse más rápido. Lo averiguaremos en la siguiente sección, donde el mismo programa se ejecuta con múltiples hilos.

El mismo programa con concurrencia

En la versión multihilo, tendríamos que introducir pequeños cambios para que el programa se ejecutara más rápido.

Recuerda, la concurrencia consiste en crear múltiples hilos y ejecutar el programa. Hay dos maneras de crear hilos - manualmente y usando la clase ThreadPoolExecutor. 

Después de crear los hilos manualmente, podrías utilizar la función join en todos los hilos para el método manual. Al hacerlo, el método principal esperaría a que todos los hilos completaran su ejecución.

En este programa, vamos a ejecutar el código con la clase ThreadPoolExecutor que forma parte del módulo concurrent. futures. Así que en primer lugar, usted tiene que poner la siguiente línea en el programa anterior. 

from concurrent.futures import ThreadPoolExecutor

Después de eso, podría cambiar el bucle for que guarda el contenido HTML en formato HTML de la siguiente manera:

  con ThreadPoolExecutor(max_trabajadores=32) como executor:
           executor.map(fetch, clinks)

El código anterior crea un pool de hilos con un máximo de 32 hilos. Para cada CPU, el parámetro max_workers difiere, y es necesario experimentar con diferentes valores. No es necesariamente igual a cuanto mayor sea el número de hilos más rápido será el tiempo de ejecución.

Así que en nuestro PC produjo un resultado de 15,14 segundos, que es mucho mejor que cuando lo ejecutamos secuencialmente.

Así que antes de pasar a la siguiente sección, aquí está el código final para el programa con ejecución concurrente:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor
import time

def get_countries():
    countries = 'https://en.wikipedia.org/wiki/List_of_national_capitals_by_population'
    all_countries = []
    response = requests.get(countries)
    soup = BeautifulSoup(response.text, "html.parser")
    countries_pl = soup.select('th .flagicon+ a')
    for link_pl in countries_pl:
        link = link_pl.get("href")
        link = urljoin(countries, link)
        
        all_countries.append(link)
    return all_countries
  
def fetch(link):
    res = requests.get(link)
    with open(link.split("/")[-1]+".html", "wb") as f:
        f.write(res.content)


def main():
  clinks = get_countries()
  print(f"Total pages: {len(clinks)}")
  start_time = time.time()
  

  with ThreadPoolExecutor(max_workers=32) as executor:
           executor.map(fetch, clinks)
        
 
  duration = time.time()-start_time
  print(f"Downloaded {len(clinks)} links in {duration} seconds")
main()

Cómo el paralelismo puede acelerar el web scraping

Ahora esperamos que haya adquirido una comprensión de la ejecución concurrente. Para ayudarte a analizar mejor, veamos cómo se comporta el mismo programa en un entorno multiprocesador con procesos ejecutándose en paralelo en varias CPU.

En primer lugar hay que importar el módulo necesario :

from multiprocessing import Pool,cpu_count

Python proporciona el método cpu_count(), que cuenta el número de CPUs de su máquina. Sin duda, es útil para determinar el número exacto de tareas que podría realizar en paralelo.

Ahora tienes que sustituir el código con el bucle for en ejecución secuencial por este código:

con Pool (cpu_count()) como p:
 
   p.map(fetch,clinks)

Después de ejecutar este código, se produjo un tiempo de ejecución global de 20,10 segundos, que es relativamente más rápido que la ejecución secuencial en el primer programa.

Conclusión

Llegados a este punto, esperamos que tenga una visión global de la programación paralela y secuencial; la elección de una u otra depende principalmente del escenario concreto al que se enfrente.

Para el escenario de web scraping, recomendamos que se comience con la ejecución concurrente y luego pasar a una solución paralela sería genial. Esperamos que haya disfrutado de la lectura de este artículo y no se olvide de leer otros artículos relacionados con el web scraping en nuestro blog.