【译】用 Rust 实现 csv 解析-part6
Published: 2021-01-24 09:19:03

>* Rust and CSV parsing 译文（用 Rust 实现 csv 解析-part6）
>* 原文链接：https://blog.burntsushi.net/csv/
>* 原文作者：[BurntSushi](https://github.com/BurntSushi)
>* 译文来自：https://github.com/suhanyujie/article-transfer-rs/
>* 译者：[suhanyujie](https://github.com/suhanyujie)
>* 译者博客：[suhanyujie](https://ishenghuo.cnblogs.com/)
>* ps：水平有限，翻译不当之处，还请指正。
>* 标签：Rust，csv

## 写入 tab 分隔符的值
在前面的章节中，我们了解了如何将 CSV 数据输出到标准输出中，如下：

```
City,State,Population,Latitude,Longitude
Davidsons Landing,AK,,65.2419444,-165.2716667
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111
```

你可能会想：如果都是这么简单地数据，那使用 CSV writer 还有什么意义？CSV writer 的好处是，它能处理所有类型的数据，而不会牺牲数据的完整性。也就是说，它知道何时引用包含特殊 CSV 字符的字段（如逗号或者换行），或在数据中出现的转义字面量引号。CSV writer 还可以方便地配置使用不同地分隔符或引用策略。

在这一节中，我们将看看如何调整 CSV writer 上的设置。特别地，我们将使用 TSV（“制表符分割值”）来替代 CSV，并且我们要求 CSV writer 引用所有非数字字段。这里有一个例子：

```rust
fn run() -> Result<(), Box<Error>> {
    let mut wtr = csv::WriterBuilder::new()
        .delimiter(b'\t')
        .quote_style(csv::QuoteStyle::NonNumeric)
        .from_writer(io::stdout());

    wtr.write_record(&["City", "State", "Population", "Latitude", "Longitude"])?;
    wtr.write_record(&["Davidsons Landing", "AK", "", "65.2419444", "-165.2716667"])?;
    wtr.write_record(&["Kenai", "AK", "7610", "60.5544444", "-151.2583333"])?;
    wtr.write_record(&["Oakman", "AL", "", "33.7133333", "-87.3886111"])?;

    wtr.flush()?;
    Ok(())
}
```

编译并运行这个例子：

```shell
$ cargo build
$ ./target/debug/csvtutor
"City"  "State" "Population"    "Latitude"      "Longitude"
"Davidsons Landing"     "AK"    ""      65.2419444      -165.2716667
"Kenai" "AK"    7610    60.5544444      -151.2583333
"Oakman"        "AL"    ""      33.7133333      -87.3886111
```

在本例中，我们使用了一个新类型 [QuoteStyle](https://docs.rs/csv/1.0.0/csv/enum.QuoteStyle.html) 。`QuoteStyle` 类型表示你可以使用不同的引用策略。默认情况下，只在必要时才向字段值周围添加引号。这可能适用于大多数例子，但你也可以一直在字段两边添加引号，或不添加引号，或者只在非数字两边加引号。

## 使用 Serde 写入

正如 CSV reader 支持将数据自动反序列化为 Rust 类型数据一样，CSV writer 也支持那样使用 Serde 将 Rust 类型数据序列化为 CSV 记录数据。在本节中，我们就学习怎么使用它。

与读一样，我们先看看如何序列化一个 Rust 元组。

```rust
fn run() -> Result<(), Box<Error>> {
    let mut wtr = csv::Writer::from_writer(io::stdout());

    // 我们仍然需要手动地写入头部
    wtr.write_record(&["City", "State", "Population", "Latitude", "Longitude"])?;

    // 但现在我们可以通过提供的常用的 Rust 值，写入记录。
    //
    // 注意，奇数列的 `None::<u64>` 预发是必须的，因为 `None` 本身没有具体的类型，但 Serde 需要一个具体的类型以便进行序列化。也就是说，`None` 的类型 `Option<T>`，而 `None::<u64>` 的类型是 `Option<u64>`。
    wtr.serialize(("Davidsons Landing", "AK", None::<u64>, 65.2419444, -165.2716667))?;
    wtr.serialize(("Kenai", "AK", Some(7610), 60.5544444, -151.2583333))?;
    wtr.serialize(("Oakman", "AL", None::<u64>, 33.7133333, -87.3886111))?;

    wtr.flush()?;
    Ok(())
}
```

编译并运行这个程序，期望的输出如下：

```shell
$ cargo build
$ ./target/debug/csvtutor
City,State,Population,Latitude,Longitude
Davidsons Landing,AK,,65.2419444,-165.2716667
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111
```

在上面的例子中，需要注意的关键点是，使用 `serialize` 而不是 `write_record` 进行写入数据。特别地，`write_record` 在写入仅包含字符串数据的简单记录时使用。另一方面，当数据包含更复杂的值时，如数字、浮点数、可选值时，则使用 `serialize`。当然，你总是可以将复杂的值转为字符串，然后再使用 `write_record` 统一写入，但 Serde 可以为你自动的完成上面那些繁琐的工作。

和读一样，我们也可以将自定义结构序列化为 CSV 记录。这样的好处是，结构体中的字段将被识别为头记录。

要将自定义结构写入 CSV 记录，我们需要再次使用 `serde_derive` crate。正如前面章节的[使用 Serde 读取数据](https://blog.burntsushi.net/csv/#reading-with-serde) 所述，我们需要在我们的 Cargo.toml 中的 `[dependencies]` 区块下加上两个 crate 依赖声明（如果没有就加上）。

```
serde = "1"
serde_derive = "1"
```

我们还需要在代码中加上两行外部库的引入代码 `extern crate` ，如下方所示：

```rust
extern crate csv;
extern crate serde;
 #[macro_use]
extern crate serde_derive;

use std::error::Error;
use std::io;
use std::process;

// 记住结构体可以派生两个 trait：Serialize 和 Deserialize！
 #[derive(Debug, Serialize)]
 #[serde(rename_all = "PascalCase")]
struct Record<'a> {
    city: &'a str,
    state: &'a str,
    population: Option<u64>,
    latitude: f64,
    longitude: f64,
}

fn run() -> Result<(), Box<Error>> {
    let mut wtr = csv::Writer::from_writer(io::stdout());

    wtr.serialize(Record {
        city: "Davidsons Landing",
        state: "AK",
        population: None,
        latitude: 65.2419444,
        longitude: -165.2716667,
    })?;
    wtr.serialize(Record {
        city: "Kenai",
        state: "AK",
        population: Some(7610),
        latitude: 60.5544444,
        longitude: -151.2583333,
    })?;
    wtr.serialize(Record {
        city: "Oakman",
        state: "AL",
        population: None,
        latitude: 33.7133333,
        longitude: -87.3886111,
    })?;

    wtr.flush()?;
    Ok(())
}

fn main() {
    if let Err(err) = run() {
        println!("{}", err);
        process::exit(1);
    }
}
```

编译并运行这个示例，虽然我们没有显式地写入头部，但输出和上次是一样的。

```shell
$ cargo build
$ ./target/debug/csvtutor
City,State,Population,Latitude,Longitude
Davidsons Landing,AK,,65.2419444,-165.2716667
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111
```

在这个例子中，可以看到，`serialize` 方法上被标记上了一个结构体字段名。这样做的话，`serialize` 将自动写入一个头部（只要其他记录尚未写入），该记录的结构体的字段是按照他们定义的顺序组成。请注意，可以通过 [`WriterBuilder::has_headers`](https://docs.rs/csv/1.0.0/csv/struct.WriterBuilder.html#method.has_headers) 方法将此行为禁用。

同样值得指出的是，在 `Record` 结构体中使用了一个生命周期参数：

```rust
struct Record<'a> {
    city: &'a str,
    state: &'a str,
    population: Option<u64>,
    latitude: f64,
    longitude: f64,
}
```

`'a` 声明周期参数对应于 `city` 和 `state` 字符串切片的生命周期。这表示 `Record` 结构体包含了 _借用_ 的数据。我们本可以在不借用任何数据的情况下编写结构体，这时也就不需要生命周期参数了：

```rust
struct Record {
    city: String,
    state: String,
    population: Option<u64>,
    latitude: f64,
    longitude: f64,
}
```

然而，由于我们使用 `String` 类型替换 `&str` 类型，我们现在被迫为我们所写的每条记录中的 `city` 和 `state` 分配一个新的 `String` 值。这样做本身没有问题，但会有点性能浪费。

关于序列化的更多示例和详细信息，可以参考 [`Writer::serialize`](https://docs.rs/csv/1.0.0/csv/struct.Writer.html#method.serialize) 方法。

## Pipelining
>* 待续...