Concurrence et parallélisme : Différences significatives pour le Web Scraping

Grattage, Les différences, Jan-17-20225 minutes de lecture

Quand on parle de concurrence et de parallélisme, il peut sembler évident qu'il s'agit des mêmes concepts dans l'exécution de programmes informatiques dans un environnement multithread. Après avoir consulté leurs définitions dans le dictionnaire Oxford, on peut être enclin à le penser. Cependant, lorsque l'on approfondit ces notions en ce qui concerne le

Quand on parle de concurrence et de parallélisme, il peut sembler évident qu'il s'agit des mêmes concepts dans l'exécution de programmes informatiques dans un environnement multithread. Après avoir consulté leurs définitions dans le dictionnaire Oxford, on peut être enclin à le penser. Cependant, lorsque vous approfondissez ces notions en ce qui concerne la manière dont l'unité centrale exécute les instructions du programme, vous remarquerez que la concurrence et le parallélisme sont deux concepts distincts. 

Cet article examine plus en détail la concurrence et le parallélisme, leurs différences et la manière dont ils fonctionnent ensemble pour améliorer la productivité de l'exécution des programmes. Enfin, nous verrons quelles sont les deux stratégies les plus adaptées au web scraping. Commençons.

Qu'est-ce que l'exécution simultanée ?

Tout d'abord, pour simplifier les choses, nous commencerons par la concurrence dans une seule application exécutée par un seul processeur. Dictionary.com définit la concurrence comme une action ou un effort combiné et l'apparition d'événements simultanés. Cependant, on pourrait dire la même chose de l'exécution parallèle, car les exécutions coïncident, et cette définition est donc quelque peu trompeuse dans le monde de la programmation informatique.

Dans la vie de tous les jours, vous aurez des exécutions simultanées sur votre ordinateur. Par exemple, vous pouvez lire un article de blog sur votre navigateur tout en écoutant de la musique sur votre lecteur Windows Media. Un autre processus est en cours d'exécution : le téléchargement d'un fichier PDF à partir d'une autre page web - tous ces exemples sont des processus distincts.

Avant l'invention des applications à exécution simultanée, les CPU exécutaient les programmes de manière séquentielle. Cela impliquait que les instructions d'un programme devaient avoir terminé leur exécution avant que l'unité centrale ne passe au programme suivant.

En revanche, l'exécution simultanée alterne un peu de chaque processus jusqu'à ce que tous soient terminés.

Dans un environnement d'exécution multithread à processeur unique, un programme s'exécute lorsqu'un autre est bloqué pour une entrée utilisateur. Vous vous demandez peut-être ce qu'est un environnement multithread. Il s'agit d'un ensemble de threads qui s'exécutent indépendamment les uns des autres - nous reviendrons sur les threads dans la prochaine section.

Il ne faut pas confondre simultanéité et exécution parallèle.

Il est alors plus facile de confondre la concurrence et le parallélisme. Dans les exemples ci-dessus, la concurrence signifie que les processus ne s'exécutent pas en parallèle. 

En revanche, si un processus nécessite une opération d'entrée/sortie, le système d'exploitation attribue l'unité centrale à un autre processus pendant qu'il termine son opération d'entrée/sortie. Cette procédure se poursuivra jusqu'à ce que tous les processus aient terminé leur exécution.

Cependant, comme la commutation des tâches par le système d'exploitation s'effectue en une nano ou microseconde, l'utilisateur a l'impression que les processus sont exécutés en parallèle, 

Qu'est-ce qu'un fil ?

Contrairement à l'exécution séquentielle, l'unité centrale ne peut pas exécuter l'ensemble du processus/programme en une seule fois avec les architectures actuelles. Au lieu de cela, la plupart des ordinateurs peuvent diviser le processus entier en plusieurs composants légers qui s'exécutent indépendamment les uns des autres dans un ordre arbitraire. Ces composants légers sont appelés "threads".

Par exemple, Google Docs peut avoir plusieurs fils d'exécution qui fonctionnent simultanément. Tandis qu'un thread enregistre automatiquement votre travail, un autre peut fonctionner en arrière-plan, vérifiant l'orthographe et la grammaire.  

