【译】用 Rust 实现 csv 解析-part2

Setup

在这一节中,我们会编写一个简单的程序来读取 CSV 数据并以 debug 的方式打印每条记录。这是基于你已经安装了 Rust 工具链,工具链中包含了 Rust 编译器和 Cargo(包管理工具)。

我们以创建一个新的 Cargo 项目作为开始:

$ cargo new --bin csvtutor
$ cd csvtutor

进入 csvtutor 目录,使用你最喜欢的文本编辑器打开 Cargo.toml 文件,向其中新增 csv = "1" 到你的依赖配置块中。此时,你的 Cargo.toml 文件内容应该如下方所示:

[package]
name = "csvtutor"
version = "0.1.0"
authors = ["Your Name"]

[dependencies]
csv = "1"

接下来,我们构建项目。由于你新增了 csv crate 作为依赖,Cargo 会自动下载并编译它。构建项目使用 Cargo 命令:

$ cargo build

在你的 target/debug 目录下,会产生一个新的二进制文件,csvtutor。这一点上这个命令不会做太多,但你可以执行这个二进制文件:

$ ./target/debug/csvtutor
Hello, world!

我们可以让程序做一些有用的事情。程序可以从标准输入读取 csv 数据并在标准输出打印每一条记录。要完成这个程序,先用你喜欢的编辑器打开 src/main.rs,然后用下面的内容替换其中的内容:

// 这可以让你的程序能访问 csv crate
extern crate csv;

// 导入标准库中的 I/O 模块,这样我们可以从标准输入读取内容
use std::io;

// `main` 函数是程序开始执行的地方
fn main() {
    // 从标准输入读取数据并创建一个 CSV 解析器
    let mut rdr = csv::Reader::from_reader(io::stdin());
    // 遍历每一条记录
    for result in rdr.records() {
        // 一旦发生错误,程序将会以不太友好的方式终止
        // 我们后面会优化这里
        let record = result.expect("a CSV record");
        // 以 debug 的方式打印
        println!("{:?}", record);
    }
}

别太担心读不懂代码的意思;我们会在下一节详细说明。现在,重新构建一下项目:

$ cargo build

如果成功了,我们可以尝试运行一下它。但在此之前,我们需要一些示例 CSV 数据!为此,我们将选择随机的 100 个美国城市,以及它们的人口规模和地理坐标。(我们将在整个教程中使用一样的 CSV 数据。)要获取数据,请从 GitHub 下载:

$ curl -LO 'https://raw.githubusercontent.com/BurntSushi/rust-csv/master/examples/data/uspop.csv'

现在,使用 uspop.csv 作为输入,来运行你的程序:

$ ./target/debug/csvtutor < uspop.csv
StringRecord(["Davidsons Landing", "AK", "", "65.2419444", "-165.2716667"])
StringRecord(["Kenai", "AK", "7610", "60.5544444", "-151.2583333"])
StringRecord(["Oakman", "AL", "", "33.7133333", "-87.3886111"])
# ... and much more

基础的异常处理

由于读取 CSV 数据可能会得到异常结果,因此本教程中的示例中是普遍存在的。因此,我们将花一点时间来学习基本的错误处理,特别是修复我们前面的一些示例,以便更友好地显示错误。如果你已经习惯在 Rust 中使用 Resulttry!/?,那么你可以安全地跳过这个部分

请注意 Rust 权威指南中包含了一些通用的异常处理的介绍。如果要更深入的了解,可以看我的 Rust 中的错误处理。如果你打算构建 Rust 库,那么这篇文章尤其重要。

这样一来,Rust 中的错误处理就有两种不同的形式:不可恢复的错误和可恢复的错误。

不可恢复的异常通常是程序中的异常,这些异常可能发生在规则被破坏的时候。此时,你的程序的状态是不可预测的,除了 panic 之外,通常也没有什么其他办法。在 Rust 中,panic 类似于简单地终止程序,但是它会在程序退出之前展开堆栈并清理资源。

另一方面,可恢复异常通常应用于可预测的错误。不存在的文件或者无效的 CSV 数据是可恢复错误的例子。在 Rust 中,可恢复异常是通过 Result 处理的。一个 Result 表示计算成功或者失败的状态。它的定义如下:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

也就是说,Result 在计算成功时包含类型为 T 的值,或者在计算失败时包含 E 类型的值。

不可恢复异常和可恢复异常之间的关系很重要。特别地,强烈建议将可恢复异常当做不可恢复异常。例如,在找不到文件或者 CSV 数据不合法时,使用 panic 不是一个好的实践。相反,可预测的异常应该使用 Rust 的 Result 类型来处理。

有了新了解的知识,让我们重新检查前面的示例并分析它的错误处理。

extern crate csv;

use std::io;

fn main() {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        let record = result.expect("a CSV record");
        println!("{:?}", record);
    }
}

在这个程序中有两个地方可能发生错误。第一个地方是从标准输入读取记录是否有问题。第二个是写入到标准输出是否有问题。一般来说,在本教程中我们将忽略后一个问题,尽管健壮的命令行应用程序应该对它进行处理(例如,当管道坏掉时)。然而,前面那个问题更值得详细研究。例如,如果这个程序的用户提供了无效的 CSV 数据,那么程序会发生 panic:

