Работа с CSV в Rust: библиотеки, примеры кода и лучшие практики
CSV (Comma-Separated Values) — один из самых распространённых форматов для хранения табличных данных. В экосистеме Rust существует мощная и гибкая библиотека csv, которая в сочетании с крейтом serde позволяет удобно читать, записывать и трансформировать CSV-данные. В этой статье мы подробно разберём, как работать с CSV в Rust, начиная с базового парсинга и заканчивая сериализацией сложных структур.
Установка и подключение зависимостей
Для начала работы добавьте в файл Cargo.toml следующие зависимости:
[dependencies]
csv = "1.3"
serde = { version = "1.0", features = ["derive"] }
Библиотека csv предоставляет низкоуровневый API для чтения и записи, а serde — автоматическую десериализацию строк CSV в структуры Rust.
Чтение CSV-файла построчно
Самый простой способ прочитать CSV — использовать Reader из крейта csv. Рассмотрим пример с файлом users.csv:
use csv::Reader;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut rdr = Reader::from_path("users.csv")?;
for result in rdr.records() {
let record = result?;
println!("{:?}", record);
}
Ok(())
}
Этот код выводит каждую строку в виде вектора строк StringRecord. Однако на практике чаще требуется работать с типизированными данными.
Десериализация в структуры с serde
Объявим структуру, соответствующую столбцам CSV:
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct User {
name: String,
age: u32,
city: String,
}
Теперь можно читать записи напрямую в экземпляры User:
use csv::Reader;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut rdr = Reader::from_path("users.csv")?;
for result in rdr.deserialize() {
let user: User = result?;
println!("{} ({} лет) из {}", user.name, user.age, user.city);
}
Ok(())
}
Библиотека автоматически сопоставляет заголовки CSV с полями структуры. Если порядок или имена столбцов отличаются, используйте атрибут #[serde(rename = "...")].
Запись CSV-файла
Создание CSV-файла выполняется с помощью Writer. Запись структурированных данных через serde:
use csv::Writer;
use serde::Serialize;
use std::error::Error;
#[derive(Debug, Serialize)]
struct Product {
id: u32,
name: String,
price: f64,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut wtr = Writer::from_path("products.csv")?;
let products = vec![
Product { id: 1, name: "Ноутбук".into(), price: 75000.0 },
Product { id: 2, name: "Мышь".into(), price: 1500.0 },
];
for product in &products {
wtr.serialize(product)?;
}
wtr.flush()?;
println!("Файл products.csv успешно записан");
Ok(())
}
Обратите внимание: Writer автоматически добавляет заголовки на основе имён полей структуры.
Настройка разделителей и кавычек
По умолчанию используется запятая и двойные кавычки. Если ваш файл использует точку с запятой (например, в европейской локали), настройте ReaderBuilder:
use csv::ReaderBuilder;
let mut rdr = ReaderBuilder::new()
.delimiter(b';')
.has_headers(true)
.from_path("data.tsv")?;
Аналогично настраивается WriterBuilder для записи.
Обработка ошибок и невалидных данных
При работе с реальными данными часто встречаются пропущенные значения или неверные типы. Используйте Option<T> для необязательных полей:
#[derive(Debug, Deserialize)]
struct Record {
name: String,
age: Option<u32>, // возраст может отсутствовать
email: String,
}
Для гибкой обработки ошибок можно перехватывать проблемные записи и логировать их