Leer y escribir CSV en Rust

El crate csv se combina con serde igual que el JSON: declaras una estructura y cada fila del archivo se convierte en un valor de ese tipo, con los números ya convertidos. Lee la cabecera por su cuenta y no hay que partir cadenas a mano.

La dependencia

cargo add csv
cargo add serde --features derive
[dependencies]
csv = "1.4.0"
serde = { version = "1.0.229", features = ["derive"] }

Los ejemplos trabajan sobre este inventario.csv:

inventario.csvnombre,precio,unidades
tornillos,1.50,120
tuercas,0.80,80
arandelas,0.25,45

Leer y escribir en el mismo programa

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
struct Articulo {
    nombre: String,
    precio: f64,
    unidades: u32,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut lector = csv::Reader::from_path("inventario.csv")?;
    let mut total = 0.0;
    for fila in lector.deserialize() {
        let a: Articulo = fila?;
        let valor = a.precio * a.unidades as f64;
        println!("{:<12} {:>8.2}", a.nombre, valor);
        total += valor;
    }
    println!("{:<12} {:>8.2}", "TOTAL", total);

    let mut escritor = csv::Writer::from_path("salida.csv")?;
    escritor.serialize(Articulo { nombre: "clavos".into(), precio: 0.10, unidades: 500 })?;
    escritor.flush()?;
    println!("\nsalida.csv escrito");
    Ok(())
}
salidatornillos      180.00
tuercas         64.00
arandelas       11.25
TOTAL          255.25

salida.csv escrito
salida.csvnombre,precio,unidades
clavos,0.1,500

Tres cosas que conviene notar. La primera es que los nombres de los campos del struct tienen que coincidir con los de la cabecera del CSV; si no coinciden, se renombran con #[serde(rename = "...")]. La segunda es que deserialize() entrega Result por fila, de ahí el fila? dentro del bucle: un CSV puede estar bien en la línea 1 y roto en la 500. Y la tercera es que el Writer escribe la cabecera solo, deducida de los nombres del struct.

El archivo escrito dice 0.1, no 0.10. Es lo mismo que pasa con JSON: un f64 guarda el número, no cuántos decimales se escribieron. Para la pantalla se arregla al formatear, con {:.2} como en el ejemplo. Si los dos decimales tienen que estar en el propio archivo, el precio no debería ser f64 sino un entero de céntimos o una cadena. Con dinero, además, los flotantes dan problemas de redondeo y lo correcto suele ser el entero.

Variantes frecuentes

Situación Cómo
Separador de punto y coma ReaderBuilder::new().delimiter(b';')
El archivo no tiene cabecera .has_headers(false)
Leer sin declarar un struct lector.records(), da StringRecord
Un campo puede venir vacío declararlo Option<T>
Leer desde memoria, no de archivo Reader::from_reader(texto.as_bytes())
Escribir a la salida estándar Writer::from_writer(io::stdout())

El código de esta página se compiló y ejecutó con rustc 1.96.1 y csv 1.4.0 antes de publicarla; las salidas y el archivo generado están copiados de la ejecución real. Documentación oficial: csv en docs.rs.