Leer un archivo de texto en Rust

Hay dos maneras de leer un archivo de texto en Rust, y la diferencia no es de estilo sino de memoria: una carga el archivo entero de golpe y la otra lo recorre línea a línea sin guardarlo todo. Con archivos pequeños da igual; con uno de varios gigabytes, no. Nada de esto necesita dependencias.

Los ejemplos leen este inventario.txt, el mismo que genera el ejemplo de escritura:

inventario.txttornillos;120
tuercas;80
arandelas;45

El archivo entero: fs::read_to_string

Una sola llamada abre el archivo, lo lee completo a una String y lo cierra:

use std::fs;

fn main() -> std::io::Result<()> {
    let contenido = fs::read_to_string("inventario.txt")?;
    print!("{contenido}");
    println!("--- {} caracteres", contenido.len());
    Ok(())
}
salidatornillos;120
tuercas;80
arandelas;45
--- 38 caracteres

Es la opción correcta casi siempre: un archivo de configuración, una plantilla, un JSON. El único inconveniente es que todo el contenido acaba en memoria, así que con archivos muy grandes conviene la siguiente forma.

Línea a línea: BufReader

BufReader lee por bloques y entrega las líneas de una en una, de modo que en memoria solo hay una cada vez:

use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() -> std::io::Result<()> {
    let archivo = File::open("inventario.txt")?;
    let lector = BufReader::new(archivo);

    for (numero, linea) in lector.lines().enumerate() {
        let linea = linea?;
        let partes: Vec<&str> = linea.split(';').collect();
        println!("{}: {} -> {}", numero + 1, partes[0], partes[1]);
    }
    Ok(())
}
salida1: tornillos -> 120
2: tuercas -> 80
3: arandelas -> 45

Hay un detalle que confunde al principio: lines() no devuelve cadenas sino Result<String>, porque la lectura puede fallar a mitad del recorrido. Por eso dentro del bucle aparece let linea = linea?;, que abre ese Result y reutiliza el nombre. lines() además quita el salto de línea final, así que no hay que recortarlo.

Cuando el archivo puede no existir

Si la ausencia del archivo es un caso previsto y no un error, conviene distinguirla del resto de fallos en vez de dejar que el programa termine:

use std::fs;

fn main() {
    match fs::read_to_string("no_existe.txt") {
        Ok(texto) => println!("{texto}"),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            println!("el archivo no existe, uso valores por defecto");
        }
        Err(e) => eprintln!("error al leer: {e}"),
    }
}
salidael archivo no existe, uso valores por defecto

La segunda rama usa una guarda, esa condición if dentro del patrón, para separar «no está» de «está pero no se puede leer», que son problemas distintos: uno se resuelve con valores por defecto y el otro hay que reportarlo. Las guardas están explicadas en la lección de pattern matching.

Cuál usar

Situación Qué usar
Archivo pequeño o mediano, lo quiero entero fs::read_to_string
Archivo grande, o proceso línea por línea BufReader con lines()
Quiero las líneas en un vector lector.lines().collect::<Result<Vec<_>, _>>()?
Son datos binarios, no texto fs::read, que devuelve Vec<u8>
Si el archivo no es UTF-8. read_to_string falla cuando el contenido no es texto UTF-8 válido, cosa frecuente con archivos antiguos en Latin-1 o generados en Windows. El error dice stream did not contain valid UTF-8. En ese caso hay que leer los bytes con fs::read y convertirlos con la codificación correcta, o usar String::from_utf8_lossy si aceptas que los caracteres no válidos se sustituyan.

Todo el código de esta página se compiló y ejecutó con rustc 1.96.1 antes de publicarla; las salidas están copiadas de la ejecución real. Documentación oficial: std::fs::read_to_string y BufReader.