fredwebx commited on
Commit
352deb4
·
verified ·
1 Parent(s): aba8222

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +36 -9
  2. app.py +268 -0
  3. requirements.txt +3 -0
README.md CHANGED
@@ -1,15 +1,42 @@
1
  ---
2
- title: CSV Data Anonymizer For GDPR
3
- emoji: 🐠
4
- colorFrom: gray
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
- license: apache-2.0
12
- short_description: Anonymize sensitive CSV columns (emails, names) for GDPR
 
 
 
 
 
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: CSV Data Anonymizer for GDPR
3
+ emoji: 🔒
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.1
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: Anonymize CSV sensitive columns for GDPR compliance
12
+ tags:
13
+ - privacy
14
+ - gdpr
15
+ - data-science
16
+ - csv
17
+ - anonymization
18
  ---
19
 
20
+ # 🔒 CSV Data Anonymizer — GDPR Ready
21
+
22
+ Upload a CSV, select the columns containing sensitive data, and download an anonymized version ready to share with your team or AI tools.
23
+
24
+ ## Features
25
+
26
+ - **Auto-detection** of data type (email, name, phone, address…)
27
+ - **Deterministic mapping**: the same real value always produces the same fake value, preserving dataset consistency
28
+ - **Supported types**: Email, First name, Last name, Full name, Phone, Address, City, Postal code, Tax ID, Date of birth, Generic
29
+ - **Preview** of the result before downloading
30
+ - **CSV download** ready to use
31
+
32
+ ## How to use
33
+
34
+ 1. Upload your CSV file
35
+ 2. Review the auto-detected column types and correct them if needed
36
+ 3. Check/uncheck the columns you want to anonymize
37
+ 4. Click **"Anonymize"**
38
+ 5. Download the anonymized CSV
39
+
40
+ ## Privacy note
41
+
42
+ The file is processed entirely in memory on the server — no data is saved to disk or transmitted to third parties.
app.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ from faker import Faker
4
+ import io
5
+ import re
6
+ import json
7
+ from typing import Optional
8
+
9
+ fake = Faker(["it_IT", "en_US"])
10
+ Faker.seed(0)
11
+
12
+ # ── Tipi di anonimizzazione disponibili ────────────────────────────────────────
13
+ ANON_TYPES = {
14
+ "email": "📧 Email",
15
+ "nome": "👤 Nome",
16
+ "cognome": "👤 Cognome",
17
+ "nome_completo": "👤 Nome Completo",
18
+ "telefono": "📱 Telefono",
19
+ "indirizzo": "🏠 Indirizzo",
20
+ "città": "🏙️ Città",
21
+ "cap": "📮 CAP",
22
+ "codice_fiscale": "🪪 Codice Fiscale",
23
+ "data_nascita": "📅 Data di Nascita",
24
+ "generico": "🔒 Generico",
25
+ }
26
+
27
+ TYPE_LABELS = {v: k for k, v in ANON_TYPES.items()} # label → chiave interna
28
+
29
+ # ── Rilevamento automatico del tipo ───────────────────────────────────────────
30
+ def _detect_type(series: pd.Series) -> str:
31
+ name = (series.name or "").lower()
32
+ sample = series.dropna().astype(str).head(200)
33
+
34
+ # Heuristica sul nome colonna
35
+ if any(k in name for k in ("email", "mail", "e-mail")):
36
+ return "email"
37
+ if any(k in name for k in ("telefon", "phone", "cell", "mobile", "tel")):
38
+ return "telefono"
39
+ if any(k in name for k in ("nome", "first", "given")):
40
+ return "nome"
41
+ if any(k in name for k in ("cognome", "surname", "last")):
42
+ return "cognome"
43
+ if any(k in name for k in ("full_name", "fullname", "nome_completo")):
44
+ return "nome_completo"
45
+ if any(k in name for k in ("indirizzo", "address", "via", "street")):
46
+ return "indirizzo"
47
+ if any(k in name for k in ("città", "city", "comune", "citta")):
48
+ return "città"
49
+ if any(k in name for k in ("cap", "postal", "zip")):
50
+ return "cap"
51
+ if any(k in name for k in ("cf", "codice_fiscale", "fiscal", "tax")):
52
+ return "codice_fiscale"
53
+ if any(k in name for k in ("nascita", "birth", "dob", "birthday")):
54
+ return "data_nascita"
55
+
56
+ # Heuristica sul contenuto
57
+ if sample.str.match(r"^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$").mean() > 0.5:
58
+ return "email"
59
+ if sample.str.match(r"^[\+\d\s\-\(\)]{7,15}$").mean() > 0.5:
60
+ return "telefono"
61
+
62
+ return "generico"
63
+
64
+ # ── Generatori di dati falsi (deterministici per valore) ─────────────────────
65
+ def _make_fake(value: str, anon_type: str, cache: dict) -> str:
66
+ if value in cache:
67
+ return cache[value]
68
+
69
+ # Seed deterministico: stesso valore → stesso dato finto
70
+ seed = abs(hash(value)) % (2**32)
71
+ fake_local = Faker(["it_IT", "en_US"])
72
+ fake_local.seed_instance(seed)
73
+
74
+ generators = {
75
+ "email": lambda: fake_local.email(),
76
+ "nome": lambda: fake_local.first_name(),
77
+ "cognome": lambda: fake_local.last_name(),
78
+ "nome_completo": lambda: fake_local.name(),
79
+ "telefono": lambda: fake_local.phone_number(),
80
+ "indirizzo": lambda: fake_local.street_address(),
81
+ "città": lambda: fake_local.city(),
82
+ "cap": lambda: fake_local.postcode(),
83
+ "codice_fiscale": lambda: _fake_cf(fake_local),
84
+ "data_nascita": lambda: fake_local.date_of_birth(minimum_age=18, maximum_age=80).strftime("%d/%m/%Y"),
85
+ "generico": lambda: f"ANONIMIZZATO_{abs(hash(value)) % 100000:05d}",
86
+ }
87
+
88
+ result = generators.get(anon_type, generators["generico"])()
89
+ cache[value] = result
90
+ return result
91
+
92
+ def _fake_cf(f: Faker) -> str:
93
+ """Genera un codice fiscale plausibile (16 char alfanumerici)."""
94
+ letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
95
+ digits = "0123456789"
96
+ cf = (
97
+ "".join(f.random_choices(letters, length=3))
98
+ + "".join(f.random_choices(letters, length=3))
99
+ + "".join(f.random_choices(digits, length=2))
100
+ + f.random_choices(letters, length=1)[0]
101
+ + "".join(f.random_choices(digits, length=2))
102
+ + f.random_choices(letters, length=1)[0]
103
+ + "".join(f.random_choices(digits, length=3))
104
+ + f.random_choices(letters, length=1)[0]
105
+ )
106
+ return cf
107
+
108
+ # ── Logica principale ─────────────────────────────────────────────────────────
109
+ def load_csv(file_obj) -> tuple:
110
+ """Carica il CSV e restituisce anteprima + configurazione colonne."""
111
+ if file_obj is None:
112
+ return None, None, gr.update(visible=False), "Carica un file CSV per iniziare."
113
+
114
+ try:
115
+ df = pd.read_csv(file_obj.name, encoding="utf-8", on_bad_lines="skip")
116
+ except UnicodeDecodeError:
117
+ df = pd.read_csv(file_obj.name, encoding="latin-1", on_bad_lines="skip")
118
+ except Exception as e:
119
+ return None, None, gr.update(visible=False), f"Errore nel leggere il file: {e}"
120
+
121
+ if df.empty or len(df.columns) == 0:
122
+ return None, None, gr.update(visible=False), "Il file CSV sembra vuoto o mal formattato."
123
+
124
+ # Costruisce la configurazione colonne (nome, tipo rilevato, selezionata)
125
+ col_config = pd.DataFrame({
126
+ "Colonna": df.columns.tolist(),
127
+ "Tipo": [ANON_TYPES[_detect_type(df[c])] for c in df.columns],
128
+ "Anonimizza": [True] * len(df.columns),
129
+ })
130
+
131
+ preview = df.head(5)
132
+ msg = f"✅ Caricato: **{len(df)} righe × {len(df.columns)} colonne**. Controlla i tipi rilevati e premi **Anonimizza**."
133
+ return df.to_json(orient="split"), col_config, gr.update(visible=True), msg
134
+
135
+
136
+ def anonymize(df_json: Optional[str], col_config_df: Optional[pd.DataFrame]) -> tuple:
137
+ """Anonimizza il DataFrame secondo la configurazione."""
138
+ if df_json is None:
139
+ return None, None, "Prima carica un CSV."
140
+ if col_config_df is None or len(col_config_df) == 0:
141
+ return None, None, "Configurazione colonne mancante."
142
+
143
+ df = pd.read_json(io.StringIO(df_json), orient="split")
144
+ result = df.copy()
145
+
146
+ processed = 0
147
+ for _, row in col_config_df.iterrows():
148
+ col_name = row["Colonna"]
149
+ do_anon = row["Anonimizza"]
150
+ type_label = row["Tipo"]
151
+
152
+ if not do_anon or col_name not in result.columns:
153
+ continue
154
+
155
+ anon_type = TYPE_LABELS.get(type_label, "generico")
156
+ cache: dict = {}
157
+
158
+ result[col_name] = result[col_name].apply(
159
+ lambda v: _make_fake(str(v), anon_type, cache) if pd.notna(v) and str(v).strip() != "" else v
160
+ )
161
+ processed += 1
162
+
163
+ # Esporta in CSV
164
+ csv_bytes = result.to_csv(index=False).encode("utf-8")
165
+ csv_path = "/tmp/anonimizzato.csv"
166
+ with open(csv_path, "wb") as f:
167
+ f.write(csv_bytes)
168
+
169
+ msg = f"✅ Anonimizzate **{processed} colonne** su {len(df.columns)} totali."
170
+ return result.head(10), csv_path, msg
171
+
172
+ # ── UI ────────────────────────────────────────────────────────────────────────
173
+ DESCRIPTION = """
174
+ # 🔒 Data Anonymizer — GDPR Ready
175
+
176
+ Carica un CSV, controlla le colonne rilevate come sensibili, e scarica la versione anonimizzata.
177
+ Il mapping è **deterministico**: lo stesso valore produce sempre lo stesso dato finto, preservando la consistenza del dataset.
178
+ """
179
+
180
+ with gr.Blocks(theme=gr.themes.Soft(), title="Data Anonymizer") as demo:
181
+ # Stato interno
182
+ df_state = gr.State(None)
183
+
184
+ gr.Markdown(DESCRIPTION)
185
+
186
+ with gr.Row():
187
+ with gr.Column(scale=1):
188
+ file_input = gr.File(
189
+ label="📂 Carica CSV",
190
+ file_types=[".csv"],
191
+ type="filepath",
192
+ )
193
+ status_box = gr.Markdown("Carica un file CSV per iniziare.")
194
+
195
+ with gr.Column(scale=2):
196
+ col_editor = gr.Dataframe(
197
+ label="⚙️ Configurazione colonne (modifica Tipo e Anonimizza)",
198
+ headers=["Colonna", "Tipo", "Anonimizza"],
199
+ datatype=["str", "str", "bool"],
200
+ col_count=(3, "fixed"),
201
+ visible=False,
202
+ interactive=True,
203
+ wrap=True,
204
+ )
205
+
206
+ anon_btn = gr.Button("🔒 Anonimizza", variant="primary", size="lg")
207
+
208
+ gr.Markdown("### Anteprima risultato (prime 10 righe)")
209
+ preview_out = gr.Dataframe(label="Anteprima anonimizzata", interactive=False, wrap=True)
210
+
211
+ download_out = gr.File(label="⬇️ Scarica CSV anonimizzato", visible=False)
212
+ result_status = gr.Markdown("")
213
+
214
+ # ── Event handlers ──────────────────────────────────────────────────────
215
+ file_input.change(
216
+ fn=load_csv,
217
+ inputs=[file_input],
218
+ outputs=[df_state, col_editor, col_editor, status_box],
219
+ )
220
+
221
+ def run_anonymize(df_json, col_config):
222
+ preview, path, msg = anonymize(df_json, col_config)
223
+ file_update = gr.update(value=path, visible=path is not None)
224
+ return preview, file_update, msg
225
+
226
+ anon_btn.click(
227
+ fn=run_anonymize,
228
+ inputs=[df_state, col_editor],
229
+ outputs=[preview_out, download_out, result_status],
230
+ )
231
+
232
+ gr.Examples(
233
+ examples=[["example.csv"]],
234
+ inputs=[file_input],
235
+ label="📋 Prova con il CSV di esempio",
236
+ run_on_click=False,
237
+ )
238
+
239
+ gr.Markdown(
240
+ """
241
+ ---
242
+ *Il file viene elaborato interamente in memoria — nessun dato viene salvato su disco o trasmesso a terzi.*
243
+ """
244
+ )
245
+
246
+ # ── CSV di esempio (generato al primo avvio) ──────────────────────────────────
247
+ import os
248
+
249
+ def _create_example():
250
+ if os.path.exists("example.csv"):
251
+ return
252
+ sample = pd.DataFrame({
253
+ "id": range(1, 6),
254
+ "nome": ["Mario", "Lucia", "Giovanni", "Sofia", "Marco"],
255
+ "cognome": ["Rossi", "Bianchi", "Ferrari", "Esposito", "Romano"],
256
+ "email": ["mario.rossi@gmail.com", "lucia.b@yahoo.it", "g.ferrari@outlook.com",
257
+ "sofia.e@libero.it", "m.romano@hotmail.com"],
258
+ "telefono": ["+39 333 1234567", "02 9876543", "+39 347 9988776",
259
+ "080 5551234", "+39 320 4567890"],
260
+ "città": ["Roma", "Milano", "Napoli", "Bari", "Torino"],
261
+ "note": ["cliente premium", "newsletter sì", "B2B", "trial", "enterprise"],
262
+ })
263
+ sample.to_csv("example.csv", index=False)
264
+
265
+ _create_example()
266
+
267
+ if __name__ == "__main__":
268
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio==4.44.1
2
+ pandas==2.2.2
3
+ faker==26.0.0