Curso de Python / Projeto: validar CSV e gerar relatórios de despesas
Uma ferramenta de relatórios CSV deve validar cabeçalhos, campos obrigatórios, quantias e identificadores duplicados antes de somar os dados. Registre as rejeições e um código de saída de falha para que um relatório parcial não seja confundido com o processamento completo da entrada.
Definir entradas, saídas e regras de rejeição
Neste projeto, id identifica uma despesa, category define sua categoria e amount contém um inteiro não negativo escrito com algarismos de 0 a 9. Não use separadores de milhares nem casas decimais na entrada. As quantias devem usar uma unidade coerente; o programa não identifica moedas nem converte unidades.
A primeira ocorrência válida aceita de um identificador ocupa esse ID. Uma repetição posterior é rejeitada, mesmo se apresentar outro valor. Uma ocorrência inválida, rejeitada antes de entrar em seen, não reserva o identificador. Espaços nas extremidades dos campos são removidos antes dessas verificações.
Use uma pasta de prática nova: o programa recusa executar se report.csv ou rejected.csv já existir. O arquivo de entrada não é alterado. O relatório de rejeições registra o número lógico do registro, contando o cabeçalho como 1; campos entre aspas com quebras de linha podem fazer esse número divergir da linha física do texto.
A aula sobre leitura e gravação de CSV apresenta separadores e aspas. Consulte também a documentação de csv.DictReader.
Preparar uma entrada com falhas conhecidas
Na pasta nova, salve este conteúdo completo como expenses-input.csv, em UTF-8. Mantenha a ordem exata das colunas e use vírgulas como separadores.
id,category,amount
e1,food,3500
e2,food,8000
e3,transport,2500
e2,food,8000
e4,books,-1
O identificador e2 está repetido, e o registro e4 contém um valor negativo. As falhas são intencionais para testar a rejeição. O programa aceita e1, a primeira ocorrência de e2 e e3, somando 3500 + 8000 + 2500 = 14000. Não “corrija” a amostra antes de verificar esse comportamento.
Executar o programa e conferir os dois arquivos
Salve o código Python completo abaixo como csv_report.py, em UTF-8, na mesma pasta. Os módulos usados pertencem à biblioteca padrão.
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)
Selecione o ambiente virtual do projeto no terminal, entre na pasta de prática e execute:
python csv_report.py
No macOS ou Ubuntu, fora de um ambiente virtual, use python3 se esse for o comando do interpretador. A saída deve mostrar accepted: 3 rejected: 2 e total: 14000. Apesar do total correto para os registros aceitos, o código de saída é 1 porque parte da entrada foi rejeitada. Confira $LASTEXITCODE no PowerShell ou echo $? em um shell de macOS ou Ubuntu imediatamente após a execução.
O arquivo report.csv deve conter:
category,total
food,11500
transport,2500
O arquivo rejected.csv deve conter:
record,reason
5,duplicate id
6,invalid amount
O registro 5 é a segunda ocorrência de e2; o registro 6 é a quantia negativa. Os motivos em inglês vêm das strings do programa: duplicate id significa identificador repetido e invalid amount, quantia inválida. O arquivo de rejeições não copia os dados completos de cada registro; use a entrada preservada para investigar o número informado.
utf-8-sig permite ler UTF-8 com ou sem a marca inicial BOM. newline="" deixa o módulo CSV tratar suas próprias quebras de linha. O cabeçalho precisa ser exatamente id,category,amount, na mesma ordem: nomes inesperados ou ordem diferente provocam ValueError antes de gerar relatórios.
O teste None in row detecta campos excedentes apresentados pelo DictReader. ID ou categoria ausente é rejeitado; uma quantia vazia falha no padrão [0-9]+. re.fullmatch exige a correspondência do campo inteiro, conforme a aula de expressões regulares e validação de formato. O código reúne um motivo por registro seguindo a ordem das condições.
Verificar repetição, correções e resultados parciais
- Compare as categorias do relatório com os registros válidos da entrada. Food deve totalizar 11500 e transport, 2500; a soma precisa coincidir com 14000.
- Execute novamente na mesma pasta. A existência dos relatórios deve provocar
FileExistsError, preservando os arquivos anteriores. Não troque o modoxporwpara contornar a regra. - Em outra pasta nova, copie o programa e uma entrada corrigida. Remova a linha duplicada e substitua a quantia -1 por 1000. Agora devem ser aceitos quatro registros, nenhum rejeitado, com total 15000 e código zero. O arquivo de rejeições terá somente o cabeçalho.
- Em outra pasta de teste, altere a ordem das colunas para conferir a rejeição do cabeçalho. Investigue a mensagem antes de distribuir qualquer resultado.
Os relatórios são escritos em sequência, não em uma única transação atômica. Se a segunda escrita falhar, o primeiro arquivo pode permanecer; falhas durante a própria escrita podem deixar arquivos incompletos. Um fluxo maior pode preparar resultados em uma pasta temporária, validá-los, registrar sua conclusão e só depois publicá-los para o próximo consumidor.
O exemplo mantém em memória os totais, os identificadores vistos e as rejeições. Ele não impõe limites gerais de tamanho, número de registros ou magnitude das quantias; valores extremos e erros de arquivo exigem políticas adicionais. Uma saída parcial ou uma exceção inesperada não deve ser convertida silenciosamente em sucesso.
Para distribuir o CSV em uma planilha, avalie como o aplicativo de destino interpreta strings externas que podem parecer fórmulas. O programa preserva categorias como texto CSV e não implementa uma política de neutralização de fórmulas. A aula de testes unitários e integração mostra uma verificação repetível do relatório e da rejeição de IDs duplicados.




