Arquivos CSV (Comma-Separated Values) estão em toda parte — exportações de planilhas, logs de servidores, dados de sensores, relatórios financeiros. Neste projeto, vamos construir uma ferramenta de linha de comando que lê arquivos CSV, calcula estatísticas descritivas (mínimo, máximo, média, contagem), filtra linhas por condição e apresenta os resultados de forma organizada. Ao construir esse analisador, você vai praticar iteradores, coleta de dados em coleções, parsing de tipos numéricos e a poderosa crate csv.
Este projeto combina vários conceitos fundamentais de Rust: pattern matching, tratamento de erros, iteradores encadeados e generics de forma prática e aplicável ao dia a dia.
O Que Vamos Construir
Nosso csv-analisar terá os seguintes recursos:
- Leitura de arquivos CSV com detecção automática de cabeçalho
- Exibição formatada dos dados em tabela
- Cálculo de estatísticas por coluna numérica (min, max, média, soma, contagem)
- Filtragem de linhas por condição simples (coluna == valor, coluna > valor)
- Seleção de colunas específicas para exibição
- Contagem total de registros
Estrutura do Projeto
csv-analisar/
├── Cargo.toml
└── src/
├── main.rs
├── cli.rs
├── leitor.rs
├── estatisticas.rs
└── formatador.rs
Configurando o Projeto
cargo new csv-analisar
cd csv-analisar
Configure o Cargo.toml:
[package]
name = "csv-analisar"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
csv = "1"
colored = "2"
A crate csv é a biblioteca padrão da comunidade Rust para trabalhar com arquivos CSV. Ela lida com delimitadores, aspas, escaping e muitos outros detalhes do formato.
Passo 1: Definindo a Interface CLI
// src/cli.rs
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "csv-analisar")]
#[command(about = "Ferramenta para análise de arquivos CSV")]
pub struct Cli {
/// Caminho do arquivo CSV
pub arquivo: String,
/// Delimitador (padrão: vírgula)
#[arg(short, long, default_value = ",")]
pub delimitador: char,
#[command(subcommand)]
pub comando: Comando,
}
#[derive(Subcommand, Debug)]
pub enum Comando {
/// Exibe os dados do CSV em formato de tabela
Exibir {
/// Número máximo de linhas a exibir
#[arg(short = 'n', long, default_value_t = 50)]
limite: usize,
/// Colunas a exibir (separadas por vírgula)
#[arg(short, long)]
colunas: Option<String>,
},
/// Calcula estatísticas das colunas numéricas
Estatisticas {
/// Coluna específica para analisar
#[arg(short, long)]
coluna: Option<String>,
},
/// Filtra linhas por condição
Filtrar {
/// Condição no formato: coluna==valor ou coluna>valor
condicao: String,
/// Número máximo de resultados
#[arg(short = 'n', long, default_value_t = 100)]
limite: usize,
},
/// Exibe informações gerais do arquivo
Info,
}
Passo 2: Leitura e Parsing do CSV
O módulo leitor.rs encapsula toda a lógica de leitura do arquivo CSV.
// src/leitor.rs
use std::error::Error;
use std::fs::File;
/// Armazena o conteúdo completo de um arquivo CSV
#[derive(Debug)]
pub struct DadosCsv {
pub cabecalhos: Vec<String>,
pub linhas: Vec<Vec<String>>,
}
impl DadosCsv {
/// Lê um arquivo CSV e retorna os dados estruturados
pub fn ler_arquivo(caminho: &str, delimitador: u8) -> Result<Self, Box<dyn Error>> {
let arquivo = File::open(caminho)?;
let mut leitor = csv::ReaderBuilder::new()
.delimiter(delimitador)
.has_headers(true)
.flexible(true)
.from_reader(arquivo);
let cabecalhos: Vec<String> = leitor
.headers()?
.iter()
.map(|h| h.trim().to_string())
.collect();
let mut linhas = Vec::new();
for resultado in leitor.records() {
let registro = resultado?;
let linha: Vec<String> = registro
.iter()
.map(|campo| campo.trim().to_string())
.collect();
linhas.push(linha);
}
Ok(Self { cabecalhos, linhas })
}
/// Retorna o índice de uma coluna pelo nome
pub fn indice_coluna(&self, nome: &str) -> Option<usize> {
self.cabecalhos
.iter()
.position(|h| h.eq_ignore_ascii_case(nome))
}
/// Retorna os valores de uma coluna específica
pub fn valores_coluna(&self, indice: usize) -> Vec<&str> {
self.linhas
.iter()
.filter_map(|linha| linha.get(indice).map(|s| s.as_str()))
.collect()
}
/// Filtra linhas com base numa condição simples
pub fn filtrar(&self, coluna_idx: usize, operador: &str, valor: &str) -> Vec<&Vec<String>> {
self.linhas
.iter()
.filter(|linha| {
if let Some(campo) = linha.get(coluna_idx) {
comparar(campo, operador, valor)
} else {
false
}
})
.collect()
}
}
/// Compara um campo com um valor usando o operador especificado
fn comparar(campo: &str, operador: &str, valor: &str) -> bool {
match operador {
"==" => campo.eq_ignore_ascii_case(valor),
"!=" => !campo.eq_ignore_ascii_case(valor),
">" => {
if let (Ok(a), Ok(b)) = (campo.parse::<f64>(), valor.parse::<f64>()) {
a > b
} else {
campo > valor
}
}
"<" => {
if let (Ok(a), Ok(b)) = (campo.parse::<f64>(), valor.parse::<f64>()) {
a < b
} else {
campo < valor
}
}
">=" => {
if let (Ok(a), Ok(b)) = (campo.parse::<f64>(), valor.parse::<f64>()) {
a >= b
} else {
campo >= valor
}
}
"<=" => {
if let (Ok(a), Ok(b)) = (campo.parse::<f64>(), valor.parse::<f64>()) {
a <= b
} else {
campo <= valor
}
}
_ => false,
}
}
/// Extrai operador e partes de uma string de condição como "coluna>=valor"
pub fn parsear_condicao(condicao: &str) -> Option<(String, String, String)> {
let operadores = [">=", "<=", "!=", "==", ">", "<"];
for op in &operadores {
if let Some(pos) = condicao.find(op) {
let coluna = condicao[..pos].trim().to_string();
let valor = condicao[pos + op.len()..].trim().to_string();
return Some((coluna, op.to_string(), valor));
}
}
None
}
A struct DadosCsv armazena os cabeçalhos e todas as linhas em memória. Para arquivos muito grandes, seria melhor usar streaming, mas para uma ferramenta de análise exploratória, manter tudo na memória simplifica bastante o código.
Passo 3: Cálculo de Estatísticas
// src/estatisticas.rs
/// Resultado das estatísticas de uma coluna numérica
#[derive(Debug)]
pub struct EstatisticasColuna {
pub nome: String,
pub contagem: usize,
pub valores_numericos: usize,
pub minimo: Option<f64>,
pub maximo: Option<f64>,
pub soma: f64,
pub media: Option<f64>,
}
/// Calcula estatísticas para uma coluna de valores
pub fn calcular(nome: &str, valores: &[&str]) -> EstatisticasColuna {
let numeros: Vec<f64> = valores
.iter()
.filter_map(|v| v.parse::<f64>().ok())
.collect();
let contagem = valores.len();
let valores_numericos = numeros.len();
let (minimo, maximo, soma, media) = if numeros.is_empty() {
(None, None, 0.0, None)
} else {
let min = numeros.iter().cloned().fold(f64::INFINITY, f64::min);
let max = numeros.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let soma: f64 = numeros.iter().sum();
let media = soma / numeros.len() as f64;
(Some(min), Some(max), soma, Some(media))
};
EstatisticasColuna {
nome: nome.to_string(),
contagem,
valores_numericos,
minimo,
maximo,
soma,
media,
}
}
Passo 4: Formatação e Exibição
// src/formatador.rs
use crate::estatisticas::EstatisticasColuna;
use colored::*;
/// Exibe dados em formato de tabela com larguras calculadas
pub fn exibir_tabela(cabecalhos: &[String], linhas: &[&Vec<String>], limite: usize) {
// Calcular a largura máxima de cada coluna
let mut larguras: Vec<usize> = cabecalhos.iter().map(|h| h.len()).collect();
for linha in linhas.iter().take(limite) {
for (i, campo) in linha.iter().enumerate() {
if i < larguras.len() && campo.len() > larguras[i] {
larguras[i] = campo.len();
}
}
}
// Limitar largura máxima a 40 caracteres
for l in larguras.iter_mut() {
if *l > 40 {
*l = 40;
}
}
// Cabeçalhos
let cab: Vec<String> = cabecalhos
.iter()
.enumerate()
.map(|(i, h)| format!("{:width$}", h, width = larguras.get(i).copied().unwrap_or(10)))
.collect();
println!("{}", cab.join(" | ").bold().cyan());
// Separador
let sep: Vec<String> = larguras.iter().map(|l| "-".repeat(*l)).collect();
println!("{}", sep.join("-+-"));
// Linhas de dados
let total = linhas.len();
for linha in linhas.iter().take(limite) {
let campos: Vec<String> = linha
.iter()
.enumerate()
.map(|(i, c)| {
let largura = larguras.get(i).copied().unwrap_or(10);
let truncado = if c.len() > largura {
format!("{}...", &c[..largura - 3])
} else {
c.clone()
};
format!("{:width$}", truncado, width = largura)
})
.collect();
println!("{}", campos.join(" | "));
}
if total > limite {
println!(
"\n{} Exibindo {} de {} registros.",
"INFO:".blue().bold(),
limite,
total
);
}
}
/// Exibe estatísticas de uma coluna
pub fn exibir_estatisticas(estat: &EstatisticasColuna) {
println!("{}", format!("Coluna: {}", estat.nome).bold().cyan());
println!(" Registros totais: {}", estat.contagem);
println!(" Valores numéricos: {}", estat.valores_numericos);
if let Some(min) = estat.minimo {
println!(" Mínimo: {:.2}", min);
}
if let Some(max) = estat.maximo {
println!(" Máximo: {:.2}", max);
}
if estat.valores_numericos > 0 {
println!(" Soma: {:.2}", estat.soma);
}
if let Some(media) = estat.media {
println!(" Média: {:.2}", media);
}
println!();
}
Passo 5: Integrando no main.rs
// src/main.rs
mod cli;
mod estatisticas;
mod formatador;
mod leitor;
use clap::Parser;
use cli::{Cli, Comando};
use colored::*;
fn main() {
let cli = Cli::parse();
let delimitador = cli.delimitador as u8;
let dados = match leitor::DadosCsv::ler_arquivo(&cli.arquivo, delimitador) {
Ok(d) => d,
Err(e) => {
eprintln!(
"{} Não foi possível ler '{}': {}",
"ERRO:".red().bold(),
cli.arquivo,
e
);
std::process::exit(1);
}
};
match cli.comando {
Comando::Info => {
println!("{}", "Informações do arquivo CSV".bold().cyan());
println!(" Arquivo: {}", cli.arquivo);
println!(" Colunas: {}", dados.cabecalhos.len());
println!(" Registros: {}", dados.linhas.len());
println!(" Cabeçalhos: {}", dados.cabecalhos.join(", "));
// Detectar colunas numéricas
let numericas: Vec<&String> = dados
.cabecalhos
.iter()
.enumerate()
.filter(|(i, _)| {
let valores = dados.valores_coluna(*i);
let num_count = valores.iter().filter(|v| v.parse::<f64>().is_ok()).count();
num_count > valores.len() / 2
})
.map(|(_, nome)| nome)
.collect();
if !numericas.is_empty() {
println!(" Numéricas: {}", numericas.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "));
}
}
Comando::Exibir { limite, colunas } => {
let (cabecalhos_filtrados, linhas_filtradas) = if let Some(cols) = colunas {
let nomes: Vec<&str> = cols.split(',').map(|s| s.trim()).collect();
let indices: Vec<usize> = nomes
.iter()
.filter_map(|nome| dados.indice_coluna(nome))
.collect();
let cab: Vec<String> = indices
.iter()
.filter_map(|i| dados.cabecalhos.get(*i).cloned())
.collect();
let lins: Vec<Vec<String>> = dados
.linhas
.iter()
.map(|linha| {
indices
.iter()
.filter_map(|i| linha.get(*i).cloned())
.collect()
})
.collect();
(cab, lins)
} else {
(dados.cabecalhos.clone(), dados.linhas.clone())
};
let refs: Vec<&Vec<String>> = linhas_filtradas.iter().collect();
formatador::exibir_tabela(&cabecalhos_filtrados, &refs, limite);
}
Comando::Estatisticas { coluna } => {
let indices: Vec<(usize, &String)> = if let Some(nome_coluna) = &coluna {
if let Some(idx) = dados.indice_coluna(nome_coluna) {
vec![(idx, &dados.cabecalhos[idx])]
} else {
eprintln!(
"{} Coluna '{}' não encontrada.",
"ERRO:".red().bold(),
nome_coluna
);
std::process::exit(1);
}
} else {
dados.cabecalhos.iter().enumerate().collect()
};
println!("{}\n", "Estatísticas do CSV".bold().cyan());
for (idx, nome) in &indices {
let valores = dados.valores_coluna(*idx);
let estat = estatisticas::calcular(nome, &valores);
// Só exibir se tiver valores numéricos
if estat.valores_numericos > 0 {
formatador::exibir_estatisticas(&estat);
}
}
}
Comando::Filtrar { condicao, limite } => {
let (col_nome, operador, valor) = match leitor::parsear_condicao(&condicao) {
Some(c) => c,
None => {
eprintln!(
"{} Condição inválida: '{}'. Use formato: coluna==valor",
"ERRO:".red().bold(),
condicao
);
std::process::exit(1);
}
};
let col_idx = match dados.indice_coluna(&col_nome) {
Some(i) => i,
None => {
eprintln!(
"{} Coluna '{}' não encontrada.",
"ERRO:".red().bold(),
col_nome
);
std::process::exit(1);
}
};
let resultados = dados.filtrar(col_idx, &operador, &valor);
println!(
"{} {} registro(s) encontrado(s) para: {}\n",
"FILTRO:".blue().bold(),
resultados.len(),
condicao
);
let limitados: Vec<&Vec<String>> = resultados.into_iter().take(limite).collect();
formatador::exibir_tabela(&dados.cabecalhos, &limitados, limite);
}
}
}
Como Executar
cargo build --release
Crie um arquivo CSV de exemplo (vendas.csv):
produto,categoria,preco,quantidade,cidade
Notebook,Eletrônicos,3500.00,15,São Paulo
Mouse,Periféricos,89.90,120,Rio de Janeiro
Teclado,Periféricos,199.90,85,Belo Horizonte
Monitor,Eletrônicos,1200.00,30,São Paulo
Webcam,Periféricos,249.00,45,Curitiba
SSD,Armazenamento,350.00,200,São Paulo
Exemplos de uso:
# Ver informações gerais
./target/release/csv-analisar vendas.csv info
# Colunas: 5, Registros: 6, Numéricas: preco, quantidade
# Exibir tabela formatada
./target/release/csv-analisar vendas.csv exibir
# Exibir apenas colunas específicas
./target/release/csv-analisar vendas.csv exibir --colunas "produto,preco,quantidade"
# Calcular estatísticas
./target/release/csv-analisar vendas.csv estatisticas
# Coluna: preco
# Mínimo: 89.90, Máximo: 3500.00, Média: 931.47
# Estatísticas de uma coluna
./target/release/csv-analisar vendas.csv estatisticas --coluna quantidade
# Filtrar registros
./target/release/csv-analisar vendas.csv filtrar "cidade==São Paulo"
./target/release/csv-analisar vendas.csv filtrar "preco>500"
./target/release/csv-analisar vendas.csv filtrar "categoria==Periféricos"
Desafios para Expandir
Ordenação por coluna: Adicione um subcomando
ordenarque ordene os registros por uma coluna escolhida (ascendente ou descendente), tratando valores numéricos e textuais.Agrupamento e agregação: Implemente
agrupar-porque agrupe registros por uma coluna e calcule soma/média/contagem para outra (ex: total de vendas por cidade).Exportação para outros formatos: Adicione a opção de exportar os dados filtrados para JSON ou Markdown, facilitando a integração com relatórios.
Suporte a múltiplos delimitadores: Detecte automaticamente se o separador é vírgula, ponto-e-vírgula, tab ou pipe, analisando as primeiras linhas do arquivo.
Gráfico ASCII de barras: Crie uma visualização simples em ASCII que mostre um gráfico de barras horizontal para as estatísticas de uma coluna numérica.
Veja Também
- Iteradores em Rust — encadeamento e transformação de sequências
- Trabalhando com Vec — coleções dinâmicas
- HashMap — mapas de chave-valor para agrupamentos
- Leitura de CSV — receita prática com a crate
csv - Formatando Strings — técnicas de formatação de saída