Tecnologie e protezioni 16 min di lettura

Scraping di siti dinamici: due approcci

Due modi per fare scraping di siti dinamici: reverse engineering delle richieste XHR all’API e browser headless — vantaggi, svantaggi e criteri di scelta.

TW
Team Web-Scraping.it
Raccolta dati per le esigenze del business
Pubblicato il 2 marzo 2025

I siti moderni raramente restituiscono l’HTML già pronto con i dati. Il catalogo dei prodotti, il feed, i prezzi, le recensioni — quasi tutto viene caricato dopo che la pagina è già stata aperta, tramite JavaScript. Per uno scraper questo significa che una semplice richiesta GET della pagina e il successivo parsing dell’HTML restituiranno uno scheletro vuoto, senza dati.

Ci sono due modi radicalmente diversi di risolvere questo problema:

  1. Intercettare le richieste API — individuare le richieste con cui il browser stesso preleva i dati e ripeterle direttamente, senza browser.
  2. Emulazione completa del browser — avviare un browser reale (o headless), lasciare che renderizzi il JS e operare sulla pagina come un utente: cliccare, scorrere, trascinare elementi.

Il primo approccio è più veloce e consuma meno risorse; il secondo è più universale e più robusto di fronte a logiche insolite. Nella pratica vengono spesso combinati.


Approccio 1. Intercettare ed emulare le richieste API

L’idea

Quando una pagina «completa» i propri dati al volo, quasi sempre effettua richieste HTTP in background (XHR/fetch) a un’API interna che restituisce JSON. Se individui questo endpoint e riproduci la richiesta con header, cookie e token corretti, puoi ottenere i dati direttamente, senza passare per il rendering. È decine di volte più veloce e non richiede un browser.

Come individuare l’API

  1. Apri i DevTools (F12) → scheda Network.
  2. Filtra per Fetch/XHR.
  3. Scorri la pagina, premi «Mostra altro», cambia categoria — in una parola, provoca il caricamento dei dati.
  4. Individua la richiesta nella cui risposta si trova il JSON che ti serve (prodotti, prezzi ecc.).
  5. Esaminala: URL, metodo, parametri della richiesta, header, corpo e cookie.
  6. Clic destro → Copy → Copy as cURL — un ottimo punto di partenza: puoi importarlo in Postman o tradurlo direttamente in codice.

Token: CSRF, sessioni, autenticazione

La difficoltà principale di questo approccio è che la richiesta non è quasi mai «nuda». Il server si aspetta una serie di dati di conferma e, senza di essi, restituisce 401, 403 o 419.

Token CSRF (Cross-Site Request Forgery). Protezione contro la falsificazione delle richieste tra siti. Il server emette un token casuale che il client deve restituire nelle richieste che modificano i dati (e talvolta anche in quelle di lettura). Dove si trova di solito:

  • in <meta name="csrf-token" content="...">, nell’HTML della pagina;
  • in un campo nascosto del form <input type="hidden" name="_token" value="...">;
  • in un cookie (spesso XSRF-TOKEN) che poi va duplicato nell’header X-CSRF-Token o X-XSRF-TOKEN.

Lo schema è questo: prima si carica la pagina normale, si estraggono il token e i cookie di sessione, poi li si inserisce nella richiesta all’API.

Cookie di sessione. Al primo accesso il server invia Set-Cookie (per esempio sessionid, PHPSESSID, laravel_session). Vanno conservati tra una richiesta e l’altra — per questo si usa un oggetto sessione (requests.Session, httpx.Client), che lo fa automaticamente.

Autenticazione (Bearer / JWT / chiave API). Se i dati sono dietro un login, l’header di solito contiene Authorization: Bearer <token>. I token JWT si ottengono tramite l’endpoint di login e poi si allegano a ogni richiesta.

Altri campi di protezione. X-Requested-With: XMLHttpRequest (spesso obbligatorio per gli endpoint AJAX), Referer, Origin e, a volte, parametri firmati (signature, nonce, timestamp) generati dal JavaScript del frontend.

