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

读取 CSV

现在我们介绍并学会了基本的错误处理,这下我们可以做我们真正要做的事情:处理 CSV 数据。我们前面已经了解了如何从 stdin 中读取 CSV 数据,但本节将介绍如何从文件中读取 CSV 数据,以及如何将 CSV reader 配置为支持读取不同的分隔符或格式策略的数据。

首先,我们修改前面那个示例,以使其接受文件路径参数而不是从 stdin 中读取。

extern crate csv;

use std::env;
use std::error::Error;
use std::ffi::OsString;
use std::fs::File;
use std::process;

fn run() -> Result<(), Box<Error>> {
    let file_path = get_first_arg()?;
    let file = File::open(file_path)?;
    let mut rdr = csv::Reader::from_reader(file);
    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

/// 返回发送给此进程的第一个参数。如果没有,则返回一个错误。
fn get_first_arg() -> Result<OsString, Box<Error>> {
    match env::args_os().nth(1) {
        None => Err(From::from("expected 1 argument, but got none")),
        Some(file_path) => Ok(file_path),
    }
}

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

如果你用上面的代码替换了 src/main.rs 文件的内容,你应该能够重新构建你的项目,尝试运行它:

$ cargo build
$ ./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

这个示例代码包含两部分:

  • 查询程序当前的命令行位置参数。我们将这段代码放入新的函数调用 get_first_arg 中。在函数中,期望第一个参数是文件路径(索引为 1;索引 0 的参数是是可执行文件的名称),因此,如果不存在, get_first_arg 将返回一个错误。
  • 打开文件的代码。在运行时,我们使用 file:open 打开一个文件。如果在打开文件时出现问题,我们将错误返回给其调用者(在这个程序中就是 main)。注意,我们没有将文件内容包装在缓冲区中。CSV reader 在内部会有缓冲区,因此不需要调用者再声明一个缓冲区。

现在是介绍另一个 CSV reader 构造函数的好时机。它使打开 CSV 文件更加便利。而不是使用下面这个:

let file_path = get_first_arg()?;
let file = File::open(file_path)?;
let mut rdr = csv::Reader::from_reader(file);

你可以使用:

let file_path = get_first_arg()?;
let mut rdr = csv::Reader::from_path(file_path)?;

csv::Reader::from_path 会打开文件,并在异常时返回错误。

读取 headers

如果有时间可以看一下 uspop.csv 的内部数据,你会注意到,它的头部记录看起来像下面这样:

City,State,Population,Latitude,Longitude

现在,如果你看看目前所有的示例程序的命令行输出,你会注意到,头部记录从未打印出来,这是为何呢?,默认情况下,CSV reader 将读取 CSV 数据中的第一条记录作为头部,第一行记录通常不作为实际数据。因此,每当你尝试读取或迭代 CSV 数据时,头记录会被跳过。

CSV reader 不会智能地处理头记录,也不会使用任何高深莫测的方法来自动检测第一个记录是否为头记录。相反,如果你不想将第一个记录作为头记录,那么你需要告诉 CSV reader,没有头记录。

要配置 CSV reader 来实现这一点,我们需要使用一个 ReaderBuilder 来构建 CSV reader。这里有个示例。(注意,代码中回到了从 stdin 中读取数据,因为这样的示例更简洁。)

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(false)
        .from_reader(io::stdin());
    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

如果你用我们的 uspop.csv 作为输入构建程序,那么你会看到头记录将被打印出来:

$ cargo build
$ ./target/debug/csvtutor < uspop.csv
StringRecord(["City", "State", "Population", "Latitude", "Longitude"])
StringRecord(["Davidsons Landing", "AK", "", "65.2419444", "-165.2716667"])
StringRecord(["Kenai", "AK", "7610", "60.5544444", "-151.2583333"])
StringRecord(["Oakman", "AL", "", "33.7133333", "-87.3886111"])

如果你需要直接访问头记录,那么你可以使用 Reader::headers 方法,示例如下:

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    {
        // 由于生命周期的原因,我们将此调用嵌套在一个新的词法作用域中。
        let headers = rdr.headers()?;
        println!("{:?}", headers);
    }
    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }
    // 我们可以任意的获取 header。没有必要建立新的作用域进行调用,因为我们再也不需要借用 reader 中的数据。
    let headers = rdr.headers()?;
    println!("{:?}", headers);
    Ok(())
}

在本例中要注意一件有趣的事是,我们对 rdr.headers() 的调用会放在一个独立的作用域中。之所以这样做,是因为 rdr.headers() 返回 reader 内部的 header 借用。此代码中的大括号嵌套的作用域可以让在我们遍历数据之前结束借用。如果我们没有将 rdr.headers() 的调用放在新的作用域中,那么代码将无法通过编译,因为我们不能在尝试借用它头部的同时,又借用 reader 来迭代数据记录。

解决这个问题的另一个方法是克隆 header 记录:

let headers = rdr.headers()?.clone();

这将原本使用来自 CSV reader 的 header 借用转变为拥有一个新值的所有权。这使代码更易懂,但代价是需要新的内存分配存放头记录。

-- 未完待续
posted @ 2020-11-08 17:04  suhanyujie  阅读(420)  评论(0编辑  收藏  举报