Scraping Urls in Parallalo

17 Febbraio 2025 di Daniele Frulla


Il calcolo parallelo è ormai diventato il default di ogni applicazione. Sviluppiamo una piccola applicazione che possa scaricare tantissime pagine web in parallelo.

Per controllare una url potreste utilizzare linkchecker.

Tools

Per scaricare le pagine web utilizzeremo Python.

Cominciamo con installare le librerie python che ci servono in un file requirements.txt:

requests
beautifulsoup4
multiprocessor

Per praticità ho creato una classe che scarica in seriale le varie pagine e il cui files si chiama search_serial.py:

import requests
import time
from bs4 import BeautifulSoup
from urllib.parse import urlparse
from bs4 import XMLParsedAsHTMLWarning
import warnings
from config import conf

warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)

class LinkExtractor:
    def __init__(self):
        self.links = set()  # Usare un set per evitare duplicati
        self.total_time = 0
        self.request_count = 0
        self.error_requests = 0

    def fetch_links(self, url):
        start_time = time.time()
        try:
            response = requests.get(url, timeout=4)
            response.raise_for_status()  # Verifica se la richiesta ha avuto successo
            soup = BeautifulSoup(response.text, 'html.parser')
            for link in soup.find_all('a', href=True):
                href = link['href']
                if self.is_valid_url(href):
                    self.links.add(href)  # Aggiungi al set
        except requests.exceptions.RequestException:
            self.error_requests += 1
            pass  # Ignora l'errore
        finally:
            elapsed_time = time.time() - start_time
            self.total_time += elapsed_time
            self.request_count += 1

    def is_valid_url(self, url):
        parsed = urlparse(url)
        return bool(parsed.scheme) and bool(parsed.netloc)

    def get_links(self):
        return list(self.links)  # Converti il set in una lista

    def fetch_links_serially(self, urls):
        for url in urls:
            self.fetch_links(url)

    def get_average_time_per_request(self):
        if self.request_count > 0:
            return self.total_time / self.request_count
        return 0

# Esempio di utilizzo
if __name__ == '__main__':
    max_links = conf['max_links']
    start_time = time.time()  # Tempo di inizio
    extractor = LinkExtractor()
    seed_links = [
        'https://en.wikipedia.org/wiki/Multiprocessing'
    ]

    # Aggiungi i link seed al set iniziale
    extractor.fetch_links_serially(seed_links)

    all_links = set(seed_links)

    while len(all_links) < max_links:
        current_links = list(all_links)
        extractor.fetch_links_serially(current_links)
        new_links = set(extractor.get_links())
        all_links.update(new_links)
        print(f"Requests: {len(current_links)}")
        print(f"Numero di link trovati: {len(all_links)}")
        print(f"Tempo medio per richiesta: {extractor.get_average_time_per_request()} secondi")

    print(f"Requests: {len(current_links)}")
    print(f"Numero di link trovati: {len(all_links)}")
    print(f"Tempo medio per richiesta: {extractor.get_average_time_per_request()} secondi")

    end_time = time.time()  # Tempo di fine
    total_execution_time = end_time - start_time
    print(f"Tempo totale di esecuzione: {total_execution_time} secondi")

    print(f"Errori nelle richieste con timeout=4 : {extractor.error_requests }")

In contrapposizione possiamo vedere un altro file che si chiama search_paralax.py il quale esegue la scansione in parallelo:

import os
import concurrent.futures
import requests
import time
from bs4 import BeautifulSoup
from urllib.parse import urlparse
from bs4 import XMLParsedAsHTMLWarning
import warnings
from config import conf

warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)