Quando questo approccio si rompe. Se il token o la firma della richiesta vengono generati da un JavaScript offuscato direttamente nel browser (o in WASM), riprodurlo lato server è estremamente difficile. È il segnale che conviene passare al secondo approccio — l’emulazione del browser, dove il JS si esegue da solo.

Esempio: Python + requests (con CSRF e paginazione)

python
import re
import requests

session = requests.Session()
BASE = "https://example-shop.com"

# 1. Carichiamo la pagina per ottenere il token CSRF e i cookie di sessione
resp = session.get(f"{BASE}/catalog", headers={
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
})

# Il CSRF può trovarsi in un tag meta...
m = re.search(r'name="csrf-token"\s+content="([^"]+)"', resp.text)
csrf = m.group(1) if m else session.cookies.get("XSRF-TOKEN")

headers = {
    "X-CSRF-Token": csrf,
    "X-Requested-With": "XMLHttpRequest",
    "Referer": f"{BASE}/catalog",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
    "Accept": "application/json",
}

# 2. Interroghiamo l'API interna pagina per pagina
all_items = []
page = 1
while True:
    r = session.get(
        f"{BASE}/api/products",
        params={"category": "phones", "page": page, "per_page": 48},
        headers=headers,
    )
    r.raise_for_status()
    payload = r.json()
    items = payload.get("items", [])
    if not items:
        break
    all_items.extend(items)
    page += 1

for it in all_items:
    print(it["title"], it["price"])

Esempio: Python + httpx (async, più veloce su grandi volumi)

python
import asyncio
import httpx

async def fetch_page(client, page):
    r = await client.get("/api/products", params={"page": page, "per_page": 48})
    return r.json().get("items", [])

async def main():
    async with httpx.AsyncClient(base_url="https://example-shop.com",
                                 headers={"X-Requested-With": "XMLHttpRequest"}) as client:
        tasks = [fetch_page(client, p) for p in range(1, 11)]
        results = await asyncio.gather(*tasks)
    items = [x for chunk in results for x in chunk]
    print(len(items))

asyncio.run(main())

Esempio: Node.js + fetch

javascript
const csrf = "..."; // estratto in anticipo dall'HTML/cookie

const res = await fetch("https://example-shop.com/api/products?page=1&per_page=48", {
  headers: {
    "X-CSRF-Token": csrf,
    "X-Requested-With": "XMLHttpRequest",
    "Accept": "application/json",
    "Cookie": "sessionid=abc123; XSRF-TOKEN=" + csrf,
  },
});

const data = await res.json();
data.items.forEach(item => console.log(item.title, item.price));

Approccio 2. Emulazione completa del browser

L’idea

Avviamo un motore reale (Chromium, Firefox, WebKit): scarica la pagina, esegue tutto il JS e renderizza il DOM. Da lì lavoriamo sulla pagina come farebbe una persona: aspettiamo che compaiano gli elementi, clicchiamo, scorriamo, trasciniamo gli slider. Tutti i token, le firme e gli script anti-bot si eseguono da soli — non dobbiamo riprodurli.

Svantaggi: è di un ordine di grandezza più lento, vorace di CPU/RAM e più facile da rilevare per i sistemi anti-bot (ma a questo si rimedia con appositi modi «stealth»).

Con che cosa emulare

  • Selenium — lo standard più datato; supporta Python, Java, C#, JavaScript e Ruby. Controlla browser reali tramite WebDriver.
  • Playwright — un framework moderno di Microsoft. Python, JavaScript/TS, .NET, Java. Chromium, Firefox e WebKit «pronti all’uso», attesa automatica intelligente degli elementi e una comoda intercettazione delle richieste di rete.
  • Puppeteer — Node.js; in origine solo Chromium (esiste un supporto sperimentale per Firefox). Molto veloce e maturo per Chrome.

Azioni sulla pagina