$ cat invalid
header1,header2
foo,bar
quux,baz,foobar
$ ./target/debug/csvtutor < invalid
StringRecord { position: Some(Position { byte: 16, line: 2, record: 1 }), fields: ["foo", "bar"] }
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: UnequalLengths { pos: Some(Position { byte: 24, line: 3, record: 2 }), expected_len: 2, len: 3 }', /checkout/src/libcore/result.rs:859
note: Run with `RUST_BACKTRACE=1` for a backtrace.

这里发生了什么?首先,我们应该讨论为什么 CSV 数据是无效的。CSV 数据由三条记录组成:一个头部和两条数据记录。头部和第一个数据记录有两个字段,但是第二个数据记录有三个字段。默认情况下, csv crate 将把不一致长度的记录视为错误。(此行为可以使用 ReaderBuilder::flexible配置来切换。)这解释了为什么在本例中只打印了第一个数据记录,因为它的字段数量与头部记录数相同。也就是说,在解析第二个数据记录之前,我们实际没有碰到错误。

(请注意,CSV reader 会自动将第一个记录看做头部。这可以通过 ReaderBuilder::has_headers 配置切换。)

那么究竟是什么导致了我们程序中的 panic 呢?应该是是循环中的第一行代码了:

for result in rdr.records() {
    let record = result.expect("a CSV record"); // 这里会发生 panic
    println!("{:?}", record);
}

这里要理解的关键一点是,rdr.records() 返回一个迭代器,这个迭代器会返回 Result 值。也就是说,它生成的不是记录,而是包含记录或错误的 Result。在 Result 上定义的 expect 方法在 Result 中拿出成功的值。由于 Result 可能包含错误,所以当它包装的是错误时,expect 就会产生 panic。

这里可能会帮助你查看 expect 的具体实现:

use std::fmt;

// This says, "for all types T and E, where E can be turned into a human
// readable debug message, define the `expect` method."
impl<T, E: fmt::Debug> Result<T, E> {
    fn expect(self, msg: &str) -> T {
        match self {
            Ok(t) => t,
            Err(e) => panic!("{}: {:?}", msg, e),
        }
    }
}

由于这导致 panic,如果 CSV 数据是无效的,那么这就是一个完全可预测的错误,我们已经把本应该是可恢复的错误转变成 不可恢复 错误。我们这样做是因为使用不可恢复的错误有时候是有利的。由于这是一种糟糕的实践,所以在本教程的其余部分,我们会尽量避免使用不可恢复错误。

转换成使用可恢复错误

我们将通过 3 个步骤将不可恢复错误转换为可恢复错误。首先,我们先摆脱 panic,手动打印一个错误消息:

extern crate csv;

use std::io;
use std::process;

fn main() {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        // Examine our Result.
        // If there was no problem, print the record.
        // Otherwise, print the error message and quit the program.
        match result {
            Ok(record) => println!("{:?}", record),
            Err(err) => {
                println!("error reading CSV from <stdin>: {}", err);
                process::exit(1);
            }
        }
    }
}

如果我们再次运行程序,我们将会看到一个错误消息,但不再是一个 panic 的消息:

$ cat invalid
header1,header2
foo,bar
quux,baz,foobar
$ ./target/debug/csvtutor < invalid
StringRecord { position: Some(Position { byte: 16, line: 2, record: 1 }), fields: ["foo", "bar"] }
error reading CSV from <stdin>: CSV error: record 2 (line: 3, byte: 24): found record with 3 fields, but the previous record has 2 fields

转换成可恢复错误的第 2 步是将我们的 CSV 记录循环放入一个单独的函数中。然后这个函数可以选择返回遇到的第一个错误,然后我们的 main 函数可以检查并决定如何处理这个错误。

extern crate csv;

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

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

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        // Examine our Result.
        // If there was no problem, print the record.
        // Otherwise, convert our error to a Box<Error> and return it.
        match result {
            Err(err) => return Err(From::from(err)),
            Ok(record) => {
              println!("{:?}", record);
            }
        }
    }
    Ok(())
}

我们的新函数 —— run,其返回值类型是 Result<(), Box<Error>>。简单地说,这表示 run 在成功时不返回任何内容,或者发生错误时,它返回 Box<Error>,它表示“任意类型的错误”。如果我们只关心发生的某一些具体错误,检查 Box<Error> 是比较难的。但出于我们的目的,我们需要做的就是优雅地打印错误消息并退出程序。

第 3 步也是最后一步,使用 Rust 语言的一个特殊特性:问号标记符。

extern crate csv;

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

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

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        // This is effectively the same code as our `match` in the
        // previous example. In other words, `?` is syntactic sugar.
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

最后一步是展示如何使用 ? 自动将错误转发给我们的调用者,而不需要显示地进行 match 匹配处理。我们在使用 ? 时,在本教程中非常重要的一点是,它只能在返回值为 Result 类型的函数中使用。

在结束本节时,我们要提醒一句:使用 Box<Error> 作为错误类型是我们在这里所能接受的最低限度。也就是说,虽然它能让我们的程序优雅地处理错误,但它使调用者很难检查发生的具体错误。然而,由于这是一个关于编写命令行 CSV 解析的教程,因此我们优先考虑自己的需求,使自己满意就行。如果你想了解更多,或者对编写处理 CSV 数据的库感兴趣,那么你可以查阅我的错误处理的博客文章

话虽如此,如果你所做的只是编写一个一次性的程序来执行 CSV 转换,那么在错误发生时使用 expect 和 panic 之类的方法是非常合理的。不过本教程将努力展示平常使用的正常代码。

-- 待续
posted @ 2020-11-01 22:27  suhanyujie  阅读(559)  评论(0编辑  收藏  举报