Concorrenza e parallelismo: Differenze significative per lo scraping del web

Raschiamento, Le differenze, Jan-17-20225 minuti di lettura

Quando si parla di concurrency e parallelismo, si può pensare che si riferiscano agli stessi concetti nell'esecuzione di programmi informatici in un ambiente multi-thread. Dopo aver visto le loro definizioni nel dizionario Oxford, si potrebbe essere portati a pensarlo. Tuttavia, quando si approfondiscono queste nozioni in relazione a

Quando si parla di concurrency e parallelismo, si può pensare che si riferiscano agli stessi concetti nell'esecuzione di programmi informatici in un ambiente multi-thread. Dopo aver visto le loro definizioni nel dizionario Oxford, si potrebbe essere portati a pensare che sia così. Tuttavia, quando si approfondiscono queste nozioni in relazione al modo in cui la CPU esegue le istruzioni del programma, si nota che la concorrenza e il parallelismo sono due concetti distinti. 

Questo articolo approfondisce il tema della concorrenza e del parallelismo, le loro differenze e la loro collaborazione per migliorare la produttività dell'esecuzione dei programmi. Infine, discuteremo quali sono le due strategie più adatte per il web scraping. Iniziamo.

Che cos'è l'esecuzione concorrente?

Innanzitutto, per semplificare le cose, inizieremo con la concorrenza in una singola applicazione eseguita in un singolo processore. Dictionary.com definisce la concurrency come un'azione o uno sforzo combinato e il verificarsi di eventi simultanei. Tuttavia, si potrebbe dire lo stesso dell'esecuzione parallela, poiché le esecuzioni coincidono, e quindi questa definizione è in qualche modo fuorviante nel mondo della programmazione informatica.

Nella vita di tutti i giorni, si hanno esecuzioni simultanee sul computer. Ad esempio, si può leggere un articolo di blog sul browser mentre si ascolta la musica su Windows Media Player. Ci sarebbe un altro processo in esecuzione: scaricare un file PDF da un'altra pagina web; tutti questi esempi sono processi separati.

Prima dell'invenzione delle applicazioni a esecuzione concorrente, le CPU eseguivano i programmi in modo sequenziale. Ciò implicava che le istruzioni di un programma dovevano completare l'esecuzione prima che la CPU passasse al programma successivo.

Al contrario, l'esecuzione concorrente alterna un po' di ogni processo finché tutti non sono stati completati.

In un ambiente di esecuzione multi-thread a processore singolo, un programma viene eseguito quando un altro è bloccato per l'input dell'utente. Ora ci si può chiedere cosa sia un ambiente multi-thread. Si tratta di un insieme di thread che vengono eseguiti in modo indipendente l'uno dall'altro - per saperne di più sui thread si veda la prossima sezione.

La concomitanza non deve essere confusa con l'esecuzione parallela.

È più facile confondere la concorrenza con il parallelismo. Negli esempi precedenti, per concurrency si intende che i processi non vengono eseguiti in parallelo. 

Se invece un processo richiede il completamento di un'operazione di Input/Output, il sistema operativo alloca la CPU a un altro processo mentre questo completa l'operazione di I/O. Questa procedura continua finché tutti i processi non completano la loro esecuzione. Questa procedura continuerà finché tutti i processi non avranno completato la loro esecuzione.

Tuttavia, poiché la commutazione dei compiti da parte del sistema operativo avviene nell'arco di un nano o microsecondo, all'utente sembrerebbe che i processi vengano eseguiti in parallelo, 

Che cos'è una filettatura?

A differenza dell'esecuzione sequenziale, con le architetture attuali la CPU non può eseguire l'intero processo/programma in una sola volta. Invece, la maggior parte dei computer può suddividere l'intero processo in diversi componenti leggeri che vengono eseguiti indipendentemente l'uno dall'altro in un ordine arbitrario. Questi componenti leggeri sono chiamati thread.

Ad esempio, Google Docs potrebbe avere diversi thread che operano in contemporanea. Mentre un thread salva automaticamente il lavoro, un altro potrebbe funzionare in background, controllando l'ortografia e la grammatica.  

Il sistema operativo determina l'ordine e la priorità dei thread, che dipende dal sistema.

Che cos'è l'esecuzione parallela?

Ora conoscete l'esecuzione di programmi informatici in un ambiente con una singola CPU. Al contrario, i computer moderni eseguono molti processi simultaneamente in più CPU, il che è noto come esecuzione parallela. La maggior parte delle architetture attuali dispone di più CPU.

Come si può vedere nel diagramma seguente, la CPU esegue ogni thread appartenente a un processo in parallelo tra loro.  

Nel parallelismo, il sistema operativo passa i thread da e verso la CPU in intervalli di macro o microsecondi, a seconda dell'architettura del sistema. Affinché il sistema operativo raggiunga l'esecuzione parallela, i programmatori di computer utilizzano il concetto noto come programmazione parallela. Nella programmazione parallela, i programmatori sviluppano il codice per utilizzare al meglio le diverse CPU. 

Come la concorrenza può accelerare il web scraping

Con così tanti domini che utilizzano il web scraping per raschiare i dati dai siti web, uno svantaggio significativo è il tempo che si impiega per raschiare grandi quantità di dati. Se non si è uno sviluppatore esperto, si può finire per perdere molto tempo a sperimentare tecniche specifiche prima di eseguire il codice senza errori e in modo perfetto.

La sezione seguente illustra alcuni dei motivi per cui il web scraping è lento.

I motivi principali per cui il web scraping è lento?

In primo luogo, lo scraper deve navigare verso il sito web di destinazione nel web scraping. Poi deve estrarre e recuperare le entità dai tag HTML da cui si desidera effettuare lo scraping. Infine, nella maggior parte dei casi, i dati vengono salvati in un file esterno come il formato CSV.  

