Rust 练习册 113:构建你自己的 CSV 处理器

在上一篇文章中,我们探讨了如何使用 Rust 进行基础的数学计算。今天,我们将深入到更实用的领域------处理 CSV 数据。CSV(逗号分隔值)是一种常见的数据交换格式,在数据分析和处理中被广泛使用。

为什么需要特殊的 CSV 处理?

你可能会想:"CSV 不就是用逗号分隔的文本吗?有什么复杂的?" 如果你也这样认为,那么看看下面的例子就知道了:

复制代码
name,age,description
Alice,30,"工程师, 喜欢 Rust"
Bob,25,""超级棒"的程序员"
Charlie,35,"喜欢
换行的描述"

注意到复杂性了吗?字段中可能包含逗号、换行符或引号,这时就需要用引号将整个字段包围起来,而字段内的引号则需要用两个引号表示。

构建我们的 CSV 构造器

让我们从头开始构建一个符合 RFC 4180 标准的 CSV 记录构造器:

rust 复制代码
// This stub file contains items which aren't used yet; feel free to remove this module attribute
// to enable stricter warnings.
#![allow(unused)]

pub struct CsvRecordBuilder {
    content: String,
    is_first: bool,
}

impl CsvRecordBuilder {
    // Create a new builder
    pub fn new() -> Self {
        CsvRecordBuilder {
            content: String::new(),
            is_first: true,
        }
    }

    /// Adds an item to the list separated by a space and a comma.
    pub fn add(&mut self, val: &str) {
        if !self.is_first {
            self.content.push(',');
        } else {
            self.is_first = false;
        }

        if val.contains(',') || val.contains('"') || val.contains('\n') {
            // 需要转义的字段,用双引号包围,并将内部的双引号转为两个双引号
            self.content.push('"');
            self.content.push_str(&val.replace('"', "\"\""));
            self.content.push('"');
        } else {
            self.content.push_str(val);
        }
    }

    /// Consumes the builder and returns the comma separated list
    pub fn build(self) -> String {
        self.content
    }
}

代码剖析

结构体设计

rust 复制代码
pub struct CsvRecordBuilder {
    content: String,
    is_first: bool,
}

