示例说明
csv::Reader 是流式的:记录逐条交付,内存占用与文件大小无关。deserialize 的迭代器把每行尝试转为结构体——类型不匹配或字段缺失只作废该行,Err 携带行级信息可定位到具体位置。表头映射用 serde 的 rename 属性解决列名与 Rust 命名规范的差异,flexible 关闭防止字段错位被静默容忍。
技术要点
- 流式交付内存恒定
- 行级错误隔离携带行号
- rename 属性映射表头命名
代码解读
ReaderBuilder 配置表头存在、全面修剪空白、严格列数三要素;stream_subscribers 遍历反序列化结果,Ok 收集 Err 连同行号进隔离区。样例数据里第二行座位数不是数字、第四行只有一列——都被隔离且行号准确,第一三行正常解析。行计数从表头后开始。
使用建议
隔离行写入死信文件供人工修复。大文件 from_reader 直接接 File 免全文读入。编码问题配 encoding_rs 预转换。
代码示例
use serde::Deserialize;
use std::io::Cursor;
#[derive(Debug, Deserialize)]
struct Subscriber {
#[serde(rename = "Email")]
email: String,
#[serde(rename = "Plan")]
plan: String,
#[serde(rename = "Seats")]
seats: u32,
}
fn stream_subscribers(
raw: &str,
) -> Result<(Vec<Subscriber>, Vec<(usize, String)>), csv::Error> {
let mut reader = csv::ReaderBuilder::new()
.has_headers(true)
.trim(csv::Trim::All)
.flexible(false)
.from_reader(Cursor::new(raw));
let mut valid = Vec::new();
let mut rejected: Vec<(usize, String)> = Vec::new();
let mut line = 1;
for outcome in reader.deserialize::<Subscriber>() {
line += 1;
match outcome {
Ok(record) => valid.push(record),
Err(err) => {
// 单行结构或类型错误:隔离并保留行号
rejected.push((line, err.to_string()));
}
}
}
Ok((valid, rejected))
}
fn main() {
let payload = "Email,Plan,Seats\n\
[email protected],pro,10\n\
[email protected],basic,not-a-number\n\
[email protected],enterprise,250\n\
broken-line-only-one-field\n";
match stream_subscribers(payload) {
Ok((valid, rejected)) => {
for record in &valid {
println!("{} on {} x{}", record.email, record.plan, record.seats);
}
for (line, reason) in &rejected {
println!("line {line} rejected: {reason}");
}
println!("valid: {}, rejected: {}", valid.len(), rejected.len());
}
Err(err) => println!("fatal csv error: {err}"),
}
}