Qui sotto — le stesse quattro azioni (clic, scroll, scroll fino a un elemento, tenere premuto il tasto del mouse + movimento) su stack diversi. Tenere premuto + movimento è la base del drag-and-drop, degli slider e dei captcha a slider.

Playwright (Python)

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://example-shop.com/catalog")

    # CLIC
    page.click("button.load-more")

    # SCROLL con la rotellina
    page.mouse.wheel(0, 2000)

    # SCROLL fino a un elemento specifico
    page.locator("footer").scroll_into_view_if_needed()

    # TIENI PREMUTO il tasto + MOVIMENTO (drag / slider)
    box = page.locator(".slider-handle").bounding_box()
    start_x = box["x"] + box["width"] / 2
    start_y = box["y"] + box["height"] / 2
    page.mouse.move(start_x, start_y)
    page.mouse.down()                                   # tasto premuto
    page.mouse.move(start_x + 200, start_y, steps=25)   # spostamento graduale (25 passi)
    page.mouse.up()                                     # rilasciamo

    browser.close()

Playwright (JavaScript/Node)

javascript
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto('https://example-shop.com/catalog');

  // CLIC
  await page.click('button.load-more');

  // SCROLL
  await page.mouse.wheel(0, 2000);

  // SCROLL fino all'elemento
  await page.locator('footer').scrollIntoViewIfNeeded();

  // TIENI PREMUTO + MOVIMENTO
  const box = await page.locator('.slider-handle').boundingBox();
  await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
  await page.mouse.down();
  await page.mouse.move(box.x + 200, box.y, { steps: 25 });
  await page.mouse.up();

  await browser.close();
})();

Selenium (Python) — con ActionChains

python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://example-shop.com/catalog")
wait = WebDriverWait(driver, 10)

# CLIC (con attesa della cliccabilità)
btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.load-more")))
btn.click()

# SCROLL
driver.execute_script("window.scrollBy(0, 2000)")

# SCROLL fino all'elemento
footer = driver.find_element(By.CSS_SELECTOR, "footer")
driver.execute_script("arguments[0].scrollIntoView({block:'center'})", footer)

# TIENI PREMUTO + MOVIMENTO
handle = driver.find_element(By.CSS_SELECTOR, ".slider-handle")
(ActionChains(driver)
    .click_and_hold(handle)     # tasto premuto
    .move_by_offset(200, 0)     # spostiamo di 200 px a destra
    .pause(0.3)
    .release()                  # rilasciamo
    .perform())

driver.quit()

Selenium (Java)

java
WebDriver driver = new ChromeDriver();
driver.get("https://example-shop.com/catalog");

// CLIC
driver.findElement(By.cssSelector("button.load-more")).click();

// SCROLL
((JavascriptExecutor) driver).executeScript("window.scrollBy(0, 2000)");

// TIENI PREMUTO + MOVIMENTO
WebElement handle = driver.findElement(By.cssSelector(".slider-handle"));
new Actions(driver)
    .clickAndHold(handle)
    .moveByOffset(200, 0)
    .release()
    .perform();

Puppeteer (Node.js)

javascript
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  await page.goto('https://example-shop.com/catalog');

  // CLIC
  await page.click('button.load-more');

  // SCROLL
  await page.evaluate(() => window.scrollBy(0, 2000));

  // SCROLL fino all'elemento
  await page.$eval('footer', el => el.scrollIntoView());

  // TIENI PREMUTO + MOVIMENTO
  const handle = await page.$('.slider-handle');
  const box = await handle.boundingBox();
  await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
  await page.mouse.down();
  await page.mouse.move(box.x + 200, box.y, { steps: 25 });
  await page.mouse.up();

  await browser.close();
})();

Modalità di mascheramento (stealth): eludere il rilevamento dell’automazione

Un browser headless eseguito «così com’è» si distingue facilmente da uno reale. I sistemi anti-bot (Cloudflare, DataDome, PerimeterX/HUMAN, Akamai ecc.) controllano decine di segnali e, se anche solo una parte tradisce l’automazione, arrivano il captcha, il challenge o il blocco. La modalità stealth è un insieme di patch e tecniche che mascherano questi indizi.

