In questa guida vediamo come anonimizzare i dati in Excel prima di condividere un file, sostituendo i valori originali con dati sintetici ma mantenendo la struttura della tabella.

È una situazione piuttosto comune quando dobbiamo chiedere aiuto a un collega, inviare un file a un consulente oppure utilizzare strumenti come ChatGPT o Copilot senza poter condividere tutti i dati reali. Potremmo semplicemente cancellare i valori oppure sostituirli con XXX, ma in questo modo perderemmo anche informazioni utili per analizzare correttamente il file.
Per questo ho creato SANITIZE.xlsm, un file Excel separato che legge una Tabella Excel, genera nuovi valori sintetici e crea un nuovo file .xlsx. Possiamo scegliere se anonimizzare tutte le colonne oppure soltanto quelle che contengono informazioni che non vogliamo condividere.
Il workbook originale viene aperto in sola lettura e non viene modificato.
Alcune specifiche sui file .xlsm, necessarie se non conosci questo formato:
- XLSM vuol dire che il file ha delle macro. Le macro possono essere malevole, quindi alcune organizzazioni le bloccano di default. In File > Opzioni > Centro protezione puoi controllare le opzioni di sicurezza relative alla macro
- Scaricare un file con macro potrebbe indicare che alcune funzionalità sono bloccate, quindi ti consiglio di selezionare il file > tasto destro > proprietà e controllare che non ci siano blocchi specifici
L'intero codice della macro lo trovi in questa guida - quindi, se non ti fidi, puoi copiarlo e rivederlo pezzo per pezzo prima di utilizzarlo. Un controllo aggiuntivo non fa mai male.
Un piccolo disclaimer - nella guida uso sanificare, sanitizzare, anonimizzare in maniera praticamente intercambiabile. A livello tecnico, i termini più corretti sono relativi alla generazione dei dati sintetici e ciò che descriviamo è più ascrivibile alla pseduo-anonimizzazione dei dati, ma per semplicità ho tenuto così, credo sia più logico almeno per una discussione "alta" sul tema senza dover andare a parlare di DPO, GDPR o simili. Inoltre, la macro è relativa alla versione italiana di Excel, anche se può essere adattata visto che il codice è liberamente scaricabile.
Perché anonimizzare i dati in Excel
Partiamo da una tabella molto semplice.
| Cliente | Regione | Prodotto | Fatturato | Variazione |
|---|---|---|---|---|
| Rossi SRL | Lombardia | Prodotto A | 12.500 € | 10% |
| Bianchi SPA | Lazio | Prodotto B | -8.300 € | -5% |
| Rossi SRL | Lombardia | Prodotto C | 9.400 € | 10% |
Se dovessi inviare questo file per chiedere aiuto su una formula o su una query, probabilmente non avrei alcun motivo per condividere il vero nome dei clienti. Potrei invece voler mantenere Regione, Prodotto e altri dati che non considero sensibili, perché possono essere utili per riprodurre correttamente il problema.
Una prima possibilità sarebbe sostituire tutto con XXX, ma perderemmo quasi completamente la struttura del dataset. Non sapremmo più, ad esempio, che Rossi SRL compare due volte, non distingueremmo importi positivi e negativi e non potremmo più utilizzare alcuni campi originali che magari non avevano alcuna necessità di essere modificati.
Con SANITIZE possiamo invece decidere, ad esempio, di anonimizzare soltanto Cliente e Fatturato.
| Cliente | Regione | Prodotto | Fatturato | Variazione |
|---|---|---|---|---|
| CLI-A7K2-P9Q4 | Lombardia | Prodotto A | 18.724,31 € | 10% |
| CLI-H5N8-X2R1 | Lazio | Prodotto B | -6.381,15 € | -5% |
| CLI-A7K2-P9Q4 | Lombardia | Prodotto C | 27.412,80 € | 10% |
I dati che abbiamo deciso di proteggere sono stati sostituiti, mentre le altre colonne sono rimaste invariate.
Come funziona SANITIZE

Per evitare di inserire la macro in ogni workbook da elaborare, utilizziamo un file separato chiamato SANITIZE.xlsm. Questo file contiene il codice VBA e può essere riutilizzato ogni volta che dobbiamo preparare una tabella da condividere.
Supponiamo di avere un file chiamato Vendite 2026.xlsx con una Tabella Excel chiamata TBL_VENDITE. Avviando SANITIZE possiamo creare automaticamente Vendite 2026_SANITIZED.xlsx, contenente un foglio e una Tabella Excel chiamati TBL_VENDITE_SANITIZED.
Il flusso è semplice: apriamo SANITIZE, selezioniamo il file sorgente e scegliamo la tabella da elaborare. A quel punto possiamo decidere se anonimizzare l'intera tabella oppure soltanto alcune colonne. Infine scegliamo dove salvare il risultato e viene creato un nuovo file .xlsx.
Il vantaggio di questo approccio è che non stiamo modificando il workbook originale e non stiamo nemmeno creando una sua copia completa. SANITIZE costruisce un nuovo file contenente soltanto la tabella selezionata.
Come utilizzare SANITIZE per anonimizzare i dati
Per utilizzare la macro è necessario che i dati siano contenuti in una Tabella Excel. Se stai lavorando con un normale intervallo puoi trasformarlo rapidamente in tabella selezionando i dati e utilizzando CTRL+T.
- Apri SANITIZE.xlsm.
- Abilita le macro.
- Avvia SanificaFileExcel.
- Seleziona il file da elaborare.
- Se il file contiene più Tabelle Excel, seleziona una cella della tabella interessata.
- Scegli se anonimizzare tutte le colonne oppure soltanto alcune.
- Se necessario, seleziona nel foglio le colonne da anonimizzare.
- Scegli il nome e la posizione del nuovo file.
- Controlla il risultato prima di condividerlo.
Se il file contiene una sola Tabella Excel, questa viene utilizzata automaticamente. Prima dell'elaborazione compare inoltre la finestra Salva con nome: se il file originale si chiama Vendite 2026.xlsm, il nome proposto sarà automaticamente Vendite 2026_SANITIZED.xlsx.
Se annulli questa finestra, la procedura termina senza creare alcun nuovo workbook. Puoi scaricare il file utilizzato in questa guida e riutilizzarlo per anonimizzare le tue Tabelle Excel.
Ti segnalo inoltre un disclaimer per chiarire meglio che cosa fa SANITIZE - ogni utilizzo richiede un controllo umano successivo.
SANITIZE è uno strumento tecnico per sostituire i valori di una Tabella Excel con dati sintetici e ridurre la presenza di informazioni originali prima della condivisione. Non garantisce che il file risultante sia anonimo ai sensi del GDPR o di altre normative sulla protezione dei dati.
La procedura mantiene intenzionalmente alcune caratteristiche del dataset, come valori ripetuti, struttura delle colonne e segno dei numeri; inoltre, se scegli di anonimizzare solo alcune colonne, quelle escluse vengono copiate con i valori originali.
Prima di condividere il file generato, verifica sempre manualmente che non contenga dati personali, riservati o altre informazioni che non possono essere comunicate al destinatario. La scelta dei dati da trattare e la valutazione dell'adeguatezza della sanitizzazione rimangono responsabilità dell'utente.
Lo strumento è fornito a scopo pratico e informativo e non costituisce una soluzione certificata di anonimizzazione né una consulenza legale o in materia di protezione dei dati.
Dopo aver scaricato il file, aprilo con Excel desktop e abilita le macro. Excel Online non supporta l'esecuzione di codice VBA.
CONSULENZA EXCEL
Vuoi applicare quanto visto ai tuoi dati e processi aziendali?
Se il tuo caso richiede più di un esempio standard, posso aiutarti a progettare o ottimizzare il file e automatizzare le attività ripetitive.

Marco Filocamo · Microsoft Excel MVP
Cerchi invece formazione Excel per il tuo team? Scopri la formazione aziendale →
Come anonimizzare i dati Excel con un esempio pratico
Prendiamo i salari della F1 per la stagione 2026-2027. Le scuderie di F1 hanno sempre due piloti in griglia e il dataset creato è molto semplice: avremo nome, cognome, scuderia e ricavi potenziali. Niente di complesso, ma utile per il nostro scopo.

Perchè può essere utile, un esempio simile? Perchè abbiamo, ad esempio:
- Max | Verstappen | Red Bull | 81
- Lewis | Hamilton | Ferrari | 70,5
- Lando | Norris | McLaren | 69,5
- Charles | Leclerc | Ferrari | 50
Se dovessi fare analisi su questi dati, vorrei solo sapere quali sono le intestazioni delle colonne e i tipi di dati inseriti. Non è per forza necessario sapere che Leclerc abbia dei guadagni potenziali di 20,5 milioni superiori rispetto ad Hamilton - ma io potrei impostare tutto il mio file finale di analisi, usando query e formule, usando dati fittizi da riportare poi in un sistema reale.
Se vuoi seguire passo passo la guida, scarica il file dei piloti qui - altrimenti, prova su un tuo file!
Assumiamo di voler scoprire la media di salario per ciascun team.