Le système d'exploitation détermine l'ordre et les threads à prioriser, ce qui dépend du système.

Qu'est-ce que l'exécution parallèle ?

Vous connaissez maintenant l'exécution de programmes informatiques dans un environnement doté d'une seule unité centrale. En revanche, les ordinateurs modernes exécutent de nombreux processus simultanément dans plusieurs unités centrales, ce que l'on appelle l'exécution parallèle. La plupart des architectures actuelles disposent de plusieurs unités centrales.

Comme vous pouvez le voir dans le diagramme ci-dessous, l'unité centrale exécute chaque thread appartenant à un processus en parallèle les uns avec les autres.  

Dans le parallélisme, le système d'exploitation fait passer les threads de l'unité centrale à l'unité centrale en l'espace de quelques macro ou microsecondes, en fonction de l'architecture du système. Pour que le système d'exploitation parvienne à une exécution parallèle, les programmeurs informatiques utilisent le concept connu sous le nom de programmation parallèle. Dans le cadre de la programmation parallèle, les programmeurs développent un code qui permet d'utiliser au mieux les différentes unités centrales. 

Comment la concurrence pourrait accélérer le scraping web

De nombreux domaines utilisent le web scraping pour extraire des données de sites web, mais l'un des inconvénients majeurs est le temps qu'il faut pour extraire de grandes quantités de données. Si vous n'êtes pas un développeur chevronné, vous risquez de perdre beaucoup de temps à expérimenter des techniques spécifiques avant d'exécuter le code parfaitement et sans erreur.

La section ci-dessous présente quelques-unes des raisons pour lesquelles le web scraping est lent.

Quelles sont les principales raisons de la lenteur du web scraping ?

Tout d'abord, le scraper doit naviguer jusqu'au site web cible dans le cadre du web scraping. Ensuite, il doit extraire et récupérer les entités des balises HTML à partir desquelles vous souhaitez faire du scrapping. Enfin, dans la plupart des cas, vous enregistrez les données dans un fichier externe, au format CSV par exemple.  

Comme vous pouvez le constater, la plupart des tâches susmentionnées nécessitent des opérations d'E/S liées lourdes, telles que l'extraction de données à partir de sites web et leur enregistrement dans des fichiers externes. La navigation vers les sites web cibles dépend souvent de facteurs externes tels que la vitesse du réseau ou l'attente de la disponibilité d'un réseau.

Comme vous pouvez le voir dans la figure ci-dessous, cette extrême lenteur peut handicaper davantage le processus de scrapping lorsque vous devez scrapper trois sites web ou plus. Il est supposé que vous exécutez l'opération de scrapping de manière séquentielle.

Par conséquent, d'une manière ou d'une autre, vous devrez appliquer la concurrence ou le parallélisme à vos opérations de scraping. Nous examinerons d'abord le parallélisme dans la section suivante.

Concurrence dans le web scraping à l'aide de Python

Je suis sûr que vous avez maintenant une vue d'ensemble de la concurrence et du parallélisme. Cette section se concentre sur la concurrence dans le web scraping avec un exemple de codage simple en Python.

Un exemple simple démontrant l'absence d'exécution simultanée

Dans cet exemple, nous allons récupérer l'URL des pays par une liste de capitales basée sur la population à partir de Wikipedia. Le programme enregistrera les liens, puis ira sur chacune des 240 pages et enregistrera localement le code HTML de ces pages.

 Pour démontrer les effets de la concurrence, nous présenterons deux programmes - l'un avec une exécution séquentielle et l'autre avec une exécution simultanée à plusieurs fils.

Voici le code :

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

Explication du code

Tout d'abord, nous importons les bibliothèques, y compris BeautifulSoap, pour extraire les données HTML. Les autres bibliothèques comprennent la requête pour accéder au site web, urllib pour joindre les URLs comme vous le découvrirez, et la bibliothèque time pour connaître le temps d'exécution total du programme.

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

