Exemplo simples de leitura e gravação de arquivos em Rust
A seguir, apresento um exemplo minimalista de como ler o conteúdo de um arquivo e, em seguida, gravar o conteúdo em outro arquivo. Vou explicar passo a passo como funciona cada parte do código.
use std::fs::File;
use std::io::{self, Read, Write};
fn main() -> io::Result<()> {
// 1. Abra o arquivo "input.txt" para leitura
let mut file = File::open("input.txt")?;
// 2. Crie uma variável para armazenar o conteúdo lido
let mut contents = String::new();
// 3. Leia todo o conteúdo do arquivo para a variável 'contents'
file.read_to_string(&mut contents)?;
// 4. Exiba o conteúdo lido no terminal
println!("Conteúdo do arquivo de entrada: {}", contents);
// 5. Crie (ou sobrescreva) o arquivo "output.txt" para gravação
let mut out_file = File::create("output.txt")?;
// 6. Escreva algo no arquivo de saída; aqui, usamos o conteúdo que lemos
writeln!(out_file, "Conteúdo copiado: {}", contents)?;
// 7. Retorne Ok(()) caso tudo dê certo
Ok(())
}
std::fs::File
: Métodos para lidar com arquivos (abrir, criar, etc.).std::io::{self, Read, Write}
: Para operações de entrada/saída, como leitura (Read
) e escrita (Write
).main
fn main() -> io::Result<()> {
// ...
}
main
retorna um io::Result<()>
. Isso significa que qualquer erro de I/O será propagado de forma adequada, em vez de usarmos panic!
.let mut file = File::open("input.txt")?;
File::open("input.txt")
tenta abrir o arquivo para leitura.?
propaga o erro se acontecer falha ao abrir o arquivo.let mut contents = String::new();
file.read_to_string(&mut contents)?;
String
vazia chamada contents
.read_to_string
lê todo o conteúdo do arquivo e joga na variável contents
.?
novamente propaga qualquer erro de leitura.println!("Conteúdo do arquivo de entrada: {}", contents);
let mut out_file = File::create("output.txt")?;
File::create("output.txt")
cria o arquivo caso não exista, ou sobrescreve caso já exista.writeln!(out_file, "Conteúdo copiado: {}", contents)?;
writeln!
funciona como println!
, porém direcionado para um writer (no caso, out_file
).Ok(())
Ok(())
main
.?
para propagar erros de forma conveniente, evitando unwrap()
que pode causar panic se algo falhar.read_to_string
já é suficiente.File::create
apaga o conteúdo do arquivo se ele já existir. Se quiser abrir para acrescentar (append), usaria algo como OpenOptions
.Pode ser mais flexível usar uma assinatura genérica que receba qualquer tipo que implemente std::io::Read
, em vez de “prender” a função a ler diretamente de arquivo e retornar uma String
.** Dessa forma, você consegue reutilizar o mesmo código para ler tanto de arquivos quanto de outros tipos de “fontes” de dados (por exemplo, um buffer de memória, stdin
, ou até mesmo um stream de rede).
Um exemplo de como ficaria esse design mais genérico, seguido de uma explicação:
use std::fs::File;
use std::io::{self, BufReader, Read};
/// Lê todo o conteúdo de um objeto que implementa `Read` e retorna uma `String`.
fn read_all<R: Read>(mut reader: R) -> io::Result<String> {
let mut contents = String::new();
reader.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() -> io::Result<()> {
// Exemplo com um arquivo
let file = File::open("input.txt")?;
let buf_reader = BufReader::new(file);
let contents = read_all(buf_reader)?;
println!("Conteúdo:\n{}", contents);
Ok(())
}
read_all
agora não sabe nem se está lendo de um arquivo, do stdin
ou de um vetor de bytes em memória (&[u8]
). Tudo que ela precisa é de algo que implemente a trait Read
.TcpStream
, não precisa alterar read_all
; basta passar o TcpStream
como parâmetro.std::io::Cursor<&[u8]>
) simulando o conteúdo de um “arquivo falso” ou dados de entrada. Isso facilita o teste de forma independente, sem precisar criar arquivos físicos.read_all
tem a única responsabilidade de ler algo que seja Read
e devolver o conteúdo em String
.BufReader
, etc., fica em outro lugar (no main
, por exemplo), mantendo as funções mais coesas.Na abordagem inicial, nós amarramos a função diretamente a um caminho de arquivo (path: &str
) e já fazíamos File::open
, BufReader::new
e read_to_string
no mesmo local. Isso é simples de entender, mas fica menos flexível, pois a função só serve para aquele caso específico (um caminho de arquivo).
Já na abordagem genérica, nós temos:
Read
).BufReader
e afins fica onde for apropriado (p. ex., na main
).Assim, se a sua intenção for deixar o código mais modular e reutilizável, usar impl Read
(ou R: Read
) é de fato uma prática mais flexível e desacoplada.
Arquivos CSV são comuns em aplicações e podemos fazer isso facilmente em Rust, utilizando o crate csv em conjunto com o Serde para ler e gravar arquivos CSV. Vou mostrar dois códigos: um para leitura de um arquivo CSV (dados de “entrada”), e outro para gravação de um arquivo CSV (dados de “saída”).
No seu arquivo Cargo.toml, inclua as seguintes dependências:
[dependencies]
csv = "1"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
Se estiver utilizando o cargo-script coloque isso dentro de um comentário no código:
//! ```cargo
//! [package]
//! edition = "2021"
//! [dependencies]
//! csv = "1"
//! serde = { version = "1.0", features = ["derive"] }
//! serde_derive = "1.0"
//! ```
Isso permitirá usar:
Suponha que temos um arquivo chamado dados.csv com o seguinte conteúdo (repare que a primeira linha são os cabeçalhos):
id,nome,cidade
1,Ana,São Paulo
2,Bruno,Rio de Janeiro
3,Carla,Belo Horizonte
use std::error::Error;
use serde::Deserialize;
use csv::ReaderBuilder;
#[derive(Debug, Deserialize)]
struct Pessoa {
id: u32,
nome: String,
cidade: String,
}
fn main() -> Result<(), Box<dyn Error>> {
// Cria um reader (leitor) para abrir e ler o arquivo CSV
let mut leitor = ReaderBuilder::new()
.has_headers(true) // Indica que a primeira linha do CSV são cabeçalhos
.from_path("dados.csv")?;
// Lê cada registro (linha) do arquivo, desserializando em uma struct Pessoa
for resultado in leitor.deserialize::<Pessoa>() {
// Se ocorrer algum erro, o ? propaga o erro automaticamente
let registro = resultado?;
// Mostra o registro lido
println!("{:?}", registro);
}
Ok(())
}
#[derive(Debug, Deserialize)]
: Faz com que a struct Pessoa
possa ser desserializada diretamente de uma linha CSV, além de permitir o uso de {:?}
para debug (impressão mais “crua”).
Cabeçalhos no CSV: Como definimos has_headers(true)
, a primeira linha (id,nome,cidade
) não é convertida em Pessoa
; ela é usada para mapear qual coluna vai para qual campo na struct.
Laço de leitura: leitor.deserialize()
retorna um iterador de Result<Pessoa, csv::Error>
. O ?
propaga o erro caso aconteça algo inesperado (por exemplo, tipo de dado inválido ou problema no arquivo).
Flexibilidade: Se você quiser ler manualmente sem Serde, pode usar leitor.records()
para trabalhar com cada linha como texto bruto; porém, usando Deserialize
, o processo de leitura e conversão fica mais simples e seguro.
Agora, vejamos um exemplo para gravar dados em um arquivo CSV, também usando Serde para serializar automaticamente structs em linhas CSV.
use std::error::Error;
use serde::Serialize;
use csv::WriterBuilder;
#[derive(Debug, Serialize)]
struct Pessoa {
id: u32,
nome: String,
cidade: String,
}
fn main() -> Result<(), Box<dyn Error>> {
// Criamos um vetor com alguns dados
let lista_de_pessoas = vec![
Pessoa {
id: 1,
nome: "Ana".to_string(),
cidade: "São Paulo".to_string(),
},
Pessoa {
id: 2,
nome: "Bruno".to_string(),
cidade: "Rio de Janeiro".to_string(),
},
Pessoa {
id: 3,
nome: "Carla".to_string(),
cidade: "Belo Horizonte".to_string(),
},
];
// Cria (ou sobrescreve) o arquivo "saida.csv" para escrita
let mut escritor = WriterBuilder::new()
.has_headers(true) // Fará com que a primeira linha escrita seja o cabeçalho (id,nome,cidade)
.from_path("saida.csv")?;
// Serializa cada "Pessoa" em uma linha CSV
for pessoa in lista_de_pessoas {
escritor.serialize(pessoa)?;
}
// Força a gravação de qualquer dado pendente no buffer
escritor.flush()?;
println!("Arquivo CSV 'saida.csv' gravado com sucesso!");
Ok(())
}
#[derive(Debug, Serialize)]
: Permite que a struct Pessoa
seja serializada automaticamente para CSV.WriterBuilder
: Assim como no leitor, podemos configurar detalhes. Com has_headers(true)
, a primeira vez que chamamos serialize
, o cabeçalho (id,nome,cidade
) será gravado automaticamente.Pessoa
é transformada em uma linha CSV com base nos campos da struct. Se usar wtr.write_record(&["...", "...", "..."])
seria a forma manual (sem Serde)..flush()
: Assegura que tudo esteja efetivamente escrito no arquivo antes de encerrarmos o programa.Para manipular arquivos JSON em Rust, a combinação mais comum é usar as crates serde (para serialização e desserialização) e serde_json (para lidar especificamente com o formato JSON). A seguir apresento exemplos de leitura e gravação de arquivos JSON em Rust, com nomes de structs, campos e variáveis em Português.
Inclua as seguintes dependências no seu Cargo.toml
:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Ou estes comentários, se estiver utilizando cargo-script:
//! ```cargo
//! [package]
//! edition = "2021"
//! [dependencies]
//! serde = { version = "1.0", features = ["derive"] }
//! serde_json = "1.0"
//! ```
Suponha que temos um arquivo pessoas.json assim:
[
{
"id": 1,
"nome": "Ana",
"cidade": "São Paulo"
},
{
"id": 2,
"nome": "Bruno",
"cidade": "Rio de Janeiro"
},
{
"id": 3,
"nome": "Carla",
"cidade": "Belo Horizonte"
}
]
use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use serde::Deserialize;
use serde_json;
#[derive(Debug, Deserialize)]
struct Pessoa {
id: u32,
nome: String,
cidade: String,
}
fn main() -> Result<(), Box<dyn Error>> {
// Abre o arquivo
let arquivo = File::open("pessoas.json")?;
// Cria um BufReader para ler de forma eficiente
let leitor = BufReader::new(arquivo);
// Desserializa o conteúdo do JSON para um vetor de Pessoa
let lista_de_pessoas: Vec<Pessoa> = serde_json::from_reader(leitor)?;
// Exibe as pessoas lidas
for pessoa in lista_de_pessoas {
println!("{:?}", pessoa);
}
Ok(())
}
Struct com #[derive(Deserialize)]
Permite que cada objeto JSON seja convertido em uma instância de Pessoa
.
BufReader
Ler o arquivo através de um buffer é mais eficiente do que leituras pontuais; serde_json::from_reader
exige um leitor que implemente std::io::Read
.
serde_json::from_reader
Faz a desserialização automática do JSON em Vec<Pessoa>
(um vetor de Pessoa
). Se o formato do JSON não bater com o da struct, um erro será retornado.
Tratamento de Erro
Aqui usamos Result<(), Box<dyn Error>>
e o operador ?
para simplificar a propagação de erros de I/O ou de parsing.
Agora vamos criar um arquivo JSON a partir de um vetor de structs Pessoa
.
use std::error::Error;
use std::fs::File;
use serde::Serialize;
use serde_json;
#[derive(Debug, Serialize)]
struct Pessoa {
id: u32,
nome: String,
cidade: String,
}
fn main() -> Result<(), Box<dyn Error>> {
// Cria alguns dados de exemplo
let lista_de_pessoas = vec![
Pessoa {
id: 1,
nome: "Ana".to_string(),
cidade: "São Paulo".to_string(),
},
Pessoa {
id: 2,
nome: "Bruno".to_string(),
cidade: "Rio de Janeiro".to_string(),
},
Pessoa {
id: 3,
nome: "Carla".to_string(),
cidade: "Belo Horizonte".to_string(),
},
];
// Cria (ou sobrescreve) o arquivo saida.json
let arquivo = File::create("saida.json")?;
// Escreve em formato JSON "bonito" (com indentação)
serde_json::to_writer_pretty(arquivo, &lista_de_pessoas)?;
println!("Arquivo JSON 'saida.json' gravado com sucesso!");
Ok(())
}
Struct com #[derive(Serialize)]
Permite serializar a struct Pessoa
diretamente em JSON.
Criação do arquivo
File::create("saida.json")
vai criar o arquivo (ou sobrescrevê-lo se já existir).
serde_json::to_writer_pretty
Vec<Pessoa>
) em formato JSON para dentro do arquivo
.serde_json::to_writer
.Result
.String
e então chamar serde_json::from_str(&sua_string)
.String
com serde_json::to_string
e depois salvar a String
manualmente.Pessoa
(tipos incorretos, campos faltando, etc.), ocorrerá erro de desserialização.fn ler_pessoas_de_arquivo(...)
e fn gravar_pessoas_em_arquivo(...)
) que façam apenas essa responsabilidade; a main
então chamaria essas funções.Dessa forma, utilizando serde_json, você consegue ler e gravar arquivos JSON em Rust de maneira fácil e idiomática!