In questo caso creo una formula del tipo:
=RAGGRUPPAPER(TBL_F1_DRIVERS[TEAM];TBL_F1_DRIVERS[POTENTIAL_EARNINGS];MEDIA;0;0;-2)
In pratica sto raggruppando i singoli team, facendo la media per guadagni (potential_earnings) e ordinandoli dal team con la media più alta a quello con la media più bassa.
Nel dataset, però, sappiamo chi prende di più e chi di meno - possiamo vedere molto chiaramente che Hamilton prende più di Leclerc. Se avessimo dei dati fittizi, potremmo fare lo stesso calcolo con la stessa formula - certo, non sarebbe interpretabile, ma la logica sarebbe perfettamente sovrapponibile al file iniziale con i salari reali. Anche se avessimo Hamilton con 200 milioni e Leclerc con 700 milioni, la formula non cambierebbe - cambierebbe il totale, ma potremmo mantenere la stessa struttura dei dati, inserendo poi ex post quelli reali.
Il flusso, in azienda, potrebbe essere:
- Devo fare eseguire dei calcoli su un dataset sensibile. Mettiamo di avere i salari aziendali e di dover fare analisi sulla media per reparto
- Voglio anonimizzare tutti i dati o parte di essi. Qui, per esempio, potremmo voler nascondere i nomi dei piloti, ma tenere i nomi delle scuderie e relativi salari. In un caso aziendale, anonimizzare qualunque informazione dei dipendenti è sempre la strada migliore
- Ottenuto il file finale, posso eseguire una serie di calcoli con le formule sui dati fittizi. Le stesse formule, potenzialmente, potrei usarle anche sul file iniziale - ma se mi interessano solo aggregazioni, non ho problemi a condividere i file anche con terzi, se ho passato il test di anonimizzazione
In sintesi - la formula non cambia, cambiano i dati inseriti. Se i dati sono sensibili, in particolare, questo è un tema cruciale.
Per chiarire che i dati non sono quelli originali, il nome della tabella e del file avranno il suffisso _sanitized. Vediamo nel pratico come funziona.

Aprendo SANITIZE, puoi cliccare sul grande pulsante che trovi e avviare la macro.

Prendiamo il file precedente, che si chiama F1_DRIVERS. Chiaramente puoi provare con il file che preferisci, facendo sempre attenzione a fare un controllo finale prima di condividere il file con dati sintetici.

Puoi scegliere tre opzioni:
- Yes = sì, anonimizzo tutte le colonne
- No = no, solo alcune
- Cancel = annulla l'operazione
Puoi modificare la macro per mettere tutto in italiano, se preferisci!

Di default, la macro creerà un nuovo file chiamato fileiniziale_SANITIZED, per chiarire che il file ha attraversato un processo di modifica.

Al termine dell'operazione, la macro fa un recap delle operazioni effettuate - in questo caso, ventidue righe per quattro colonne (88 celle) sono state modificate.

Come noti, è ora impossibile capire quali siano i piloti più pagati o a quali scuderie appartengano, dato che tutte le informazioni sono state sostituite con dati sintetici.
C'è un dettaglio importante, però. I dati originali sono sì sostituiti, ma coerentemente - se un testo viene modificato, il risultato sarà uguale. Questo elemento toglie una parte di vera anonimizzazione, dato che, se avessimo 50 transazioni di un cliente e 2 di un altro, sarebbe facilissimo capire chi è chi.
In un ambito aziendale, trovo corretto questo ragionamento perchè altrimenti sarebbe impossibile fare aggregazioni o calcoli sui dati fittizi - se ogni cella è completamente indipendente, non è possibile fare operazioni. Tienilo però a mente per i tuoi casi e, se vuoi, puoi sempre modificare la macro come preferisci.

Qui vediamo, ad esempio che un team è stato cambiato con TEA-8QP2-PZL6. Qual è il team? Non lo sappiamo ma, come noti, il valore è restituito due volte. Ciò è coerente con il nostro utilizzo, visto che potremo fare calcoli, anche se su dati fittizi.

Nello specifico, TEA-8QP2-PZL6 è il valore di Ferrari. Questa informazione puoi ricavarla, però, solo andando a controllare il file precedente, altrimenti non sarebbe possibile scoprirla in autonomia, dato che tutti i team hanno effettivamente due piloti a roster.
Come anonimizzare solo parte di un file Excel
Vediamo un caso diverso, nel quale vogliamo usare lo stesso dataset ma anonimizzare solo alcune colonne. Vogliamo mantenere invariati i valori di team e guadagni, nascondendo però nomi e cognomi dei piloti. Vediamo come fare.

Il flusso è identico al precedente. Aprendo SANITIZE, arriverai poi alla scelta del numero di colonne da modificare e in questo caso dovrai dire No.

A questo punto apriamo il file precedente, selezionando solo NAME e SURNAME, cioè nome e cognome. La nostra aspettativa è che questi valori diventino impossibile da comprendere, con il nome di scuderia (TEAM) e guadagni (POTENTIAL_EARNINGS) invariati.

La macro prende quindi i nostri dati, indicando chiaramente quante siano le colonne da modificare e da tenere invariate.

Salviamo il file, come nel caso precedente - ma stavolta, ci aspettiamo un altro output, perchè dobbiamo poter fare calcoli su scuderia e guadagni.

Vediamo come la macro abbia fatto ciò che volevamo! Abbiamo infatti una lista di piloti, ma non possiamo determinare chi sia il più pagato nelle singole scuderie.