Le programme commence par le module principal, qui appelle la fonction get_countries(). La fonction accède alors à l'URL de Wikipedia spécifiée dans la variable countries via l'instance BeautifulSoup à travers l'analyseur HTML.

Il recherche ensuite l'URL de la liste des pays dans le tableau en extrayant la valeur de l'attribut href de la balise d'ancrage.

Les liens que vous récupérez sont des liens relatifs. La fonction urljoin les convertit en liens absolus. Ces liens sont ensuite ajoutés au tableau all_countries, qui est renvoyé à la fonction principale 

Ensuite, la fonction fetch enregistre le contenu HTML de chaque lien dans un fichier HTML. C'est ce que font ces morceaux de code :

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

Enfin, la fonction principale imprime le temps nécessaire à l'enregistrement des fichiers au format HTML. Dans notre PC, cela a pris 131,22 secondes.

Ce temps pourrait certainement être accéléré. Nous le découvrirons dans la section suivante, où le même programme est exécuté avec plusieurs threads.

Le même programme avec la concurrence

Dans la version multithread, nous devrions apporter des modifications mineures pour que le programme s'exécute plus rapidement.

Rappelez-vous que la concurrence consiste à créer plusieurs threads et à exécuter le programme. Il existe deux façons de créer des threads : manuellement et en utilisant la classe ThreadPoolExecutor. 

Après avoir créé les threads manuellement, vous pouvez utiliser la fonction join sur tous les threads pour la méthode manuelle. Ainsi, la méthode principale attendra que tous les threads aient terminé leur exécution.

Dans ce programme, nous allons exécuter le code avec la classe ThreadPoolExecutor qui fait partie du module concurrent. futures. Donc, tout d'abord, vous devez mettre la ligne ci-dessous dans le programme ci-dessus. 

from concurrent.futures import ThreadPoolExecutor

Ensuite, vous pouvez modifier la boucle for qui enregistre le contenu HTML au format HTML comme suit :

  avec ThreadPoolExecutor(max_workers=32) comme executor :
           executor.map(fetch, clinks)

Le code ci-dessus crée un pool de threads avec un maximum de 32 threads. Pour chaque unité centrale, le paramètre max_workers diffère, et vous devez expérimenter avec différentes valeurs. Cela ne signifie pas nécessairement que plus le nombre de threads est élevé, plus le temps d'exécution est rapide.

Notre PC a donc produit un résultat de 15,14 secondes, ce qui est bien mieux que lorsque nous l'avons exécuté de manière séquentielle.

Avant de passer à la section suivante, voici le code final du programme avec exécution simultanée :

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

Comment le parallélisme peut accélérer le scraping web

Nous espérons maintenant que vous avez acquis une certaine compréhension de l'exécution simultanée. Pour vous aider à mieux analyser, examinons les performances du même programme dans un environnement multiprocesseur avec des processus s'exécutant en parallèle sur plusieurs unités centrales.

Vous devez tout d'abord importer le module requis :

from multiprocessing import Pool,cpu_count

Python fournit la méthode cpu_count(), qui compte le nombre d'unités centrales de votre machine. Elle est sans aucun doute utile pour déterminer le nombre précis de tâches pouvant être exécutées en parallèle.

Vous devez maintenant remplacer le code avec la boucle for dans l'exécution séquentielle par ce code :

avec Pool (cpu_count()) as p :
 
   p.map(fetch,clinks)

Après avoir exécuté ce code, il a produit un temps d'exécution global de 20,10 secondes, ce qui est relativement plus rapide que l'exécution séquentielle du premier programme.

Conclusion

À ce stade, nous espérons que vous avez une vue d'ensemble de la programmation parallèle et séquentielle - le choix d'utiliser l'une ou l'autre dépend principalement du scénario particulier auquel vous avez été confronté.

Pour le scénario du web scraping, nous recommandons de commencer par l'exécution simultanée et de passer ensuite à une solution parallèle. Nous espérons que vous avez apprécié la lecture de cet article. N'oubliez pas de lire d'autres articles relatifs au web scraping sur notre blog.