CMS e piattaforme 14 min di lettura

Scraping dei tassi di cambio: API, fonti, codice e archiviazione dei dati

Raccolta dei tassi di cambio: API ufficiali delle banche centrali, aggregatori, scraping dei siti bancari e archiviazione dello storico per l’analisi.

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

Il tasso di cambio è una di quelle grandezze che sembrano innocue («solo un numero») ma che, maneggiate senza cura, si trasformano in una fonte di errori difficili da rintracciare: scarti di centesimi, un nominale applicato male, il tasso «perso» del fine settimana, l’errore di arrotondamento accumulato nel report annuale. Questo articolo è un’analisi pratica di dove prendere i tassi di cambio (la Banca Centrale Europea, le altre banche centrali e i servizi internazionali), di come estrarli in cinque linguaggi, del tipo di dato in cui conservarli e del perché i tassi delle banche commerciali siano una storia a parte, con i loro aggregatori.


1. Da dove vengono i tassi di cambio

Conviene distinguere fin dall’inizio due tipi di cambio radicalmente diversi:

Il tasso ufficiale o di riferimento della banca centrale. Un unico valore per data, senza acquisto/vendita. È il tasso «contabile»: con questo si calcolano imposte, dazi doganali, contabilità e contratti. È stabile, viene pubblicato secondo un calendario (di solito una volta al giorno) e quasi tutte le banche centrali offrono una fonte gratuita e leggibile dalle macchine.

I tassi delle banche commerciali e dei cambiavalute. Ogni banca applica il proprio tasso di acquisto e di vendita con il suo spread; cambia nel corso della giornata, differisce da quello ufficiale e dipende dall’istituto, dalla città, dall’importo e dal fatto che l’operazione sia in contanti o tramite bonifico. Qui una fonte unica non esiste: se ne occupano gli aggregatori (sezione 6).

Per la maggior parte dei compiti (contabilità, prezzi multivaluta, convertitori) basta il tasso ufficiale di riferimento. Se il compito è «mostrare all’utente dove conviene comprare dollari», servono i tassi bancari.


2. La Banca Centrale Europea e le altre banche centrali

Quasi tutte le banche centrali pubblicano i dati gratis e senza chiave. I formati variano: a volte un JSON curato, a volte XML, a volte CSV. Per l’euro la fonte primaria è la BCE; anche la Banca d’Italia ripubblica ogni giorno gli stessi cambi di riferimento sul proprio sito.

Fonte Cosa pubblica Accesso Formato
BCE — tassi di riferimento dell’euro ~30 valute rispetto all’EUR, un valore per giorno lavorativo https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml, senza chiave XML
BCE — storico eurofxref-hist-90d.xml (ultimi 90 giorni) e eurofxref-hist.xml (serie dal 1999) senza chiave XML/CSV
NBP (Banca Nazionale di Polonia) tabelle A/B (tasso medio) e C (acquisto/vendita) api.nbp.pl, senza chiave JSON/XML
Banco de México tasso di cambio FIX e serie storiche API SIE, con token gratuito JSON
Bank of Canada serie di tassi di cambio API Valet, senza chiave JSON/CSV

Gli endpoint sono riportati così com’erano al momento della stesura. Le banche di tanto in tanto cambiano la propria infrastruttura, quindi prima di andare in produzione conviene verificarli sulla pagina ufficiale per sviluppatori di ciascun istituto.

A cosa fare attenzione nei dati delle banche centrali:

  • Il nominale (nominal / scale / quant). Il tasso non è sempre pubblicato per 1 unità: molte fonti quotano le valute di piccolo taglio per 10, 100 o 1000 unità (capita spesso con lo yen giapponese o il fiorino ungherese). Ignorare questo campo è l’errore classico che restituisce un tasso gonfiato di 100 volte.
  • La codifica. Non tutti i feed arrivano in UTF-8: ci sono ancora fonti che pubblicano XML o CSV in codifiche legacy (windows-1252, ISO-8859-1). Leggile «così come sono» e otterrai caratteri corrotti nei nomi.
  • Il separatore decimale. Alcune fonti europee separano la parte decimale con la virgola (74,1234) invece che con il punto, soprattutto nelle esportazioni CSV.
  • Il calendario. La BCE pubblica i tassi di riferimento una volta per giorno lavorativo, verso le 16:00 CET; nei weekend e nei giorni festivi non c’è un valore nuovo. Altre banche centrali fissano il tasso il giorno prima o dopo la sessione di mercato, ognuna con le proprie regole.

3. API internazionali

