在上一篇文章中,我们探讨了如何使用 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 格式的规则:
- 除了第一个元素外,每个新元素前都要加逗号
- 如果字段包含特殊字符(逗号、引号、换行符),需要用双引号包围
- 字段内的引号要转义为两个连续的引号
构建最终结果
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 处理能力在很多场景下都很有用:
- 数据导出:将数据库查询结果导出为 CSV 文件
- 报表生成:生成可用于 Excel 或其他电子表格软件的报表
- 数据迁移:在不同系统之间迁移数据
- 日志处理:将结构化日志输出为 CSV 格式便于分析
总结
通过这个练习,我们不仅学会了如何处理 CSV 数据格式,更重要的是掌握了 Rust 中的一些关键概念:
- Builder 设计模式的实现
- 所有权和借用机制的应用
- 字符串处理技巧
- 错误预防的设计思路
在下一个练习中,我们将继续探索 Rust 的更多强大功能!