class LinkExtractor:
    def __init__(self):
        self.links = set()  # Usare un set per evitare duplicati
        self.num_processors = os.cpu_count()  # Determina il numero di processori disponibili
        self.total_time = 0
        self.request_count = 0
        self.error_requests = 0
        print(f"Numero di processori disponibili: {self.num_processors}")

    def fetch_links(self, url):
        start_time = time.time()
        try:
            response = requests.get(url, timeout=4)
            response.raise_for_status()  # Verifica se la richiesta ha avuto successo
            soup = BeautifulSoup(response.text, 'html.parser')
            for link in soup.find_all('a', href=True):
                href = link['href']
                if self.is_valid_url(href):
                    self.links.add(href)  # Aggiungi al set
        except requests.exceptions.RequestException:
            self.error_requests += 1
            pass  # Ignora l'errore
        finally:
            elapsed_time = time.time() - start_time
            self.total_time += elapsed_time
            self.request_count += 1

    def is_valid_url(self, url):
        parsed = urlparse(url)
        return bool(parsed.scheme) and bool(parsed.netloc)

    def get_links(self):
        return list(self.links)  # Converti il set in una lista

    def fetch_links_concurrently(self, urls):
        with concurrent.futures.ThreadPoolExecutor(max_workers=self.num_processors/2) as executor:
            executor.map(self.fetch_links, urls)

    def get_average_time_per_request(self):
        if self.request_count > 0:
            return self.total_time / self.request_count
        return 0

# Esempio di utilizzo
if __name__ == '__main__':
    max_links = conf['max_links']
    start_time = time.time()  # Tempo di inizio

    extractor = LinkExtractor()
    seed_links = [
        'https://en.wikipedia.org/wiki/Multiprocessing'
    ]

    # Aggiungi i link seed al set iniziale
    extractor.fetch_links_concurrently(seed_links)

    all_links = set(seed_links)

    while len(all_links) < max_links:
        current_links = list(all_links)
        extractor.fetch_links_concurrently(current_links)
        new_links = set(extractor.get_links())
        all_links.update(new_links)
        print(f"Numero di link trovati: {len(all_links)}")
        print(f"Tempo medio per richiesta: {extractor.get_average_time_per_request()} secondi")

    end_time = time.time()  # Tempo di fine
    total_execution_time = end_time - start_time
    print(f"Tempo totale di esecuzione: {total_execution_time} secondi")

    print(f"Errori nelle richieste con timeout=4 : {extractor.error_requests }")

Risultati e Tempistiche

Potete notare i tempi di esecuzione minori per l’esecuzione in parallelo rispetto a quella seriale.

Esecuzione in seriale:

Requests: 1
Numero di link trovati: 54
Tempo medio per richiesta: 0.40172243118286133 secondi
Requests: 54
Numero di link trovati: 916
Tempo medio per richiesta: 0.9019935854843685 secondi
Requests: 916
Numero di link trovati: 19853
Tempo medio per richiesta: 0.9663913620352255 secondi
Requests: 916
Numero di link trovati: 19853
Tempo medio per richiesta: 0.9663913620352255 secondi
Tempo totale di esecuzione: 939.5156524181366 secondi
Errori nelle richieste con timeout=4 : 88

Esecuzione in parallelo:

Numero di processori disponibili: 8
Numero di link trovati: 54
Tempo medio per richiesta: 0.401678204536438 secondi
Numero di link trovati: 916
Tempo medio per richiesta: 0.7874308058193752 secondi
Numero di link trovati: 16806
Tempo medio per richiesta: 0.7118699607044581 secondi
Tempo totale di esecuzione: 181.69285488128662 secondi
Errori nelle richieste con timeout=4 : 131

Il test è stato eseguito con 4 processi paralleli, e avrebbero dovuto inserire nella lista delle urls 2000 links validi. In parallelo ne sono state inserite meno, ma come puoi vedere il tempo di esecuzione è nettamente inferiore alla esecuzione seriale.

Codice

Tutto il codice lo potete trovare nel web scraping parallelo github.

Related Posts


Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *


Copyright di Caterina Mezzapelle Part. I.V.A. 02413940814 - R.E.A. 191812