Quando servono cross rate, molte valute o un’unica fonte «tutto rispetto a USD/EUR», i servizi internazionali sono più comodi.

Servizio Fonte dei dati Chiave Limite gratuito Note
Frankfurter (api.frankfurter.dev) BCE non serve nessun limite mensile (solo protezione anti-abuso) ~30 valute, storico dal 1999, open source, si può fare self-host
BCE diretta (file eurofxref-daily.xml su ecb.europa.eu) BCE non serve XML della fonte primaria, tutto rispetto all’EUR
NBP (Polonia, api.nbp.pl) Banca Nazionale di Polonia non serve la tabella C include acquisto/vendita
exchangerate-api.com mix di più banche centrali necessaria ~1500/mese 160+ valute, midpoint mediato
Open Exchange Rates aggregato necessaria 1000/mese nel piano gratuito solo base USD
Fixer.io / currencylayer (apilayer) BCE e altri necessaria ~100/mese
exchangerate.host (apilayer) aggregato necessaria piano gratuito limitato conversione e storico; richiede la chiave dal passaggio ad apilayer
currencyapi.com aggregato necessaria ~300/mese include le cripto
Twelve Data / Alpha Vantage quotazioni di mercato necessaria limiti forex intraday, non tasso «contabile»

La sfumatura chiave degli aggregatori internazionali: i loro tassi sono un midpoint indicativo (senza spread). Vanno benissimo per convertire i prezzi in modo orientativo nell’e-commerce o per le dashboard, ma non vanno bene per il trading forex reale né per calcolare l’importo esatto che la banca addebiterà in una conversione. Frankfurter e la BCE, inoltre, pubblicano i tassi solo nei giorni lavorativi: una richiesta per il 1° gennaio restituisce il tasso dell’ultimo giorno lavorativo, e nella risposta c’è il campo date con la data reale del tasso — fai riferimento a quel campo, non alla data richiesta.


4. Cinque soluzioni in linguaggi diversi

Per mostrare la varietà di fonti e linguaggi, ogni esempio punta a una fonte o a un taglio di dati diverso. In tutti l’accento cade deliberatamente su due cose: rispettare il nominale (o la base di quotazione) e conservare il valore senza float binario.

4.1. PHP — BCE (XML, namespace, BCMath)

php
<?php
declare(strict_types=1);

/**
 * Restituisce i tassi di riferimento della BCE rispetto all'euro come stringhe.
 * Stringhe + BCMath: per non perdere precisione con i float.
 */
function fetchEcbRates(): array
{
    $raw = file_get_contents('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml');
    if ($raw === false) {
        throw new RuntimeException('Impossibile ottenere i dati della BCE');
    }

    $xml = simplexml_load_string($raw);
    if ($xml === false) {
        throw new RuntimeException('Errore nel parsing XML');
    }

    // Il feed usa un namespace di default: va registrato per XPath
    $xml->registerXPathNamespace('e', 'http://www.ecb.int/vocabulary/2002-08-01/eurofxref');

    $rates = [];
    foreach ($xml->xpath('//e:Cube[@currency]') as $cube) {
        $code  = (string) $cube['currency'];   // USD, GBP, JPY...
        $value = (string) $cube['rate'];       // unità di valuta per 1 EUR
        $rates[$code] = $value;                // salviamo la stringa così com'è
    }
    return $rates;
}

$rates = fetchEcbRates();
echo "EUR->USD: {$rates['USD']}\n";
// Cross rate USD->JPY con aritmetica di stringhe (niente float)
echo 'USD->JPY: ' . bcdiv($rates['JPY'], $rates['USD'], 6) . "\n";

Qui gli aspetti da notare sono due. Primo, il namespace: senza registerXPathNamespace la query XPath restituisce un risultato vuoto anche se i dati ci sono. Secondo, bcdiv: la BCE quota tutto rispetto all’euro, quindi qualsiasi cross rate nasce da una divisione, e farla con l’aritmetica di stringhe evita di passare per i float. Richiede l’estensione bcmath.

4.2. Python — NBP (JSON, Decimal)

python
from decimal import Decimal, getcontext
import requests

getcontext().prec = 28  # margine di precisione abbondante


def fetch_nbp_rates() -> dict[str, Decimal]:
    """Tassi medi ufficiali della Banca Nazionale di Polonia (tipo Decimal)."""
    resp = requests.get(
        "https://api.nbp.pl/api/exchangerates/tables/A/",
        params={"format": "json"},
        timeout=10,
    )
    resp.raise_for_status()

    table = resp.json()[0]  # la tabella A arriva come lista con un solo elemento
    rates: dict[str, Decimal] = {}
    for item in table["rates"]:
        code = item["code"]                      # USD, EUR, CHF...
        rates[code] = Decimal(str(item["mid"]))  # tasso medio della tabella A
    return rates


