Corso Python / Progetto CSV: validare dati e creare un rapporto
Un programma di reportistica CSV deve verificare intestazione, campi obbligatori, importi e identificatori duplicati prima di sommare i dati. In questo progetto Python produci un rapporto per categoria e un elenco degli scarti: il codice di uscita segnala se alcune righe sono state escluse, anche quando il totale delle righe accettate è corretto.
Stabilire ingresso, risultati e regole di rifiuto
Ogni record contiene id, category e amount, esattamente in quest’ordine nell’intestazione. L’identificatore distingue una spesa; la categoria determina il gruppo; l’importo deve contenere soltanto cifre decimali ASCII e rappresentare un intero non negativo. Non inserire separatori delle migliaia, segni o decimali. Gli spazi esterni vengono eliminati prima della verifica.
La prima occorrenza valida e accettata riserva un ID. Una riga successiva con lo stesso ID viene scartata; una prima riga non valida non riserva invece quell’ID. Le categorie non vengono normalizzate nelle maiuscole: food e Food sono gruppi diversi. Le righe scartate lasciano un motivo nel file di controllo senza modificare il sorgente.
Il modulo standard csv gestisce campi delimitati e virgolette. DictReader associa i campi ai nomi dell’intestazione; un numero eccessivo di campi produce una chiave None, che il programma rifiuta. La documentazione ufficiale del modulo csv spiega anche l’uso di newline="" durante lettura e scrittura.
Esegui ogni prova in una cartella nuova: se esistono già report.csv o rejected.csv, il programma si ferma per non sovrascriverli. I percorsi sono relativi alla cartella corrente del terminale, non automaticamente alla posizione dello script.
Preparare il file di prova completo
In una cartella di esercitazione vuota salva questo contenuto UTF-8 come expenses-input.csv:
id,category,amount
e1,food,3500
e2,food,8000
e3,transport,2500
e2,food,8000
e4,books,-1
Il quarto record di dati ripete e2 e l’ultimo contiene un importo negativo. Sono difetti intenzionali, necessari per osservare le esclusioni. Il file viene aperto con utf-8-sig, che permette anche un eventuale BOM UTF-8 iniziale.
Creare il programma e confrontare i due risultati
Nella stessa cartella salva il contenuto completo seguente come csv_report.py. Non richiede pacchetti esterni:
import csv
import re
from collections import defaultdict
from pathlib import Path
source = Path("expenses-input.csv")
report = Path("report.csv")
rejected = Path("rejected.csv")
if report.exists() or rejected.exists():
raise FileExistsError("Use a new output directory")
totals = defaultdict(int)
seen = set()
errors = []
accepted = 0
with source.open(encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f)
if reader.fieldnames != ["id", "category", "amount"]:
raise ValueError("unexpected header")
for number, row in enumerate(reader, start=2):
key = (row.get("id") or "").strip()
category = (row.get("category") or "").strip()
amount_text = (row.get("amount") or "").strip()
reason = ""
if None in row or not key or not category:
reason = "invalid fields"
elif key in seen:
reason = "duplicate id"
elif not re.fullmatch(r"[0-9]+", amount_text):
reason = "invalid amount"
if reason:
errors.append({"record": number, "reason": reason})
continue
seen.add(key)
totals[category] += int(amount_text)
accepted += 1
with report.open("x", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["category", "total"])
writer.writerows(sorted(totals.items()))
with rejected.open("x", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["record", "reason"])
writer.writeheader()
writer.writerows(errors)
print("accepted:", accepted, "rejected:", len(errors))
print("total:", sum(totals.values()))
raise SystemExit(1 if errors else 0)
Dal terminale aperto nella cartella di prova esegui:
python csv_report.py
Il programma stampa accepted: 3 rejected: 2 e total: 14000. Il codice di uscita è 1, perché sono stati scartati due record. Controllalo immediatamente dopo l’esecuzione con $LASTEXITCODE in PowerShell o echo $? in una shell macOS/Ubuntu. Non interpretare un rapporto presente sul disco come prova che tutto l’ingresso sia stato accettato.
Il contenuto completo di report.csv deve essere:
category,total
food,11500
transport,2500
Il contenuto completo di rejected.csv deve essere:
record,reason
5,duplicate id
6,invalid amount
Il totale di food è 3.500 + 8.000 = 11.500; il duplicato non viene sommato. Aggiungendo 2.500 di trasporto si ottengono 14.000. record conta i record CSV logici, considerando l’intestazione come 1: il duplicato è 5 e l’importo negativo è 6. Un campo tra virgolette può contenere un ritorno a capo, quindi questi numeri non coincidono necessariamente con le righe fisiche di un editor.
Per ogni riga viene registrato un solo motivo: prima si controllano i campi, poi la duplicazione, infine l’importo. Se una riga ha più problemi, il primo controllo applicabile determina la motivazione. Il file degli scarti conserva numero e motivo, non una copia completa dei dati: tieni il sorgente originale per poter correggere la riga.
Verificare nuove esecuzioni e gestire risultati parziali
- Confronta separatamente conteggio delle righe accettate, somma per categoria, scarti e codice di uscita. Per l’ingresso fornito devono risultare tre accettazioni, due scarti e codice 1.
- Copia programma e ingresso in una seconda cartella vuota. Elimina la riga duplicata e cambia
-1in1000. Devono risultare quattro accettazioni, nessuno scarto, totale 15.000 e codice 0.rejected.csvrimane presente con la sola intestazione. - In una terza cartella prova a cambiare l’ordine dell’intestazione in
category,id,amount. La verifica rigorosa sollevaValueErrorprima di creare i rapporti: non basta che i nomi compaiano in un ordine qualsiasi. - Riesegui nella cartella che contiene già i risultati. Il controllo deve interrompersi e i file esistenti devono rimanere invariati. Per riprovare usa un’altra cartella invece di rimuovere la protezione.
La creazione dei due file non è una transazione atomica. Se la seconda scrittura fallisce, il primo rapporto può restare sul disco; un errore non equivale a un rollback automatico. In una pipeline reale puoi generare in una cartella temporanea, validare entrambi i file e pubblicarli solo con un indicatore di completamento. Non distribuire direttamente un risultato incompleto.
Gli importi vengono letti come interi, ma la grammatica del programma è volutamente stretta: 8.5, +8, 8_000 e una stringa vuota non soddisfano [0-9]+. Zero è ammesso. Non sono previsti conversione valutaria, normalizzazione dei nomi o recupero automatico di un CSV danneggiato.
Se distribuisci il CSV a un foglio di calcolo, considera separatamente come quell’applicazione interpreta stringhe esterne simili a formule: le virgolette CSV non garantiscono che una cella venga trattata come testo. La lezione sulla lettura e scrittura dei CSV approfondisce delimitatori e record; il controllo di integrazione con unittest e subprocess verifica automaticamente una duplicazione usando questo programma completo e una cartella temporanea.




