← 中文资源

简体中文版本

CSV 流式读取与反序列化

用 csv crate 逐行流式反序列化:表头重命名字段映射、行级错误隔离带行号、坏行不中断整条流继续消费。

阅读英文原文

示例说明

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}"),
    }
}