if __name__ == "__main__":
    rates = fetch_nbp_rates()
    print(f"USD: {rates['USD']:.4f} PLN")
    print(f"EUR: {rates['EUR']:.4f} PLN")

Il punto essenziale è Decimal(str(value)), non Decimal(value). Se il numero è già arrivato come float, avvolgerlo in str fissa esattamente la rappresentazione decimale che c’era nel JSON. La tabella C della stessa API, tra l’altro, pubblica i tassi di acquisto e vendita (bid/ask): utile quando oltre al tasso medio ti serve lo spread.

4.3. JavaScript / Node.js — ExchangeRate-API (JSON, fetch nativo)

javascript
// Node 18+: fetch è integrato
async function fetchOpenErApiRates() {
  const res = await fetch("https://open.er-api.com/v6/latest/EUR");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  const data = await res.json(); // attenzione: qui i numeri JSON sono già double
  if (data.result !== "success") throw new Error("Risposta inattesa dall'API");

  return {
    base: data.base_code,               // "EUR"
    updated: data.time_last_update_utc, // data reale dell'ultimo aggiornamento
    rates: data.rates,                  // { USD: ..., CHF: ..., ... }
  };
}

fetchOpenErApiRates().then(({ base, updated, rates }) => {
  console.log(`Base ${base}, aggiornato: ${updated}`);
  console.log(`EUR->USD: ${rates.USD}`);
  console.log(`EUR->CHF: ${rates.CHF}`);
});

È l’endpoint aperto (senza chiave, con attribuzione al provider) di exchangerate-api.com, il servizio già visto in tabella. JavaScript non ha un tipo decimale nativo: number è un double IEEE 754 e, nel momento in cui res.json() fa il parsing della risposta, i valori vivono già in quel formato. Per mostrare un tasso di cambio basta e avanza; per i calcoli monetari si usa una libreria come decimal.js o big.js alimentata con stringhe e, se serve l’esattezza bit per bit, si conserva il corpo grezzo della risposta (res.text()) e si estrae il letterale da lì.

4.4. Go — storico della BCE (XML, tipizzazione rigorosa)

go
package main

import (
    "encoding/xml"
    "fmt"
    "io"
    "net/http"
    "time"
)

// Strutture per l'XML della BCE: elemento Cube annidato su tre livelli
type Envelope struct {
    Days []Day `xml:"Cube>Cube"`
}

type Day struct {
    Date  string `xml:"time,attr"` // data del tasso: 2025-03-20
    Rates []Rate `xml:"Cube"`
}

type Rate struct {
    Currency string `xml:"currency,attr"` // USD, GBP...
    Value    string `xml:"rate,attr"`     // unità per 1 EUR
}

func fetchEcbHistory() ([]Day, error) {
    url := "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml"

    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    var env Envelope
    if err := xml.Unmarshal(body, &env); err != nil {
        return nil, err
    }
    return env.Days, nil
}

func main() {
    days, err := fetchEcbHistory()
    if err != nil {
        panic(err)
    }
    latest := days[0] // il feed arriva ordinato dal giorno più recente al più vecchio
    fmt.Printf("Tassi del %s:\n", latest.Date)
    for _, r := range latest.Rates {
        if r.Currency == "USD" || r.Currency == "GBP" {
            // conserviamo la stringa grezza; per l'aritmetica, shopspring/decimal
            fmt.Printf("EUR->%s: %s\n", r.Currency, r.Value)
        }
    }
}

Go non ha un tipo decimal nella libreria standard, quindi il tasso si conserva come stringa e per i calcoli si ricorre a github.com/shopspring/decimal. Il file dei 90 giorni completa l’esempio PHP con il taglio «storico»: con una sola richiesta ottieni l’intera serie recente, ideale per popolare il database la prima volta. Tieni presente che la serie non contiene righe per i weekend e i festivi: quando cerchi «il tasso del giorno X» devi essere pronto a risalire al giorno lavorativo precedente.

4.5. C# / .NET — Frankfurter (BCE, fonte internazionale, decimal)

c#
using System.Net.Http.Json;
using System.Text.Json.Serialization;

public record FrankfurterResponse(
    [property: JsonPropertyName("base")] string Base,
    [property: JsonPropertyName("date")] string Date,
    [property: JsonPropertyName("rates")] Dictionary<string, decimal> Rates
);

public static class CurrencyClient
{
    private static readonly HttpClient Http = new();