Come si può vedere, la maggior parte dei compiti sopra descritti richiede operazioni di I/O vincolate, come l'estrazione di dati dai siti web e il loro salvataggio su file esterni. La navigazione verso i siti web di destinazione dipende spesso da fattori esterni, come la velocità della rete o l'attesa della disponibilità di una rete.

Come si può vedere nella figura seguente, questo consumo di tempo estremamente lento può ostacolare ulteriormente il processo di scraping quando si devono scrapare tre o più siti web. Si presuppone che l'operazione di scraping venga eseguita in sequenza.

Pertanto, in un modo o nell'altro, dovrete applicare la concorrenza o il parallelismo alle vostre operazioni di scraping. Nella prossima sezione esamineremo prima il parallelismo.

Concorrenza nel web scraping con Python

Sono sicuro che ormai avete una panoramica della concorrenza e del parallelismo. Questa sezione si concentrerà sulla concorrenza nel web scraping con un semplice esempio di codifica in Python.

Un semplice esempio di dimostrazione senza esecuzione concorrente

In questo esempio, si scrape l'URL dei Paesi da un elenco di capitali basato sulla popolazione da Wikipedia. Il programma salverà i link e poi andrà su ciascuna delle 240 pagine e salverà l'HTML di quelle pagine a livello locale.

 Per dimostrare gli effetti della concomitanza, mostreremo due programmi: uno con esecuzione sequenziale e l'altro in concomitanza con multi-thread.

Ecco il codice:

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

Spiegazione del codice

Per prima cosa, importiamo le librerie, tra cui BeautifulSoap, per estrarre i dati HTML. Le altre librerie includono request per accedere al sito web, urllib per unire gli URL, come scoprirete, e la libreria time per conoscere il tempo totale di esecuzione del programma.

importare le richieste
da bs4 import BeautifulSoup
da urllib.parse import urljoin
importare tempo

Il programma inizia con il modulo principale, che chiama la funzione get_countries(). La funzione accede quindi all'URL di Wikipedia specificato nella variabile countries tramite l'istanza BeautifulSoup attraverso il parser HTML.

Quindi cerca l'URL per l'elenco dei Paesi nella tabella, estraendo il valore nell'attributo href del tag di ancoraggio.

I link recuperati sono link relativi. La funzione urljoin li convertirà in collegamenti assoluti. Questi collegamenti vengono poi aggiunti all'array all_countries, che viene restituito alla funzione principale 

Poi la funzione fetch salva il contenuto HTML di ogni link come file HTML. È ciò che fanno questi pezzi di codice:

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

Infine, la funzione principale stampa il tempo necessario per salvare i file in formato HTML. Nel nostro PC sono stati necessari 131,22 secondi.

Ebbene, questo tempo potrebbe essere reso più veloce. Lo scopriremo nella prossima sezione, dove lo stesso programma viene eseguito con più thread.

Lo stesso programma con concomitanza

Nella versione multithread, dovremo apportare piccole modifiche per velocizzare l'esecuzione del programma.

Ricordate che la concorrenza consiste nel creare più thread ed eseguire il programma. Esistono due modi per creare i thread: manualmente e utilizzando la classe ThreadPoolExecutor. 

Dopo aver creato i thread manualmente, si può usare la funzione join su tutti i thread per il metodo manuale. In questo modo, il metodo principale aspetterà che tutti i thread completino la loro esecuzione.

In questo programma, eseguiremo il codice con la classe ThreadPoolExecutor, che fa parte del modulo concurrent. futures. Quindi, per prima cosa, è necessario inserire la riga seguente nel programma precedente. 

da concurrent.futures import ThreadPoolExecutor

Successivamente, si può modificare il ciclo for che salva il contenuto HTML in formato HTML come segue:

  con ThreadPoolExecutor(max_workers=32) come executor:
           executor.map(fetch, clinks)

Il codice precedente crea un pool di thread con un massimo di 32 thread. Per ogni CPU, il parametro max_workers varia e bisogna sperimentare diversi valori. Non è detto che il numero di thread sia maggiore e che il tempo di esecuzione sia più veloce.

Il nostro PC ha quindi prodotto un risultato di 15,14 secondi, decisamente migliore rispetto all'esecuzione sequenziale.

Prima di passare alla prossima sezione, ecco il codice finale del programma con esecuzione concorrente:

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

Come il parallelismo potrebbe accelerare il web scraping

Ora speriamo che abbiate acquisito una comprensione dell'esecuzione concorrente. Per aiutarvi ad analizzare meglio, vediamo come si comporta lo stesso programma in un ambiente multiprocessore con processi che vengono eseguiti in parallelo su più CPU.

Per prima cosa è necessario importare il modulo richiesto :

da multiprocessing import Pool,cpu_count

Python fornisce il metodo cpu_count(), che conta il numero di CPU presenti nella macchina. È indubbiamente utile per determinare il numero preciso di compiti che si possono eseguire in parallelo.

Ora è necessario sostituire il codice con il ciclo for in esecuzione sequenziale con questo codice:

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

Dopo l'esecuzione di questo codice, il tempo di esecuzione complessivo è stato di 20,10 secondi, relativamente più veloce rispetto all'esecuzione sequenziale del primo programma.

Conclusione

A questo punto, speriamo che abbiate una panoramica completa della programmazione parallela e sequenziale: la scelta di utilizzare l'una piuttosto che l'altra dipende principalmente dallo scenario particolare che avete affrontato.

Per lo scenario del web scraping, consigliamo di iniziare con l'esecuzione concorrente per poi passare a una soluzione parallela. Ci auguriamo che la lettura di questo articolo vi sia piaciuta. Non dimenticate di leggere altri articoli sul web scraping come questo nel nostro blog.