Da quali indizi ti individuano

  • navigator.webdriver === true — la spia più evidente, che il browser controllato da WebDriver/CDP attiva automaticamente.
  • Artefatti dell’headless. Assenza di window.chrome, elenco dei plugin vuoto (navigator.plugins), valori anomali di navigator.languages, un renderer WebGL del tipo SwiftShader/Google Inc. invece di una scheda grafica reale.
  • Fingerprinting. Canvas, WebGL, AudioContext e l’insieme dei font generano un’«impronta» stabile dell’ambiente; in un headless di default risulta sospettosamente generica.
  • Impronta TLS/JA3. A livello della connessione HTTP stessa, l’«handshake» di un client Python o Node differisce da quello di Chrome — e viene rilevato ancora prima dell’esecuzione del JS (vale anche per l’approccio 1).
  • Comportamento. Clic istantanei senza movimento del mouse, tempi perfettamente regolari, l’accesso diretto a una pagina interna senza navigazione — tutto questo non è umano.
  • Reputazione dell’IP. Gli intervalli dei data center (AWS, Hetzner ecc.) sono segnalati; gli indirizzi residenziali (residential) e mobili destano meno sospetti.

Strumenti già pronti

  • puppeteer-extra + puppeteer-extra-plugin-stealth (Node) — il pacchetto più noto; nasconde navigator.webdriver, corregge WebGL/plugins/languages e decine di altre «fughe».
  • playwright-extra con lo stesso plugin stealth — l’equivalente per Playwright su Node.
  • undetected-chromedriver (Python, sopra Selenium) — un ChromeDriver patchato che supera molti controlli di Cloudflare. La sua evoluzione è nodriver (senza il protocollo webdriver, puramente tramite CDP).
  • SeleniumBase in modalità UC (--uc) — un wrapper su Selenium con anti-detection integrato.
  • rebrowser-patches — patch di basso livello per il runtime di Puppeteer/Playwright che chiudono fughe di CDP più sottili.

Importante: nessun plugin stealth offre garanzie. I sistemi anti-bot si aggiornano di continuo e ciò che passava ieri domani può essere rilevato. È una «corsa agli armamenti», non una configurazione una tantum.

Esempi

Python — undetected-chromedriver:

python
import undetected_chromedriver as uc

options = uc.ChromeOptions()
options.add_argument("--lang=it-IT")
# per l'anti-detection di solito NON si usa headless, oppure si usa la modalità nuova:
# options.add_argument("--headless=new")

driver = uc.Chrome(options=options)
driver.get("https://example-shop.com/catalog")
print(driver.title)
driver.quit()

Node — puppeteer-extra + stealth:

javascript
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

(async () => {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  // UA credibile e header coerenti
  await page.setUserAgent(
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
    '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
  );
  await page.goto('https://example-shop.com/catalog');
  await browser.close();
})();

Node — playwright-extra + stealth:

javascript
const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth')();

chromium.use(stealth);

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto('https://example-shop.com/catalog');
  await browser.close();
})();

Tecniche manuali (in aggiunta ai plugin o al loro posto)

  • Nascondere webdriver e sistemare l’ambiente. Tramite CDP/init script prima che la pagina carichi:
python
# Playwright (Python): viene eseguito in ogni nuovo documento PRIMA degli script del sito
page.add_init_script(
    "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
)
  • Nuova modalità headless. Nelle versioni recenti di Chrome il flag --headless=new è più vicino a un browser normale rispetto al vecchio headless; a volte conviene eseguire direttamente in modalità non headless sotto un display virtuale (Xvfb).
  • Profilo persistente. Avviare con user_data_dir conserva i cookie e il «riscaldamento» della sessione tra un’esecuzione e l’altra — assomiglia meno a un bot appena creato.
  • Comportamento umano. Pause casuali, movimento del mouse lungo una curva (Bézier), scroll a scatti anziché in un unico salto. Per generare le traiettorie esistono librerie come pyautogui/bezier oppure il parametro steps integrato in mouse.move.
  • Proxy. I proxy residenziali e mobili con rotazione riducono sensibilmente la quota di challenge rispetto agli IP dei data center.