我们定义了一个 [CsvRecordBuilder](file:///Users/zacksleo/projects/github/zacksleo/exercism-rust/exercises/concept/csv-builder/src/lib.rs#L4-L7) 结构体,其中:

  • content: 存储正在构建的 CSV 字符串
  • is_first: 跟踪是否是第一个添加的元素,避免在开头添加逗号

构造函数

rust 复制代码
pub fn new() -> Self {
    CsvRecordBuilder {
        content: String::new(),
        is_first: true,
    }
}

构造函数初始化了空的内容字符串和 true 的 [is_first](file:///Users/zacksleo/projects/github/zacksleo/exercism-rust/exercises/concept/csv-builder/src/lib.rs#L5-L5) 标志。

添加元素的方法

rust 复制代码
pub fn add(&mut self, val: &str) {
    if !self.is_first {
        self.content.push(',');
    } else {
        self.is_first = false;
    }

    if val.contains(',') || val.contains('"') || val.contains('\n') {
        // 需要转义的字段,用双引号包围,并将内部的双引号转为两个双引号
        self.content.push('"');
        self.content.push_str(&val.replace('"', "\"\""));
        self.content.push('"');
    } else {
        self.content.push_str(val);
    }
}

这个方法体现了 CSV 格式的规则:

  1. 除了第一个元素外,每个新元素前都要加逗号
  2. 如果字段包含特殊字符(逗号、引号、换行符),需要用双引号包围
  3. 字段内的引号要转义为两个连续的引号

构建最终结果

rust 复制代码
pub fn build(self) -> String {
    self.content
}

使用所有权转移的方式消费 self 并返回最终的字符串。这是 Rust 中 Builder 模式的典型实现。

测试用例验证

通过测试用例我们可以看到各种场景下的行为:

rust 复制代码
use csv_builder::*;

#[test]
fn test_no_escaping() {
    let mut builder = CsvRecordBuilder::new();

    builder.add("ant");
    builder.add("bat");
    builder.add("cat");

    let list = builder.build();
    // Note that builder has been consumed so we cannot use it
    assert_eq!("ant,bat,cat", &list);
}

#[test]
fn test_quote() {
    let mut builder = CsvRecordBuilder::new();

    builder.add("ant");
    builder.add("ba\"t");
    builder.add("cat");

    let list = builder.build();
    assert_eq!(r#"ant,"ba""t",cat"#, &list);
}

#[test]
fn test_new_line() {
    let mut builder = CsvRecordBuilder::new();

    builder.add("ant");
    builder.add("ba\nt");

    let list = builder.build();
    assert_eq!("ant,\"ba\nt\"", &list);
}

#[test]
fn test_comma() {
    let mut builder = CsvRecordBuilder::new();

    builder.add("ant");
    builder.add("ba,t");

    let list = builder.build();
    assert_eq!("ant,\"ba,t\"", &list);
}

#[test]
fn test_empty() {
    let builder = CsvRecordBuilder::new();
    let list = builder.build();
    assert!(list.is_empty());
}

Rust 特性的体现

这个练习展示了 Rust 的几个重要特性:

1. 所有權系統

rust 复制代码
pub fn build(self) -> String {
    self.content
}

通过按值获取 self 参数,我们消耗了构建器实例,这防止了在构建后继续使用的错误。

2. 可变性控制

rust 复制代码
pub fn add(&mut self, val: &str) {
    // ...
}

只有在需要修改对象状态时才使用可变引用,体现了 Rust 对可变性的精确控制。

3. 字符串操作

rust 复制代码
self.content.push_str(&val.replace('"', "\"\""));

标准库提供了丰富的字符串操作功能,而且内存安全由编译器保证。

4. 模式匹配与条件判断

rust 复制代码
if val.contains(',') || val.contains('"') || val.contains('\n') {
    // 处理需要转义的情况
} else {
    // 正常处理
}

简洁明了的条件判断,无需复杂的嵌套。

实际应用场景

这种 CSV 处理能力在很多场景下都很有用:

  1. 数据导出:将数据库查询结果导出为 CSV 文件
  2. 报表生成:生成可用于 Excel 或其他电子表格软件的报表
  3. 数据迁移:在不同系统之间迁移数据
  4. 日志处理:将结构化日志输出为 CSV 格式便于分析

总结

通过这个练习,我们不仅学会了如何处理 CSV 数据格式,更重要的是掌握了 Rust 中的一些关键概念:

  • Builder 设计模式的实现
  • 所有权和借用机制的应用
  • 字符串处理技巧
  • 错误预防的设计思路

在下一个练习中,我们将继续探索 Rust 的更多强大功能!

相关推荐
定义小花3 小时前
c++ cmake qt
开发语言·c++·qt
软件开发技术深度爱好者3 小时前
Python + Ursina设计3D小游戏
开发语言·python
黑客思维者3 小时前
Python 3.14(2025最新版)的核心语法特性分析
服务器·开发语言·python·多线程
蚊子爱喝水3 小时前
PHP/ThinkPHP 最佳实践:DeepSeek/OpenAI API 实时流式输出 (Streaming) 完整指南
开发语言·php
Jaxson Lin3 小时前
Java编程进阶:打造专属于你的背单词软件V1.0
java·开发语言
wadesir3 小时前
高效存储与访问:Rust语言三角矩阵压缩(从零开始掌握Rust稀疏矩阵存储技巧)
算法·矩阵·rust
whltaoin3 小时前
【Java SE】Java IO 类常用方法大全:从字节流到 NIO 的核心 API 汇总
java·开发语言·api·nio
weixin_307779133 小时前
Jenkins Pipeline Graph View插件:可视化流水线的核心工具
运维·开发语言·架构·jenkins
秋月的私语3 小时前
c#字符串Split与CSV解析中的引号处理
服务器·开发语言·c#