Possiamo quindi eseguire lo stesso identico calcolo precedente, cambiando solo il nome della tabella:
=RAGGRUPPAPER(TBL_F1_DRIVERS_SANITIZED[TEAM];TBL_F1_DRIVERS_SANITIZED[POTENTIAL_EARNINGS];MEDIA;0;0;-2)
Sappiamo che, in ordine, Ferrari, McLaren e Red Bull sono quelle con i piloti con maggior potenziale di guadagno, in media. Impossibile sarebbe però sapere chi sia Leclerc e Hamilton, in questo caso.
In azienda, questo rende possibile anonimizzare colonne selezionate di un file in modo semplice. Come sempre, però, è importante fare un controllo finale per evitare che i valori possano comunque essere riconoscibili.
Come scegliere le colonne Excel da anonimizzare (dettaglio)
Dopo aver individuato la tabella, come abbiamo visto, SANITIZE chiede se vogliamo anonimizzare tutte le colonne.
Se scegliamo Sì, tutti i valori della tabella vengono sostituiti con dati sintetici. Se scegliamo No, possiamo selezionare direttamente nel foglio le colonne che vogliamo anonimizzare.
Non è necessario selezionare tutte le celle della colonna: possiamo utilizzare anche soltanto le intestazioni oppure una qualsiasi cella appartenente alle colonne interessate. Per selezionare colonne non adiacenti possiamo utilizzare CTRL su Windows oppure CMD su Mac.
Ad esempio, in una tabella composta da:
Cliente | Regione | Prodotto | Quantità | Fatturato
potremmo scegliere di anonimizzare soltanto:
Cliente | Fatturato
SANITIZE modificherà quindi queste due colonne e copierà senza modifiche Regione, Prodotto e Quantità.
Prima di procedere viene mostrato un avviso specifico, perché in questa modalità il nuovo file continuerà a contenere i valori originali delle colonne che non abbiamo selezionato. La scelta delle colonne deve quindi essere fatta con attenzione.
Come vengono generati i dati sintetici
Per ogni colonna selezionata la macro cerca di capire quale tipo di informazione è presente. In alcuni casi utilizza il nome dell'intestazione, mentre per i valori numerici controlla anche il formato Excel applicato alle celle.
Una colonna Email, ad esempio, può produrre un valore come:
Una colonna Telefono può diventare:
TEL-5831940276
Per una normale colonna testuale come Cliente viene invece generato un codice sintetico che utilizza una parte dell'intestazione come prefisso:
CLI-A7K2-P9Q4
La macro gestisce diversi casi comuni, fra cui email, URL, IBAN, telefoni, CAP, codici, date, orari, durate, età, percentuali, importi, valute e numeri generici. Se non viene riconosciuto un tipo particolare, viene comunque generato un valore coerente con la natura del dato originale.
Le colonne che non abbiamo selezionato non passano invece attraverso questa logica: il loro contenuto viene riportato nel nuovo file senza modifiche.
Come mantenere i valori ripetuti
Uno degli aspetti più importanti riguarda i duplicati. Se lo stesso cliente compare più volte all'interno della stessa colonna, non vogliamo generare ogni volta un valore differente, perché perderemmo un'informazione utile sulla struttura della tabella.
La prima volta che SANITIZE incontra Rossi SRL, ad esempio, potrebbe associarlo a CLI-A7K2-P9Q4. Se lo stesso valore compare nuovamente nella colonna Cliente, verrà utilizzato ancora lo stesso valore sintetico.
La corrispondenza tra dato originale e dato sintetico viene mantenuta solamente in memoria durante l'esecuzione. Non viene scritta nel file finale e non viene salvata altrove, quindi una nuova esecuzione della macro produrrà una mappatura differente.
Questo permette di mantenere utilizzabili conteggi, raggruppamenti e altre analisi che dipendono dalla presenza di valori ripetuti.
Come mantenere numeri positivi e negativi
Per i valori numerici anonimizzati viene mantenuto anche il segno originale. Nei dati aziendali un numero negativo può indicare uno storno, un reso, una rettifica o un movimento in uscita, quindi trasformarlo casualmente in un valore positivo altererebbe il significato della tabella.
La regola utilizzata è semplice:
positivo → positivo
negativo → negativo
zero → zero
Un valore di 1.500 € può diventare, ad esempio, 64.239,85 €, mentre -1.500 € può diventare -28.431,17 €. Lo stesso comportamento viene applicato alle percentuali e agli altri valori numerici per i quali il segno ha significato.
La macro genera direttamente il nuovo valore nel verso corretto e applica anche un secondo controllo prima di inserirlo nella tabella.
Come gestire date, percentuali e valute
In Excel una data, una percentuale e un orario sono tecnicamente numeri. Non possiamo quindi riconoscerli semplicemente controllando il contenuto della cella.
La macro analizza anche il formato numerico applicato da Excel. Un valore visualizzato come 15/05/2026 può quindi essere riconosciuto come data, 12,5% come percentuale e 1.250,00 € come importo in valuta.
Dopo aver generato il nuovo valore, SANITIZE applica nuovamente il formato originale. In questo modo una percentuale rimane visualizzata come percentuale, una data continua ad apparire come data e un importo mantiene il formato valuta.
La stessa logica viene utilizzata anche per data e ora, orari e durate.
Come viene protetto il file originale
Il workbook sorgente viene aperto in sola lettura utilizzando il parametro ReadOnly:
ReadOnly:=True
Alla fine viene chiuso senza salvare modifiche:
wbOrigine.Close SaveChanges:=False
In questo modo SANITIZE non deve cancellare, sostituire o modificare i dati presenti nel file originale.
Prima dell'apertura viene inoltre utilizzata questa istruzione:
Application.AutomationSecurity = _
msoAutomationSecurityForceDisable
per evitare l'esecuzione automatica di eventuali macro VBA presenti nel workbook selezionato. Una volta aperto il file viene ripristinata l'impostazione precedente.
Come vengono generati i valori casuali
Questo punto è fondamentale. Per generare i nuovi valori non utilizzo il normale Rnd di VBA, ma la macro si appoggia invece al generatore casuale fornito dal sistema operativo.
Su Windows viene utilizzato BCryptGenRandom, mentre su macOS viene utilizzato arc4random_buf. Ogni nuova esecuzione produce quindi una mappatura differente: lo stesso cliente che in una prima elaborazione diventa CLI-A7K2-P9Q4 potrebbe ottenere un codice completamente diverso la volta successiva.
In sostanza - è praticamente impossibile, per un utente che non abbia visione dei dati precedenti, fare reverse engineering e riuscire a ricavare i dati prima del passaggio nella macro.
Codice VBA per anonimizzare i dati in Excel
Il codice completo gestisce la selezione del file, della tabella e delle colonne da anonimizzare, oltre alla creazione del nuovo workbook con i dati sintetici. La macro è molto ricca e articolata, ti lascio qui il codice qualora volessi darci un'occhiata ma eviterò di andare troppo in dettaglio nella guida.
Option Explicit
'=========================================================
' SANITIZE.xlsm
'
' Compatibile con:
' - Excel per Windows
' - Excel per macOS
'
' FUNZIONALITÀ
' - Selezione file sorgente
' - Apertura in sola lettura
' - Selezione Tabella Excel
' - Possibilità di anonimizzare:
' 1. tutte le colonne
' 2. solo alcune colonne selezionate
' - Dati sintetici casuali
' - Valori uguali -> stesso valore sintetico
' - Segno numerico preservato
' - Formati Excel mantenuti
' - Nuovo file .xlsx
'
' ATTENZIONE
' Se vengono selezionate solo alcune colonne,
' le colonne non selezionate vengono copiate
' nel file finale SENZA anonimizzazione.
'=========================================================
'=========================================================
' RANDOM SICURO CROSS-PLATFORM
'=========================================================
#If Mac Then
Private Declare PtrSafe Sub arc4random_buf _
Lib "/usr/lib/libSystem.B.dylib" ( _
ByRef buffer As Any, _
ByVal dimensione As LongPtr)
#Else
Private Declare PtrSafe Function BCryptGenRandom _
Lib "bcrypt.dll" ( _
ByVal hAlgorithm As LongPtr, _
ByRef pbBuffer As Any, _
ByVal cbBuffer As Long, _
ByVal dwFlags As Long) As Long
Private Const BCRYPT_USE_SYSTEM_PREFERRED_RNG As Long = &H2
#End If
'=========================================================
' PROCEDURA PRINCIPALE
'=========================================================
Public Sub SanificaFileExcel()
Const SOGLIA_AVVISO_CELLE As Double = 2000000#
Dim percorsoOrigine As Variant
Dim percorsoOutput As Variant
Dim percorsoOutputFinale As String
Dim nomeFileOrigine As String
Dim nomeFileDefault As String
Dim nomeOutput As String
Dim wbOrigine As Workbook
Dim wbDestinazione As Workbook
Dim wsDestinazione As Worksheet
Dim tblOrigine As ListObject
Dim tblDestinazione As ListObject
Dim rngOutput As Range
Dim dati As Variant
Dim intestazioni As Variant
Dim output() As Variant
Dim mappe() As Collection
Dim valoriUsati() As Collection
Dim sanitizzaColonna() As Boolean
Dim formatiColonna() As String
Dim formatiMisti() As Boolean
Dim formatoColonna As Variant
Dim formatoCorrente As String
Dim righe As Long
Dim colonne As Long
Dim r As Long
Dim c As Long
Dim chiave As String
Dim chiaveSintetica As String
Dim valoreSintetico As Variant
Dim numeroTabelle As Long
Dim numeroCelle As Double
Dim numeroColonneSanitizzate As Long
Dim tentativi As Long
Dim passoAvanzamento As Long
Dim stileTabella As String
Dim risposta As VbMsgBoxResult
Dim statoScreenUpdating As Boolean
Dim statoEnableEvents As Boolean
Dim statoDisplayAlerts As Boolean
Dim sicurezzaPrecedente As MsoAutomationSecurity
Dim descrizioneErrore As String
Dim sovrascriviOutput As Boolean
Dim outputCompleto As Boolean
'-----------------------------------------------------
' Stato iniziale Excel
'-----------------------------------------------------
statoScreenUpdating = _
Application.screenUpdating
statoEnableEvents = _
Application.enableEvents
statoDisplayAlerts = _
Application.displayAlerts
sicurezzaPrecedente = _
Application.automationSecurity
On Error GoTo GestioneErrore
'=====================================================
' 1. SELEZIONE FILE SORGENTE
'=====================================================
percorsoOrigine = _
Application.GetOpenFilename( _
Title:="Seleziona il file Excel da sanitizzare")
If VarType(percorsoOrigine) = vbBoolean Then
If percorsoOrigine = False Then
Exit Sub
End If
End If
If Not EstensioneExcelSupportata( _
CStr(percorsoOrigine)) Then
MsgBox _
"Seleziona un file Excel nei formati:" & _
vbCrLf & vbCrLf & _
".xlsx" & vbCrLf & _
".xlsm" & vbCrLf & _
".xlsb" & vbCrLf & _
".xls", _
vbExclamation, _
"Formato non supportato"
Exit Sub
End If
'-----------------------------------------------------
' SANITIZE.xlsm non può essere il sorgente
'-----------------------------------------------------
If StrComp( _
CStr(percorsoOrigine), _
ThisWorkbook.FullName, _
vbTextCompare) = 0 Then
MsgBox _
"Seleziona il file contenente i dati da sanitizzare, " & _
"non SANITIZE.xlsm.", _
vbExclamation, _
"File non valido"
Exit Sub
End If
'-----------------------------------------------------
' Il sorgente deve essere chiuso
'-----------------------------------------------------
If WorkbookGiaAperto( _
CStr(percorsoOrigine)) Then
MsgBox _
"Il file selezionato è già aperto." & _
vbCrLf & vbCrLf & _
"Chiudilo e avvia nuovamente SANITIZE.", _
vbExclamation, _
"File già aperto"
Exit Sub
End If
Application.screenUpdating = False
Application.enableEvents = False
Application.StatusBar = _
"SANITIZE - Apertura del file sorgente..."
'=====================================================
' 2. APERTURA READ-ONLY
'=====================================================
Application.automationSecurity = _
msoAutomationSecurityForceDisable
Set wbOrigine = _
Workbooks.Open( _
Filename:=CStr(percorsoOrigine), _
UpdateLinks:=0, _
ReadOnly:=True, _
AddToMru:=False, _
Notify:=False, _
IgnoreReadOnlyRecommended:=True)
Application.automationSecurity = _
sicurezzaPrecedente
nomeFileOrigine = _
wbOrigine.Name
'=====================================================
' 3. INDIVIDUAZIONE TABELLA
'=====================================================
numeroTabelle = _
ContaTabelle( _
wbOrigine, _
tblOrigine)
If numeroTabelle = 0 Then
MsgBox _
"Il file selezionato non contiene Tabelle Excel." & _
vbCrLf & vbCrLf & _
"Trasforma prima l'intervallo da sanitizzare " & _
"in una Tabella Excel.", _
vbExclamation, _
"Nessuna tabella trovata"
GoTo AnnullaOperazione
End If
If numeroTabelle > 1 Then
Application.screenUpdating = True
Set tblOrigine = _
SelezionaTabellaDalWorkbook( _
wbOrigine)
Application.screenUpdating = False
If tblOrigine Is Nothing Then
GoTo AnnullaOperazione
End If
End If
If tblOrigine.DataBodyRange Is Nothing Then
MsgBox _
"La tabella selezionata non contiene dati.", _
vbExclamation, _
"Tabella vuota"
GoTo AnnullaOperazione
End If
'=====================================================
' 4. DIMENSIONI
'=====================================================
righe = _
tblOrigine.ListRows.Count
colonne = _
tblOrigine.ListColumns.Count
numeroCelle = _
CDbl(righe) * _
CDbl(colonne)
ReDim sanitizzaColonna( _
1 To colonne)
'=====================================================
' 5. SCELTA DELLE COLONNE DA ANONIMIZZARE
'=====================================================
risposta = _
MsgBox( _
"Vuoi anonimizzare tutte le colonne della tabella?" & _
vbCrLf & vbCrLf & _
"Sì = anonimizza tutte le colonne" & _
vbCrLf & _
"No = scegli solo alcune colonne" & _
vbCrLf & _
"Annulla = interrompi", _
vbYesNoCancel + vbQuestion, _
"Colonne da anonimizzare")
If risposta = vbCancel Then
GoTo AnnullaOperazione
End If
If risposta = vbYes Then
For c = 1 To colonne
sanitizzaColonna(c) = True
Next c
numeroColonneSanitizzate = _
colonne
Else
Application.screenUpdating = True
If Not SelezionaColonneDaSanitizzare( _
tblOrigine, _
sanitizzaColonna, _
numeroColonneSanitizzate) Then
Application.screenUpdating = False
GoTo AnnullaOperazione
End If
Application.screenUpdating = False
'-------------------------------------------------
' Avviso importante:
' le altre colonne rimarranno originali
'-------------------------------------------------
risposta = _
MsgBox( _
"Hai scelto di anonimizzare " & _
numeroColonneSanitizzate & _
" colonne su " & _
colonne & _
"." & _
vbCrLf & vbCrLf & _
"Le altre " & _
(colonne - numeroColonneSanitizzate) & _
" colonne verranno copiate nel nuovo file " & _
"con i valori ORIGINALI." & _
vbCrLf & vbCrLf & _
"Continua solo se queste colonne possono essere condivise." & _
vbCrLf & vbCrLf & _
"Continuare?", _
vbYesNo + vbExclamation + vbDefaultButton2, _
"Dati non anonimizzati")
If risposta <> vbYes Then
GoTo AnnullaOperazione
End If
End If
'=====================================================
' 6. AVVISO DATASET GRANDE
'=====================================================
If numeroCelle > SOGLIA_AVVISO_CELLE Then
risposta = _
MsgBox( _
"La tabella contiene:" & _
vbCrLf & vbCrLf & _
Format$(righe, "#,##0") & " righe" & _
vbCrLf & _
Format$(colonne, "#,##0") & " colonne" & _
vbCrLf & _
Format$(numeroCelle, "#,##0") & " celle" & _
vbCrLf & vbCrLf & _
"La sanitizzazione potrebbe richiedere " & _
"più memoria e tempo del normale." & _
vbCrLf & vbCrLf & _
"Continuare?", _
vbYesNo + vbQuestion + vbDefaultButton2, _
"Tabella di grandi dimensioni")
If risposta <> vbYes Then
GoTo AnnullaOperazione
End If
End If
'=====================================================
' 7. NOMI OUTPUT
'=====================================================
nomeOutput = _
CreaNomeOutput( _
tblOrigine.Name)
nomeFileDefault = _
NomeFileSanitizzato( _
nomeFileOrigine)
stileTabella = _
tblOrigine.TableStyle
'=====================================================
' 8. SALVA CON NOME PRIMA DELL'ELABORAZIONE
'=====================================================
RichiediPercorsoOutput:
sovrascriviOutput = False
Application.StatusBar = False
Application.screenUpdating = True
#If Mac Then
percorsoOutput = _
Application.GetSaveAsFilename( _
InitialFileName:=nomeFileDefault, _
Title:="Salva il file sanitizzato")
#Else
percorsoOutput = _
Application.GetSaveAsFilename( _
InitialFileName:=nomeFileDefault, _
FileFilter:= _
"Cartella di lavoro Excel (*.xlsx),*.xlsx", _
FilterIndex:=1, _
Title:="Salva il file sanitizzato")
#End If
Application.screenUpdating = False
If VarType(percorsoOutput) = vbBoolean Then
If percorsoOutput = False Then
GoTo AnnullaOperazione
End If
End If
percorsoOutputFinale = _
AssicuraEstensioneXlsx( _
CStr(percorsoOutput))
'-----------------------------------------------------
' Non può sovrascrivere il sorgente
'-----------------------------------------------------
If StrComp( _
percorsoOutputFinale, _
CStr(percorsoOrigine), _
vbTextCompare) = 0 Then
MsgBox _
"Il file sanitizzato non può sovrascrivere " & _
"il file originale.", _
vbExclamation, _
"Percorso non valido"
GoTo RichiediPercorsoOutput
End If
'-----------------------------------------------------
' File già esistente
'-----------------------------------------------------
If FileEsiste( _
percorsoOutputFinale) Then
risposta = _
MsgBox( _
"Esiste già un file con questo nome:" & _
vbCrLf & vbCrLf & _
percorsoOutputFinale & _
vbCrLf & vbCrLf & _
"Vuoi sostituirlo?", _
vbYesNo + vbExclamation + vbDefaultButton2, _
"File già esistente")
If risposta <> vbYes Then
GoTo RichiediPercorsoOutput
End If
sovrascriviOutput = True
End If
'=====================================================
' 9. LETTURA DATI
'=====================================================
Application.StatusBar = _
"SANITIZE - Lettura dei dati..."
dati = _
ValoriRange2D( _
tblOrigine.DataBodyRange)
intestazioni = _
ValoriRange2D( _
tblOrigine.HeaderRowRange)
ReDim output( _
1 To righe + 1, _
1 To colonne)
ReDim mappe( _
1 To colonne)
ReDim valoriUsati( _
1 To colonne)
ReDim formatiColonna( _
1 To colonne)
ReDim formatiMisti( _
1 To colonne)
'=====================================================
' 10. PREPARAZIONE COLONNE
'=====================================================
For c = 1 To colonne
output(1, c) = _
intestazioni(1, c)
'-------------------------------------------------
' Le Collection servono solamente
' alle colonne da anonimizzare
'-------------------------------------------------
If sanitizzaColonna(c) Then
Set mappe(c) = _
New Collection
Set valoriUsati(c) = _
New Collection
End If
formatoColonna = _
tblOrigine.ListColumns(c) _
.DataBodyRange _
.NumberFormatLocal
If IsNull(formatoColonna) Then
formatiMisti(c) = True
formatiColonna(c) = _
CStr( _
tblOrigine.DataBodyRange _
.Cells(1, c) _
.NumberFormatLocal)
Else
formatiMisti(c) = False
formatiColonna(c) = _
CStr(formatoColonna)
End If
Next c
passoAvanzamento = _
righe \ 100
If passoAvanzamento < 1 Then
passoAvanzamento = 1
End If
'=====================================================
' 11. ELABORAZIONE
'=====================================================
For r = 1 To righe
If _
r = 1 Or _
r = righe Or _
r Mod passoAvanzamento = 0 Then
Application.StatusBar = _
"SANITIZE - Elaborazione: " & _
Format$(r / righe, "0%") & _
" (" & _
Format$(r, "#,##0") & _
" / " & _
Format$(righe, "#,##0") & _
" righe)"
DoEvents
End If
For c = 1 To colonne
'=================================================
' COLONNA NON SELEZIONATA
'
' Il valore viene copiato senza modifiche.
'=================================================
If Not sanitizzaColonna(c) Then
output(r + 1, c) = _
dati(r, c)
'=================================================
' COLONNA DA ANONIMIZZARE
'=================================================
Else
'---------------------------------------------
' Vuoto
'---------------------------------------------
If ValoreVuoto( _
dati(r, c)) Then
output(r + 1, c) = _
vbNullString
'---------------------------------------------
' Errore Excel
'---------------------------------------------
ElseIf IsError( _
dati(r, c)) Then
output(r + 1, c) = _
CVErr(xlErrNA)
Else
'-----------------------------------------
' Formato
'-----------------------------------------
If formatiMisti(c) Then
formatoCorrente = _
CStr( _
tblOrigine.DataBodyRange _
.Cells(r, c) _
.NumberFormatLocal)
Else
formatoCorrente = _
formatiColonna(c)
End If
'-----------------------------------------
' Chiave originale
'-----------------------------------------
chiave = _
ChiaveValore( _
dati(r, c))
'-----------------------------------------
' Nuovo mapping
'-----------------------------------------
If Not EsisteChiave( _
mappe(c), _
chiave) Then
tentativi = 0
Do
valoreSintetico = _
ValoreSanitizzato( _
valoreOriginale:=dati(r, c), _
intestazione:=CStr( _
intestazioni(1, c)), _
formatoNumero:=formatoCorrente)
'---------------------------------
' Verifica finale del segno
'---------------------------------
valoreSintetico = _
ForzaSegnoCorretto( _
valoreOriginale:=dati(r, c), _
valoreSintetico:=valoreSintetico, _
intestazione:=CStr( _
intestazioni(1, c)), _
formatoNumero:=formatoCorrente)
chiaveSintetica = _
ChiaveValore( _
valoreSintetico)
tentativi = _
tentativi + 1
If tentativi > 10000 Then
Err.Raise _
vbObjectError + 2000, _
"SanitizzaFileExcel", _
"Impossibile generare un valore " & _
"sintetico univoco per la colonna '" & _
CStr(intestazioni(1, c)) & "'."
End If
Loop While _
EsisteChiave( _
valoriUsati(c), _
chiaveSintetica)
mappe(c).Add _
Item:=valoreSintetico, _
Key:=chiave
valoriUsati(c).Add _
Item:=True, _
Key:=chiaveSintetica
End If
output(r + 1, c) = _
mappe(c).Item(chiave)
End If
End If
Next c
Next r
'=====================================================
' 12. CREA NUOVO WORKBOOK
'=====================================================
Application.StatusBar = _
"SANITIZE - Creazione del nuovo file..."
Set wbDestinazione = _
Workbooks.Add( _
xlWBATWorksheet)
Set wsDestinazione = _
wbDestinazione.Worksheets(1)
wsDestinazione.Name = _
nomeOutput
'=====================================================
' 13. SCRIVE I DATI
'=====================================================
Set rngOutput = _
wsDestinazione.Range("A1").Resize( _
righe + 1, _
colonne)
rngOutput.Value2 = _
output
'=====================================================
' 14. RIPRISTINA FORMATI
'=====================================================
Application.StatusBar = _
"SANITIZE - Ripristino dei formati..."
For c = 1 To colonne
If formatiMisti(c) Then
For r = 1 To righe
wsDestinazione.Cells( _
r + 1, _
c).NumberFormatLocal = _
tblOrigine.DataBodyRange _
.Cells(r, c) _
.NumberFormatLocal
Next r
Else
wsDestinazione.Range( _
wsDestinazione.Cells(2, c), _
wsDestinazione.Cells( _
righe + 1, _
c)).NumberFormatLocal = _
formatiColonna(c)
End If
Next c
'=====================================================
' 15. CREA TABELLA EXCEL
'=====================================================
Set tblDestinazione = _
wsDestinazione.ListObjects.Add( _
SourceType:=xlSrcRange, _
Source:=rngOutput, _
XlListObjectHasHeaders:=xlYes)
tblDestinazione.Name = _
nomeOutput
On Error Resume Next
tblDestinazione.TableStyle = _
stileTabella
If Err.Number <> 0 Then
Err.Clear
tblDestinazione.TableStyle = _
"TableStyleMedium2"
End If
On Error GoTo GestioneErrore
rngOutput.Columns.AutoFit
outputCompleto = True
'=====================================================
' 16. CHIUDE SORGENTE
'=====================================================
wbOrigine.Close _
SaveChanges:=False
Set wbOrigine = Nothing
'=====================================================
' 17. SALVATAGGIO
'=====================================================
Application.StatusBar = _
"SANITIZE - Salvataggio del file..."
If sovrascriviOutput Then
Application.displayAlerts = False
End If
wbDestinazione.SaveAs _
Filename:=percorsoOutputFinale, _
FileFormat:=xlOpenXMLWorkbook, _
CreateBackup:=False
Application.displayAlerts = _
statoDisplayAlerts
'=====================================================
' 18. RIPRISTINO EXCEL
'=====================================================
RipristinaApplicazione _
statoScreenUpdating, _
statoEnableEvents, _
statoDisplayAlerts, _
sicurezzaPrecedente
wbDestinazione.Activate
'=====================================================
' 19. FINE
'=====================================================
MsgBox _
"Sanitizzazione completata." & _
vbCrLf & vbCrLf & _
"File: " & _
wbDestinazione.Name & _
vbCrLf & _
"Foglio: " & _
wsDestinazione.Name & _
vbCrLf & _
"Tabella: " & _
tblDestinazione.Name & _
vbCrLf & _
"Righe: " & _
Format$(righe, "#,##0") & _
vbCrLf & _
"Colonne totali: " & _
Format$(colonne, "#,##0") & _
vbCrLf & _
"Colonne anonimizzate: " & _
Format$(numeroColonneSanitizzate, "#,##0") & _
vbCrLf & vbCrLf & _
"Il file originale non è stato modificato.", _
vbInformation, _
"SANITIZE"
Exit Sub
'=========================================================
' ANNULLAMENTO
'=========================================================
AnnullaOperazione:
On Error Resume Next
If Not wbOrigine Is Nothing Then
wbOrigine.Close _
SaveChanges:=False
End If
RipristinaApplicazione _
statoScreenUpdating, _
statoEnableEvents, _
statoDisplayAlerts, _
sicurezzaPrecedente
On Error GoTo 0
Exit Sub
'=========================================================
' GESTIONE ERRORI
'=========================================================
GestioneErrore:
descrizioneErrore = _
Err.Description
On Error Resume Next
If Not wbOrigine Is Nothing Then
wbOrigine.Close _
SaveChanges:=False
End If
If Not wbDestinazione Is Nothing Then
If outputCompleto Then
wbDestinazione.Activate
Else
wbDestinazione.Close _
SaveChanges:=False
End If
End If
RipristinaApplicazione _
statoScreenUpdating, _
statoEnableEvents, _
statoDisplayAlerts, _
sicurezzaPrecedente
On Error GoTo 0
MsgBox _
"Operazione interrotta:" & _
vbCrLf & vbCrLf & _
descrizioneErrore, _
vbCritical, _
"SANITIZE"
End Sub
'=========================================================
' SELEZIONE COLONNE DA ANONIMIZZARE
'
' L'utente può selezionare:
' - intestazioni
' - celle della tabella
' - intere colonne
' - più aree
'
' Qualsiasi colonna della tabella intercettata
' dalla selezione viene marcata per la sanitizzazione.
'=========================================================
Private Function SelezionaColonneDaSanitizzare( _
ByVal tbl As ListObject, _
ByRef sanitizzaColonna() As Boolean, _
ByRef numeroColonneSelezionate As Long) As Boolean
Dim scelta As Range
Dim intersezione As Range
Dim c As Long
Riprova:
Set scelta = Nothing
tbl.Parent.Activate
On Error Resume Next
Set scelta = _
Application.InputBox( _
Prompt:= _
"Seleziona una o più colonne della tabella " & _
"che vuoi anonimizzare." & _
vbCrLf & vbCrLf & _
"Puoi selezionare le celle delle intestazioni " & _
"oppure qualsiasi cella delle colonne interessate." & _
vbCrLf & vbCrLf & _
"Per colonne non adiacenti usa Ctrl su Windows " & _
"o Cmd su Mac.", _
Title:="Seleziona le colonne da anonimizzare", _
Type:=8)
On Error GoTo 0
If scelta Is Nothing Then
SelezionaColonneDaSanitizzare = False
Exit Function
End If
'-----------------------------------------------------
' La selezione deve appartenere alla tabella
'-----------------------------------------------------
Set intersezione = Nothing
On Error Resume Next
Set intersezione = _
Intersect( _
scelta, _
tbl.Range)
On Error GoTo 0
If intersezione Is Nothing Then
MsgBox _
"La selezione non contiene colonne della Tabella Excel." & _
vbCrLf & vbCrLf & _
"Seleziona almeno una cella appartenente alla tabella.", _
vbExclamation, _
"Selezione non valida"
GoTo Riprova
End If
'-----------------------------------------------------
' Reset
'-----------------------------------------------------
numeroColonneSelezionate = 0
For c = LBound(sanitizzaColonna) _
To UBound(sanitizzaColonna)
sanitizzaColonna(c) = False
Next c
'-----------------------------------------------------
' Identifica le colonne intercettate
'-----------------------------------------------------
For c = 1 To tbl.ListColumns.Count
If Not Intersect( _
scelta, _
tbl.ListColumns(c).Range) Is Nothing Then
sanitizzaColonna(c) = True
numeroColonneSelezionate = _
numeroColonneSelezionate + 1
End If
Next c
If numeroColonneSelezionate = 0 Then
MsgBox _
"Non è stata selezionata alcuna colonna valida.", _
vbExclamation, _
"Selezione non valida"
GoTo Riprova
End If
SelezionaColonneDaSanitizzare = _
True
End Function
'=========================================================
' GENERAZIONE VALORE SINTETICO
'=========================================================
Private Function ValoreSanitizzato( _
ByVal valoreOriginale As Variant, _
ByVal intestazione As String, _
ByVal formatoNumero As String) As Variant
Dim tipo As String
Dim prefisso As String
Dim originale As Double
tipo = _
TipoDato( _
intestazione, _
formatoNumero)
prefisso = _
PrefissoIntestazione( _
intestazione)
'=====================================================
' TESTI SPECIALI
'=====================================================
Select Case tipo
Case "EMAIL"
ValoreSanitizzato = _
"utente." & _
LCase$(TokenCasuale(8)) & _
"@example.com"
Exit Function
Case "URL"
ValoreSanitizzato = _
"https://example.com/" & _
LCase$(TokenCasuale(10))
Exit Function
Case "IBAN"
ValoreSanitizzato = _
"IT00X" & _
CifreCasuali(5) & _
CifreCasuali(5) & _
TokenCasuale(12)
Exit Function
Case "PHONE"
ValoreSanitizzato = _
"TEL-" & _
CifreCasuali(10)
Exit Function
Case "POSTAL"
If IsNumeric(valoreOriginale) Then
ValoreSanitizzato = _
NumeroCasualeStessoSegno( _
CDbl(valoreOriginale), _
1, _
99999)
Else
ValoreSanitizzato = _
CifreCasuali(5)
End If
Exit Function
End Select
'=====================================================
' BOOLEAN
'=====================================================
If VarType(valoreOriginale) = vbBoolean Then
ValoreSanitizzato = _
(CasualeSicuro(0, 1) = 1)
Exit Function
End If
'=====================================================
' NUMERI
'=====================================================
If IsNumeric(valoreOriginale) Then
originale = _
CDbl(valoreOriginale)
If originale = 0 Then
ValoreSanitizzato = 0
Exit Function
End If
Select Case tipo
Case "DATETIME"
ValoreSanitizzato = _
CDbl(DateSerial(2000, 1, 1)) + _
CasualeSicuro( _
0, _
CLng( _
Date - _
DateSerial(2000, 1, 1))) + _
CasualeSicuro( _
0, _
86399) / _
86400#
Case "DATE"
ValoreSanitizzato = _
CDbl(DateSerial(2000, 1, 1)) + _
CasualeSicuro( _
0, _
CLng( _
Date - _
DateSerial(2000, 1, 1)))
Case "TIME"
ValoreSanitizzato = _
CasualeSicuro( _
1, _
86399) / _
86400#
Case "DURATION"
ValoreSanitizzato = _
NumeroCasualeStessoSegno( _
originale, _
1, _
259200, _
86400#)
Case "AGE"
ValoreSanitizzato = _
NumeroCasualeStessoSegno( _
originale, _
18, _
90)
Case "PERCENT"
ValoreSanitizzato = _
NumeroCasualeStessoSegno( _
originale, _
1, _
1000000, _
1000000#)
Case "CURRENCY"
ValoreSanitizzato = _
NumeroCasualeStessoSegno( _
originale, _
1, _
100000000, _
100#)
Case "CODE"
ValoreSanitizzato = _
NumeroCasualeStessoSegno( _
originale, _
100000, _
999999)
Case Else
If originale <> Fix(originale) Then
ValoreSanitizzato = _
NumeroCasualeStessoSegno( _
originale, _
1, _
100000000, _
100#)
Else
ValoreSanitizzato = _
NumeroCasualeStessoSegno( _
originale, _
1, _
999999)
End If
End Select
Exit Function
End If
'=====================================================
' TESTO GENERICO
'=====================================================
If tipo = "CODE" Then
ValoreSanitizzato = _
prefisso & _
"-" & _
TokenCasuale(8)
Else
ValoreSanitizzato = _
prefisso & _
"-" & _
TokenCasuale(4) & _
"-" & _
TokenCasuale(4)
End If
End Function
'=========================================================
' NUMERO CASUALE CON STESSO SEGNO
'=========================================================
Private Function NumeroCasualeStessoSegno( _
ByVal originale As Double, _
ByVal minimo As Long, _
ByVal massimo As Long, _
Optional ByVal divisore As Double = 1#) As Double
Dim risultato As Double
If originale = 0 Then
NumeroCasualeStessoSegno = 0
Exit Function
End If
risultato = _
CasualeSicuro( _
minimo, _
massimo) / _
divisore
If originale > 0 Then
NumeroCasualeStessoSegno = _
risultato
Else
NumeroCasualeStessoSegno = _
-risultato
End If
End Function
'=========================================================
' VERIFICA FINALE DEL SEGNO
'=========================================================
Private Function ForzaSegnoCorretto( _
ByVal valoreOriginale As Variant, _
ByVal valoreSintetico As Variant, _
ByVal intestazione As String, _
ByVal formatoNumero As String) As Variant
Dim tipo As String
Dim originale As Double
Dim sintetico As Double
If Not IsNumeric(valoreOriginale) Then
ForzaSegnoCorretto = _
valoreSintetico
Exit Function
End If
If Not IsNumeric(valoreSintetico) Then
ForzaSegnoCorretto = _
valoreSintetico
Exit Function
End If
tipo = _
TipoDato( _
intestazione, _
formatoNumero)
'-----------------------------------------------------
' Date e orari esclusi dalla logica del segno
'-----------------------------------------------------
If _
tipo = "DATE" Or _
tipo = "DATETIME" Or _
tipo = "TIME" Then
ForzaSegnoCorretto = _
valoreSintetico
Exit Function
End If
originale = _
CDbl(valoreOriginale)
sintetico = _
CDbl(valoreSintetico)
If originale = 0 Then
ForzaSegnoCorretto = 0
ElseIf originale > 0 Then
If sintetico = 0 Then
ForzaSegnoCorretto = 1
Else
ForzaSegnoCorretto = _
Abs(sintetico)
End If
Else
If sintetico = 0 Then
ForzaSegnoCorretto = -1
Else
ForzaSegnoCorretto = _
-Abs(sintetico)
End If
End If
End Function
'=========================================================
' IDENTIFICAZIONE TIPO DATO
'=========================================================
Private Function TipoDato( _
ByVal intestazione As String, _
ByVal formatoNumero As String) As String
Dim nome As String
nome = _
NormalizzaIntestazione( _
intestazione)
'-----------------------------------------------------
' FORMATO EXCEL
'-----------------------------------------------------
If FormatoPercentuale(formatoNumero) Then
TipoDato = "PERCENT"
Exit Function
End If
If FormatoDurata(formatoNumero) Then
TipoDato = "DURATION"
Exit Function
End If
If FormatoData(formatoNumero) And _
FormatoOra(formatoNumero) Then
TipoDato = "DATETIME"
Exit Function
End If
If FormatoData(formatoNumero) Then
TipoDato = "DATE"
Exit Function
End If
If FormatoOra(formatoNumero) Then
TipoDato = "TIME"
Exit Function
End If
If FormatoValuta(formatoNumero) Then
TipoDato = "CURRENCY"
Exit Function
End If
'-----------------------------------------------------
' INTESTAZIONE
'-----------------------------------------------------
If _
Contiene(nome, "email") Or _
Contiene(nome, "mail") Then
TipoDato = "EMAIL"
Exit Function
End If
If _
Contiene(nome, "url") Or _
Contiene(nome, "website") Or _
Contiene(nome, "sito") Then
TipoDato = "URL"
Exit Function
End If
If Contiene(nome, "iban") Then
TipoDato = "IBAN"
Exit Function
End If
If _
Contiene(nome, "telefono") Or _
Contiene(nome, "cellulare") Or _
Contiene(nome, "phone") Or _
Contiene(nome, "mobile") Then
TipoDato = "PHONE"
Exit Function
End If
If _
Contiene(nome, "dataora") Or _
Contiene(nome, "datetime") Or _
Contiene(nome, "timestamp") Then
TipoDato = "DATETIME"
Exit Function
End If
If _
Contiene(nome, "durata") Or _
Contiene(nome, "duration") Then
TipoDato = "DURATION"
Exit Function
End If
If _
Contiene(nome, "data") Or _
Contiene(nome, "date") Then
TipoDato = "DATE"
Exit Function
End If
If _
nome = "ora" Or _
nome = "time" Or _
Contiene(nome, "orario") Then
TipoDato = "TIME"
Exit Function
End If
If _
nome = "età" Or _
nome = "eta" Or _
nome = "age" Then
TipoDato = "AGE"
Exit Function
End If
If _
nome = "cap" Or _
Contiene(nome, "postalcode") Or _
Contiene(nome, "zipcode") Then
TipoDato = "POSTAL"
Exit Function
End If
If _
Contiene(intestazione, "%") Or _
Contiene(nome, "percent") Or _
Contiene(nome, "sconto") Or _
Contiene(nome, "tasso") Or _
Contiene(nome, "discount") Or _
Contiene(nome, "rate") Then
TipoDato = "PERCENT"
Exit Function
End If
If _
Contiene(nome, "importo") Or _
Contiene(nome, "prezzo") Or _
Contiene(nome, "costo") Or _
Contiene(nome, "ricavo") Or _
Contiene(nome, "fatturato") Or _
Contiene(nome, "amount") Or _
Contiene(nome, "price") Or _
Contiene(nome, "cost") Or _
Contiene(nome, "revenue") Then
TipoDato = "CURRENCY"
Exit Function
End If
If _
nome = "id" Or _
nome = "code" Or _
nome = "codice" Or _
Left$(nome, 2) = "id" Or _
Right$(nome, 2) = "id" Or _
Contiene(nome, "codice") Then
TipoDato = "CODE"
Exit Function
End If
TipoDato = _
"NUMBER"
End Function
'=========================================================
' FORMATI
'=========================================================
Private Function FormatoPercentuale( _
ByVal formato As String) As Boolean
FormatoPercentuale = _
Contiene( _
formato, _
"%")
End Function
Private Function FormatoValuta( _
ByVal formato As String) As Boolean
Dim simboloLocale As String
On Error Resume Next
simboloLocale = _
CStr( _
Application.International( _
xlCurrencyCode))
On Error GoTo 0
If _
Contiene(formato, "€") Or _
Contiene(formato, "$") Or _
Contiene(formato, "£") Or _
Contiene(formato, "¥") Or _
Contiene(formato, "EUR") Or _
Contiene(formato, "USD") Or _
Contiene(formato, "GBP") Or _
Contiene(formato, "CHF") Or _
Contiene(formato, "JPY") Then
FormatoValuta = True
Exit Function
End If
If Len(simboloLocale) > 0 Then
FormatoValuta = _
Contiene( _
formato, _
simboloLocale)
End If
End Function
Private Function FormatoData( _
ByVal formato As String) As Boolean
Dim f As String
f = _
LCase$(formato)
FormatoData = _
InStr(f, "yyyy") > 0 Or _
InStr(f, "yy") > 0 Or _
InStr(f, "aaaa") > 0 Or _
InStr(f, "gg") > 0 Or _
InStr(f, "dd") > 0 Or _
InStr(f, "jj") > 0
End Function
Private Function FormatoOra( _
ByVal formato As String) As Boolean
Dim f As String
f = _
LCase$(formato)
FormatoOra = _
InStr(f, ":") > 0 And _
( _
InStr(f, "h") > 0 Or _
InStr(f, "ss") > 0 _
)
End Function
Private Function FormatoDurata( _
ByVal formato As String) As Boolean
Dim f As String
f = _
LCase$(formato)
FormatoDurata = _
InStr(f, "[h]") > 0 Or _
InStr(f, "[hh]") > 0 Or _
InStr(f, "[m]") > 0 Or _
InStr(f, "[mm]") > 0 Or _
InStr(f, "[s]") > 0 Or _
InStr(f, "[ss]") > 0
End Function
'=========================================================
' RANDOM
'=========================================================
Private Function Casuale32Bit() As Double
Dim valore As Long
#If Mac Then
arc4random_buf _
valore, _
LenB(valore)
#Else
Dim risultato As Long
risultato = _
BCryptGenRandom( _
0, _
valore, _
LenB(valore), _
BCRYPT_USE_SYSTEM_PREFERRED_RNG)
If risultato <> 0 Then
Err.Raise _
vbObjectError + 1000, _
"Casuale32Bit", _
"Impossibile generare un numero casuale sicuro."
End If
#End If
If valore < 0 Then
Casuale32Bit = _
CDbl(valore) + _
4294967296#
Else
Casuale32Bit = _
CDbl(valore)
End If
End Function
Private Function CasualeSicuro( _
ByVal minimo As Long, _
ByVal massimo As Long) As Long
Dim ampiezza As Double
Dim valore As Double
Dim limite As Double
Dim resto As Double
If massimo < minimo Then
Err.Raise _
vbObjectError + 1001, _
"CasualeSicuro", _
"Intervallo casuale non valido."
End If
ampiezza = _
CDbl(massimo) - _
CDbl(minimo) + _
1#
limite = _
Fix( _
4294967296# / _
ampiezza) * _
ampiezza
Do
valore = _
Casuale32Bit()
Loop While valore >= limite
resto = _
valore - _
Fix( _
valore / _
ampiezza) * _
ampiezza
CasualeSicuro = _
minimo + _
CLng(resto)
End Function
Private Function TokenCasuale( _
ByVal lunghezza As Long) As String
Const CARATTERI As String = _
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Dim risultato As String
Dim i As Long
For i = 1 To lunghezza
risultato = _
risultato & _
Mid$( _
CARATTERI, _
CasualeSicuro( _
1, _
Len(CARATTERI)), _
1)
Next i
TokenCasuale = _
risultato
End Function
Private Function CifreCasuali( _
ByVal lunghezza As Long) As String
Dim risultato As String
Dim i As Long
For i = 1 To lunghezza
risultato = _
risultato & _
CStr( _
CasualeSicuro( _
0, _
9))
Next i
CifreCasuali = _
risultato
End Function
'=========================================================
' MAPPING
'=========================================================
Private Function ChiaveValore( _
ByVal valore As Variant) As String
Dim rappresentazione As String
Select Case VarType(valore)
Case vbBoolean
rappresentazione = _
"B|" & _
CStr(CBool(valore))
Case _
vbByte, _
vbInteger, _
vbLong, _
vbSingle, _
vbDouble, _
vbCurrency, _
vbDecimal
rappresentazione = _
"N|" & _
Format$( _
CDbl(valore), _
"0.############################")
Case vbDate
rappresentazione = _
"D|" & _
Format$( _
CDbl(valore), _
"0.############################")
Case Else
rappresentazione = _
"T|" & _
CStr(valore)
End Select
ChiaveValore = _
ChiaveCompatta( _
rappresentazione)
End Function
Private Function ChiaveCompatta( _
ByVal testo As String) As String
Const MODULO As Double = _
2147483629#
Dim h1 As Double
Dim h2 As Double
Dim codice As Long
Dim i As Long
h1 = 5381#
h2 = 7919#
For i = 1 To Len(testo)
codice = _
AscW( _
Mid$( _
testo, _
i, _
1))
If codice < 0 Then
codice = _
codice + _
65536
End If
h1 = _
h1 * 33# + _
CDbl(codice)
h1 = _
h1 - _
Fix( _
h1 / MODULO) * _
MODULO
h2 = _
h2 * 65599# + _
CDbl(codice)
h2 = _
h2 - _
Fix( _
h2 / MODULO) * _
MODULO
Next i
ChiaveCompatta = _
"K" & _
CStr(Len(testo)) & _
"_" & _
Hex$(CLng(h1)) & _
"_" & _
Hex$(CLng(h2))
End Function
Private Function EsisteChiave( _
ByVal raccolta As Collection, _
ByVal chiave As String) As Boolean
Dim valore As Variant
On Error Resume Next
Err.Clear
valore = _
raccolta.Item(chiave)
EsisteChiave = _
(Err.Number = 0)
Err.Clear
On Error GoTo 0
End Function
'=========================================================
' UTILITY DATI
'=========================================================
Private Function ValoreVuoto( _
ByVal valore As Variant) As Boolean
If IsEmpty(valore) Or _
IsNull(valore) Then
ValoreVuoto = True
ElseIf Not IsError(valore) Then
ValoreVuoto = _
(CStr(valore) = vbNullString)
End If
End Function
Private Function NormalizzaIntestazione( _
ByVal intestazione As String) As String
Dim risultato As String
risultato = _
LCase$( _
Trim$(intestazione))
risultato = _
Replace$( _
risultato, _
" ", _
vbNullString)
risultato = _
Replace$( _
risultato, _
"_", _
vbNullString)
risultato = _
Replace$( _
risultato, _
"-", _
vbNullString)
NormalizzaIntestazione = _
risultato
End Function
Private Function PrefissoIntestazione( _
ByVal intestazione As String) As String
Dim risultato As String
risultato = _
UCase$( _
NormalizzaIntestazione( _
intestazione))
If Len(risultato) < 3 Then
risultato = _
risultato & _
String$( _
3 - Len(risultato), _
"X")
End If
PrefissoIntestazione = _
Left$( _
risultato, _
3)
End Function
Private Function Contiene( _
ByVal testo As String, _
ByVal termine As String) As Boolean
Contiene = _
InStr( _
1, _
testo, _
termine, _
vbTextCompare) > 0
End Function
'=========================================================
' RANGE -> ARRAY 2D
'=========================================================
Private Function ValoriRange2D( _
ByVal rng As Range) As Variant
Dim valore As Variant
Dim matrice() As Variant
valore = _
rng.Value2
If rng.Cells.CountLarge = 1 Then
ReDim matrice(1 To 1, 1 To 1)
matrice(1, 1) = _
valore
ValoriRange2D = _
matrice
Else
ValoriRange2D = _
valore
End If
End Function
'=========================================================
' CONTA TABELLE
'=========================================================
Private Function ContaTabelle( _
ByVal wb As Workbook, _
ByRef tabellaUnica As ListObject) As Long
Dim ws As Worksheet
Dim tbl As ListObject
Dim n As Long
For Each ws In wb.Worksheets
For Each tbl In ws.ListObjects
n = n + 1
If n = 1 Then
Set tabellaUnica = _
tbl
End If
Next tbl
Next ws
ContaTabelle = _
n
End Function
'=========================================================
' SELEZIONE TABELLA
'=========================================================
Private Function SelezionaTabellaDalWorkbook( _
ByVal wb As Workbook) As ListObject
Dim scelta As Range
Dim tbl As ListObject
Do
wb.Activate
On Error Resume Next
Set scelta = _
Application.InputBox( _
Prompt:= _
"Il file contiene più Tabelle Excel." & _
vbCrLf & vbCrLf & _
"Seleziona una cella della tabella " & _
"che vuoi sanitizzare.", _
Title:="Seleziona la tabella", _
Type:=8)
On Error GoTo 0
If scelta Is Nothing Then
Exit Function
End If
If Not scelta.Worksheet.Parent Is wb Then
MsgBox _
"Seleziona una cella nel file da sanitizzare.", _
vbExclamation, _
"Selezione non valida"
Else
Set tbl = Nothing
On Error Resume Next
Set tbl = _
scelta.ListObject
On Error GoTo 0
If tbl Is Nothing Then
MsgBox _
"La cella selezionata non appartiene " & _
"a una Tabella Excel.", _
vbExclamation, _
"Selezione non valida"
Else
Set SelezionaTabellaDalWorkbook = _
tbl
Exit Function
End If
End If
Set scelta = Nothing
Loop
End Function
'=========================================================
' NOMI
'=========================================================
Private Function NomeFileSanitizzato( _
ByVal nomeFileOriginale As String) As String
Dim posizionePunto As Long
Dim nomeBase As String
posizionePunto = _
InStrRev( _
nomeFileOriginale, _
".")
If posizionePunto > 1 Then
nomeBase = _
Left$( _
nomeFileOriginale, _
posizionePunto - 1)
Else
nomeBase = _
nomeFileOriginale
End If
NomeFileSanitizzato = _
nomeBase & _
"_SANITIZED.xlsx"
End Function
Private Function CreaNomeOutput( _
ByVal nomeOriginale As String) As String
Const SUFFISSO As String = _
"_SANITIZED"
Dim base As String
Dim lunghezzaBase As Long
base = nomeOriginale
base = Replace$(base, "\", "_")
base = Replace$(base, "/", "_")
base = Replace$(base, ":", "_")
base = Replace$(base, "*", "_")
base = Replace$(base, "?", "_")
base = Replace$(base, "[", "_")
base = Replace$(base, "]", "_")
base = Replace$(base, "'", "_")
lunghezzaBase = _
31 - _
Len(SUFFISSO)
base = _
Left$( _
base, _
lunghezzaBase)
If Len(base) = 0 Then
base = "DATA"
End If
CreaNomeOutput = _
base & _
SUFFISSO
End Function
'=========================================================
' UTILITY FILE
'=========================================================
Private Function EstensioneExcelSupportata( _
ByVal percorso As String) As Boolean
Dim posizione As Long
Dim estensione As String
posizione = _
InStrRev( _
percorso, _
".")
If posizione = 0 Then
Exit Function
End If
estensione = _
LCase$( _
Mid$( _
percorso, _
posizione + 1))
Select Case estensione
Case _
"xlsx", _
"xlsm", _
"xlsb", _
"xls"
EstensioneExcelSupportata = _
True
End Select
End Function
Private Function WorkbookGiaAperto( _
ByVal percorso As String) As Boolean
Dim wb As Workbook
For Each wb In _
Application.Workbooks
On Error Resume Next
If StrComp( _
wb.FullName, _
percorso, _
vbTextCompare) = 0 Then
WorkbookGiaAperto = _
True
On Error GoTo 0
Exit Function
End If
On Error GoTo 0
Next wb
End Function
Private Function AssicuraEstensioneXlsx( _
ByVal percorso As String) As String
Dim posizionePunto As Long
Dim posizioneSeparatore As Long
Dim p1 As Long
Dim p2 As Long
If LCase$( _
Right$( _
percorso, _
5)) = ".xlsx" Then
AssicuraEstensioneXlsx = _
percorso
Exit Function
End If
posizionePunto = _
InStrRev( _
percorso, _
".")
p1 = _
InStrRev( _
percorso, _
"\")
p2 = _
InStrRev( _
percorso, _
"/")
If p1 > p2 Then
posizioneSeparatore = _
p1
Else
posizioneSeparatore = _
p2
End If
If posizionePunto > posizioneSeparatore Then
AssicuraEstensioneXlsx = _
Left$( _
percorso, _
posizionePunto - 1) & _
".xlsx"
Else
AssicuraEstensioneXlsx = _
percorso & _
".xlsx"
End If
End Function
Private Function FileEsiste( _
ByVal percorso As String) As Boolean
On Error Resume Next
FileEsiste = _
Len( _
Dir$(percorso)) > 0
On Error GoTo 0
End Function
'=========================================================
' RIPRISTINO EXCEL
'=========================================================
Private Sub RipristinaApplicazione( _
ByVal screenUpdating As Boolean, _
ByVal enableEvents As Boolean, _
ByVal displayAlerts As Boolean, _
ByVal automationSecurity As MsoAutomationSecurity)
On Error Resume Next
Application.StatusBar = False
Application.screenUpdating = _
screenUpdating
Application.enableEvents = _
enableEvents
Application.displayAlerts = _
displayAlerts
Application.automationSecurity = _
automationSecurity
On Error GoTo 0
End Sub
Il codice completo lo trovi qui sopra, se ti interessa approfondire.
Cosa viene mantenuto nel file finale
Il comportamento cambia in base alla modalità scelta. Se anonimizzamo tutte le colonne, il file risultante contiene dati sintetici per l'intera tabella. Se scegliamo invece soltanto alcune colonne, il risultato sarà una combinazione di valori sintetici e valori originali.
| Informazione | Comportamento |
|---|---|
| Intestazioni | Mantenute |
| Numero di righe | Mantenuto |
| Numero di colonne | Mantenuto |
| Celle vuote | Mantenute |
| Colonne selezionate | Valori sostituiti |
| Colonne non selezionate | Valori originali |
| Valori ripetuti nelle colonne anonimizzate | Mantenuti come ripetizione |
| Numeri positivi | Rimangono positivi |
| Numeri negativi | Rimangono negativi |
| Zero | Rimane zero |
| Formati numerici | Mantenuti |
| Formule originali | Non copiate |
| Power Query | Non copiata |
| Altri fogli | Non copiati |
| Mapping originale/sintetico | Non salvato |
La possibilità di lasciare alcune colonne originali è utile quando vogliamo proteggere soltanto una parte del dataset, ma rende ancora più importante controllare il file prima di condividerlo.
Cosa non viene mantenuto
La mappatura dei valori viene gestita separatamente per ogni colonna anonimizzata. Questo significa che lo stesso identificativo presente in due colonne differenti non viene necessariamente sostituito con lo stesso valore sintetico.
Se 12345 compare sia nella colonna ID Cliente sia in Cliente collegato, le due colonne potrebbero ricevere due valori differenti. Se il problema da analizzare dipende proprio dalla relazione tra queste due colonne, sarebbe necessario utilizzare una logica diversa con una mappatura condivisa.
Allo stesso modo, scegliere di non anonimizzare una colonna significa che i suoi valori originali saranno presenti nel file finale. Non bisogna quindi interpretare il suffisso _SANITIZED come garanzia che qualsiasi informazione contenuta nella tabella sia stata sostituita.
Questo punto è importante perchè un caso tipico potrebbe essere legato al dover collegare ID in database diversi - visto che la macro punta proprio a creare valori che non siano ripetibili, sarebbe impossibile fare operazioni quali merge di query o simili.
Anonimizzazione o sanitizzazione?
In questa guida utilizzo il termine anonimizzare perché è quello più immediato per descrivere ciò che vogliamo ottenere. Tecnicamente dovremmo però parlare di sanitizzazione o di generazione di dati sintetici.
La macro mantiene infatti intenzionalmente alcune caratteristiche del dataset originale. Se un cliente compare 200 volte, il relativo valore sintetico continuerà a comparire 200 volte; allo stesso modo viene mantenuta la distribuzione tra valori positivi, negativi e zero.
Inoltre, quando scegliamo di anonimizzare soltanto alcune colonne, il file contiene esplicitamente anche dati originali. Queste caratteristiche sono utili per mantenere il dataset utilizzabile, ma non considererei quindi l'esecuzione della macro come una garanzia automatica di anonimizzazione in senso normativo.
Cosa controllare prima di condividere il file
Un file Excel può contenere informazioni anche al di fuori della tabella visibile: fogli nascosti, formule, nomi definiti, commenti, query, connessioni, collegamenti esterni o cache delle PivotTable.
È proprio per questo che SANITIZE non duplica l'intero workbook. Crea un nuovo file .xlsx e inserisce soltanto la tabella selezionata.
Prima di condividerlo farei comunque sempre un ultimo controllo, soprattutto se abbiamo scelto di anonimizzare soltanto alcune colonne. Apri il file _SANITIZED.xlsx, verifica le intestazioni e qualche riga e assicurati che tutte le colonne lasciate originali possano effettivamente essere condivise.
SANITIZE funziona su Windows e Mac?
La macro è stata strutturata per funzionare sia su Excel desktop per Windows sia su Excel desktop per macOS. La differenza principale riguarda il generatore casuale utilizzato, che viene scelto automaticamente in base al sistema operativo.
Anche la selezione delle colonne può essere effettuata su entrambe le piattaforme. Per selezionare colonne non adiacenti possiamo utilizzare CTRL su Windows e CMD su Mac.
Excel Online non esegue VBA, quindi SANITIZE.xlsm deve essere utilizzato tramite l'applicazione desktop di Excel.
Ammetto - non l'ho testato su MAC, dovrebbe funzionare ma fammi sapere in caso non sia così!
SANITIZE funziona con tabelle grandi?
Non ho inserito un limite fisso sul numero di righe o colonne. Le dimensioni vengono lette direttamente dalla Tabella Excel e le matrici utilizzate dalla macro vengono dimensionate dinamicamente.
Naturalmente rimangono i limiti pratici di Excel, della memoria disponibile e delle prestazioni di VBA. Per questo, oltre i 2 milioni di celle, SANITIZE mostra un avviso prima di procedere. Non è un blocco: possiamo comunque continuare.
Durante l'elaborazione viene inoltre mostrata la percentuale di avanzamento direttamente nella barra di stato di Excel.
CONSULENZA EXCEL
Vuoi applicare quanto visto ai tuoi dati e processi aziendali?
Se il tuo caso richiede più di un esempio standard, posso aiutarti a progettare o ottimizzare il file e automatizzare le attività ripetitive.

Marco Filocamo · Microsoft Excel MVP
Cerchi invece formazione Excel per il tuo team? Scopri la formazione aziendale →
Conclusioni
Quando dobbiamo condividere un file Excel per mostrare un problema, una formula o un grafico, spesso non abbiamo bisogno di condividere tutti i dati originali. In alcuni casi vogliamo sostituire l'intera tabella, in altri è sufficiente proteggere soltanto determinate colonne.
SANITIZE permette entrambe le modalità. Possiamo anonimizzare tutta la tabella oppure selezionare soltanto le colonne da sostituire, mantenendo per quelle scelte valori ripetuti, formati, date, percentuali e segno dei numeri.
Il file originale viene aperto in sola lettura e il risultato viene creato separatamente come .xlsx. Se lasciamo alcune colonne non anonimizzate, queste vengono riportate nel nuovo file con i valori originali, quindi il controllo finale rimane sempre fondamentale.
Se invece hai un file Excel o un processo aziendale che vuoi ottimizzare o automatizzare, puoi descrivermi il processo da automatizzare e valutiamo insieme la soluzione più adatta!
Sono un formatore e consulente esperto nell’uso e nell’insegnamento di Microsoft Excel. Microsoft MVP in Excel dal 2024.
Negli ultimi 3 anni ho tenuto corsi presso realtà in multinazionali come Aruba, Bridor, IMI Orton, Primadonna e SISAL, oltre a PMI e startup di diverso genere.



































Realizzo corsi di formazione Excel dedicati per aziende, supporto professionisti 1:1 a distanza con call mirate e collaboro con aziende offrendo servizi di consulenza quali creazione di business plan, dashboard di vendita e non solo.
Vuoi saperne di più? Prenota una call gratuita di 15′!