Mascheramento (stealth) per l’approccio 1 (senza browser)

Anche l’intercettazione dell’API viene rilevata — tramite l’impronta TLS del client HTTP. Perché la richiesta sembri, già a livello di «handshake», un Chrome reale, si usano client che falsificano l’impronta TLS:

python
# curl_cffi sa imitare il TLS/JA3 di un browser specifico
from curl_cffi import requests

r = requests.get(
    "https://example-shop.com/api/products?page=1",
    impersonate="chrome124",   # falsifichiamo l'handshake di Chrome 124
)
print(r.json())

Alternative: tls-client (Python/Go), curl-impersonate (un binario di sistema). Questo spesso risolve il problema quando un requests «nudo» riceve un 403 mentre nel browser quella stessa pagina si apre.


Cosa sa fare ciascuna libreria

Lo spartiacque fondamentale: se lo strumento esegue JavaScript e se sa imitare le azioni del mouse. I client HTTP e i parser HTML non fanno né l’una né l’altra cosa — servono solo per i siti statici o per il primo approccio (l’emulazione dell’API).

Libreria Linguaggio Rendering JS Azioni (clic/scroll/drag) Uso
requests / httpx Python No No Client HTTP
aiohttp Python No No HTTP asincrono
BeautifulSoup / lxml Python No No Parsing di HTML
Scrapy Python No (serve il plugin Splash/Playwright) No Framework di crawling
Selenium Python/Java/C#/JS/Ruby Controllo del browser
Playwright Python/JS/.NET/Java Controllo del browser
Puppeteer Node.js Sì (Chromium) Controllo del browser
Cypress JS Sì (ma orientato ai test e2e) Test
axios / fetch / got Node.js No No Client HTTP
cheerio Node.js No No Parsing di HTML (stile jQuery)
Colly Go No No Framework di crawling
chromedp / rod Go Controllo del browser
HtmlUnit Java Parziale/instabile Limitate Browser headless
jsoup Java No No Parsing di HTML

Conclusione breve:

  • Devi solo ripetere una richiesta all’APIrequests/httpx (Python), fetch/got (Node), Colly (Go).
  • Fare il parsing di un HTML già ottenutoBeautifulSoup/lxml, cheerio, jsoup.
  • Ti serve il rendering del JS e le azioni con il mousePlaywright, Selenium, Puppeteer, chromedp/rod.
  • HtmlUnit gestisce un po’ di JS, ma con le SPA moderne inciampa spesso — per un rendering serio si ricorre a Playwright/Selenium.

Caso pratico: monitoraggio di negozi online e scroll infinito

È forse il compito pratico più frequente. Nei cataloghi i prodotti di solito non vengono mostrati tutti in una volta: si applica il lazy loading / scroll infinito — le nuove schede si caricano man mano che scorri (o premendo «Mostra altro»). Una semplice richiesta dell’HTML restituirà solo la prima «porzione».

Ci sono due strade, entrambe molto usate nel monitoraggio di prezzi e assortimento:

Strada A (preferibile): intercettare l’API di paginazione

Durante lo scroll il negozio chiama quasi sempre qualcosa come /api/catalog?page=2&offset=48. Se è così — dimentica il browser e raccogli i dati pagina per pagina in modo diretto (vedi l’approccio 1). È veloce, stabile e scala fino a migliaia di prodotti. È così che si costruisce la maggior parte dei monitoraggi industriali: il browser si usa una sola volta — per esplorare la struttura dell’API e i token, mentre la raccolta vera e propria passa da un client HTTP.

Strada B: rendering + scroll quando l’API è chiusa