    public static async Task<FrankfurterResponse> FetchEcbRatesAsync(string baseCcy = "EUR")
    {
        var url = $"https://api.frankfurter.dev/v1/latest?base={baseCcy}";
        return await Http.GetFromJsonAsync<FrankfurterResponse>(url)
               ?? throw new InvalidOperationException("Risposta vuota da Frankfurter");
    }
}

class Program
{
    static async Task Main()
    {
        var data = await CurrencyClient.FetchEcbRatesAsync("USD");
        Console.WriteLine($"Data del tasso: {data.Date}"); // data reale della BCE
        Console.WriteLine($"USD->EUR: {data.Rates["EUR"]}");
        Console.WriteLine($"USD->GBP: {data.Rates["GBP"]}");
    }
}

System.Text.Json deserializza i numeri JSON direttamente in decimal (leggendo il letterale testuale), quindi la precisione non si perde. In .NET, decimal è il tipo giusto sia per i tassi di cambio sia per il denaro.


5. In quale tipo di dato conservare il tasso di cambio

È probabilmente la grande questione tecnica del tema — e quella dove si sbaglia più spesso.

Perché non float/double

I numeri binari in virgola mobile (IEEE 754) non possono rappresentare con esattezza la maggior parte delle frazioni decimali. 0.1 + 0.2 non è uguale a 0.3. Su un singolo tasso non si nota, ma moltiplicando per gli importi, riconvertendo più volte e aggregando per periodo gli errori si accumulano — e nel report finanziario compaiono inspiegabili differenze di centesimi che non passano la riconciliazione. Per denaro e tassi di cambio, float/double sono vietati.

Cosa usare

Livello Scelta corretta
Database DECIMAL / NUMERIC con precision e scale fissi
Python decimal.Decimal
PHP BCMath / stringhe (o una libreria Money)
Java / C# BigDecimal / decimal
Go github.com/shopspring/decimal
JavaScript decimal.js / big.js (conservare come stringa)

Quante cifre decimali

I tassi vengono pubblicati di solito con 4–6 cifre decimali, ma le valute con valori unitari molto grandi (IDR, COP, CLP) producono parti intere voluminose. Un compromesso sicuro per il database è NUMERIC(20, 6); per il «tasso per 1 unità» normalizzato a volte si riserva più precisione, ad esempio NUMERIC(24, 10), perché la divisione per il nominale non perda cifre.

Cosa salvare oltre al numero in sé

Un tasso di cambio senza contesto non vale nulla. Il record minimo utile contiene la fonte, entrambe le valute, il nominale, la data di validità e il tipo di tasso:

sql
CREATE TABLE exchange_rates (
    id            BIGSERIAL PRIMARY KEY,
    source        VARCHAR(32)   NOT NULL,   -- 'ECB', 'NBP', 'BANXICO', 'FRANKFURTER'
    base_ccy      CHAR(3)       NOT NULL,   -- valuta in cui è espresso il tasso: EUR, PLN, MXN
    quote_ccy     CHAR(3)       NOT NULL,   -- valuta quotata: USD, GBP...
    nominal       INTEGER       NOT NULL DEFAULT 1,
    rate          NUMERIC(20,6) NOT NULL,   -- tasso per `nominal` unità (come nella fonte)
    rate_per_one  NUMERIC(24,10) NOT NULL,  -- tasso normalizzato per 1 unità
    rate_type     VARCHAR(8)    NOT NULL DEFAULT 'official', -- official | buy | sell
    effective_date DATE         NOT NULL,   -- data in cui il tasso è in vigore
    fetched_at    TIMESTAMPTZ   NOT NULL DEFAULT now(),
    UNIQUE (source, base_ccy, quote_ccy, rate_type, effective_date)
);

Conviene salvare sia il rate «grezzo» (così come lo ha consegnato la fonte, con il suo nominale) sia il rate_per_one normalizzato: il primo serve per la riconciliazione con la fonte primaria, il secondo per i calcoli. I codici valuta seguono lo standard ISO 4217 (tre lettere): elimina alla radice il problema delle grafie diverse.

Una nota a parte sugli importi monetari (non i tassi): spesso si archiviano come interi nelle unità minori — centesimi, cent. Cioè 19.99 USD = 1999. Questo elimina del tutto l’aritmetica frazionaria. Ma i tassi di cambio non si conservano così: a loro serve la frazione decimale.


6. Tassi bancari e aggregatori