Se l’endpoint è protetto da una firma non banale o i dati vengono generati esclusivamente sul client, non resta che scorrere con il browser e raccogliere le schede dal DOM. L’algoritmo: scorriamo verso il basso → aspettiamo il caricamento → contiamo le schede → ripetiamo finché il numero cresce.

python
from playwright.sync_api import sync_playwright

def scrape_catalog(url):
    products = []
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(url)

        prev_count = -1
        stable_rounds = 0

        while stable_rounds < 2:  # 2 scroll «vuoti» consecutivi = siamo arrivati alla fine
            # scorriamo fino in fondo
            page.mouse.wheel(0, 4000)
            page.wait_for_timeout(1500)  # diamo tempo al caricamento

            cards = page.locator(".product-card")
            count = cards.count()

            if count == prev_count:
                stable_rounds += 1
            else:
                stable_rounds = 0
            prev_count = count

        # raccogliamo tutte le schede dopo lo scroll completo
        cards = page.locator(".product-card")
        for i in range(cards.count()):
            card = cards.nth(i)
            products.append({
                "title": card.locator(".title").inner_text(),
                "price": card.locator(".price").inner_text(),
                "url":   card.locator("a").get_attribute("href"),
            })

        browser.close()
    return products

data = scrape_catalog("https://example-shop.com/catalog/phones")
print(f"Prodotti raccolti: {len(data)}")

La stessa tecnica per il pulsante «Mostra altro» — clicchiamo finché il pulsante esiste:

python
while page.locator("button.load-more").count() > 0:
    page.click("button.load-more")
    page.wait_for_timeout(1200)

Ibrido: il meglio dei due mondi

Playwright e Puppeteer sanno ascoltare il traffico di rete. Puoi aprire la pagina nel browser (così passano tutti i token e i controlli anti-bot), ma prelevare i dati non dal DOM, bensì dalle risposte di quella stessa API che il browser chiama durante lo scroll:

python
def handle_response(response):
    if "/api/products" in response.url:
        data = response.json()
        # salviamo il JSON già pronto — non serve fare il parsing dell'HTML
        save(data["items"])

page.on("response", handle_response)
page.goto("https://example-shop.com/catalog")
# poi basta scorrere — i dati «arrivano» da soli al gestore

È spesso l’opzione ottimale per il monitoraggio: la robustezza dell’emulazione del browser + un JSON pulito e strutturato al posto del fragile parsing del markup.

Consigli pratici per il monitoraggio

  • Aspetta i dati, non il tempo. Invece di «dormire 1,5 secondi», usa l’attesa della comparsa di un elemento (wait_for_selector) o della quiete della rete (wait_for_load_state("networkidle")) — è più affidabile e spesso più veloce.
  • Deduplicazione. Con lo scroll infinito alcune schede possono essere lette più volte — raccogli per id/url univoco.
  • Limita la frequenza. Richieste troppo aggressive sovraccaricano il sito e portano rapidamente al blocco. Inserisci ritardi e usa i pool di proxy con cautela e nel rispetto della legge.
  • Metti in cache la fase di esplorazione. Individua la struttura dell’API e i token una sola volta; in produzione esegui una raccolta HTTP leggera e tieni il browser pesante come riserva.

Come scegliere l’approccio

Criterio Intercettazione dell’API Emulazione del browser
Velocità Molto alta Bassa
Consumo di risorse Minimo Alto (CPU/RAM)
Complessità di configurazione Maggiore (reverse engineering dei token) Minore (tutto «come un utente»)
Robustezza ai cambi di markup Alta (dipende dall’API) Media (dipende dai selettori)
Superare token/firme lato client Difficile Automatico
Scalabilità sul volume Eccellente Limitata

Regola pratica: verifica sempre prima se basta l’intercettazione dell’API — è più veloce, più economica e più stabile. Passa all’emulazione del browser solo quando l’API è nascosta dietro crittografia lato client, protetta da una logica anti-bot complessa o quando devi riprodurre un’interazione non banale (drag-and-drop, slider, form a più passaggi).