Il tasso ufficiale di riferimento è uno solo. Ma chi va a cambiare valuta vede numeri molto diversi: ogni banca ha il suo tasso di acquisto e di vendita, con spread, che cambia nel corso della giornata. Riunire in un unico posto i tassi di decine di istituti è esattamente il lavoro degli aggregatori.

Esempi di fonti di questo ecosistema:

  • Riferimenti di mercato: XE, Wise o la stessa ricerca di Google mostrano il tasso medio di mercato — utile come riferimento, non come prezzo allo sportello.
  • Comparatori di rimesse: servizi come Monito confrontano commissioni e tassi effettivi di diversi provider di trasferimento di denaro.
  • Portali finanziari locali: nei mercati con più tassi di cambio simultanei il monitoraggio lo fanno i media; il caso classico è l’Argentina, dove portali come Ámbito o DolarHoy pubblicano ogni giorno il dollaro ufficiale, il «blue» e i tassi finanziari. In Italia i cambi di riferimento si trovano nelle pagine dei mercati dei quotidiani finanziari come Il Sole 24 Ore.
  • Siti di banche e cambiavalute: il tasso allo sportello di ogni istituto è pubblicato sul suo sito e, quando un’API non c’è, si estrae dall’HTML con lo scraping.

Sfumatura tecnica: molti aggregatori non offrono un’API aperta e i dati vanno estratti dall’HTML. Qui è fondamentale giocare pulito: rispettare il robots.txt e le condizioni d’uso, non bombardare il sito di richieste (rate limiting, cache) e, se possibile, citare la fonte. Alcuni servizi un’API ce l’hanno, ma a pagamento. I dati delle banche centrali, invece, di norma si possono riutilizzare liberamente.

Se costruisci il tuo aggregatore, un’architettura ragionevole è un insieme di raccoglitori indipendenti (uno per fonte) → uno strato di normalizzazione (codici ISO 4217, tasso per unità, separazione acquisto/vendita) → un archivio unico → una tua API sopra. Nel modello dei dati, a differenza dei tassi ufficiali, compaiono dimensioni obbligatorie: l’istituto, il tipo di operazione (acquisto/vendita, contanti/bonifico), a volte la città e l’importo, e quasi sempre l’orario esatto di rilevazione, perché questi tassi vivono minuti.


7. Consigli pratici

  • Usa la cache. Le banche centrali aggiornano il tasso una volta al giorno (di rado due). Interrogare l’API più spesso di quanto cambino i dati è inutile e dannoso: metti in cache lato applicazione.
  • Considera weekend e festivi. Nei giorni non lavorativi non viene pubblicato un tasso nuovo: di solito viene restituito l’ultimo. Guarda sempre la data del tasso nella risposta, non quella richiesta.
  • Prevedi un fallback. Qualsiasi fonte può andare giù. Conviene averne una di riserva (ad esempio un’API internazionale come Frankfurter o un’altra fonte ufficiale) e timeout con retry a intervalli esponenziali.
  • Normalizza in ingresso. Ricodificare i feed legacy, sostituire la virgola decimale con il punto, dividere per il nominale, portare i codici a ISO 4217: tutto questo è meglio farlo al caricamento, e salvare nel database solo dati puliti.
  • Monitora le anomalie. Un salto brusco del tasso, di più volte, è più spesso segno di un errore della fonte o dell’estrazione (il nominale dimenticato!) che di un evento reale. Il semplice controllo «scostamento rispetto a ieri non oltre N%» intercetta la maggior parte di questi casi.

8. Dove si applica

  • E-commerce — prezzi multivaluta, listino localizzato per l’acquirente.
  • Contabilità — conversione delle operazioni al tasso ufficiale della data, imposte, dazi doganali.
  • Fintech, wallet, scambio P2P — conversione e visualizzazione dei saldi.
  • Analisi e dashboard di BI — ricondurre i ricavi multivaluta a una sola moneta.
  • Convertitori e servizi di viaggio — ricalcolo rapido per l’utente.
  • Contratti e fatturazione — fissazione del tasso alla data di emissione della fattura.

In sintesi

Lo scraping dei tassi di cambio è un compito in cui l’80% della difficoltà non sta nella richiesta HTTP ma nei dettagli: il nominale, la codifica, il separatore decimale, i giorni festivi e — soprattutto — la scelta di un tipo di dato decimale al posto del float lungo tutto il percorso, dall’estrattore al database. Per i tassi ufficiali esiste quasi sempre una fonte gratuita della stessa banca centrale; per quelli bancari servono aggregatori e uno scraping accurato e rispettoso della fonte. Aggiungi la cache e la normalizzazione in ingresso, e otterrai dati di cui potersi fidare nei calcoli finanziari.