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

管道操作

In this section, we’re going to cover a few examples that demonstrate programs that take CSV data as input, and produce possibly transformed or filtered CSV data as output. This shows how to write a complete program that efficiently reads and writes CSV data. Rust is well positioned to perform this task, since you’ll get great performance with the convenience of a high level CSV library.

在这一节中,我们将介绍几个示例,这些示例把 CSV 数据作为输入,并对其进行转换或过滤等操作,再将结果输出。这是一个完整的有效读写 CSV 数据的程序流程。Rust 可以很好地完成这个任务,因为你可以利用高级地 CSV 库的便利性来获得出色的性能。

搜索过滤

The first example of CSV pipelining we’ll look at is a simple filter. It takes as input some CSV data on stdin and a single string query as its only positional argument, and it will produce as output CSV data that only contains rows with a field that matches the query.

我们把看到的 CSV 管道示例视为一个简单的过滤器。它把 stdin 中的一些 CSV 数据和单个字符查询作为位置参数,并且它将返回与查询相匹配的字段对应的 CSV 数据记录作为输出。

extern crate csv;

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

fn run() -> Result<(), Box<Error>> {
    // 从位置参数获取查询
    // 如果没有参数,则返回错误
    let query = match env::args().nth(1) {
        None => return Err(From::from("expected 1 argument, but got none")),
        Some(query) => query,
    };

   // 构建 CSV reader 和 writer,分别从 stdin 进行 CSV 读取和写入到 stdout。
    let mut rdr = csv::Reader::from_reader(io::stdin());
    let mut wtr = csv::Writer::from_writer(io::stdout());

   // 读取记录之前,我们应该写入头部记录
    wtr.write_record(rdr.headers()?)?;

    // 迭代 `rdr` 中的所有记录,只写入匹配的记录
    // 从 `query` 到 `wtr`.
    for result in rdr.records() {
        let record = result?;
        if record.iter().any(|field| field == &query) {
            wtr.write_record(&record)?;
        }
    }

    // CSV writer 使用内部的 buffer,所以当完成的时候,将其刷新。
    wtr.flush()?;
    Ok(())
}

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

如果编译这段程序,并用它查询 uspop.csv 中 MA 相关的数据,可以看到结果中仅有一条符合:

$ cargo build
$ ./csvtutor MA < uspop.csv
City,State,Population,Latitude,Longitude
Reading,MA,23441,42.5255556,-71.0958333

这个示例实际上并没有引入新的东西。它只是结合了前面几节中介绍到的 CSV reader 和 writer 知识点。

让我们在这个例子中添加一个分支。在实际使用中,你可能会遇到没有正确编码的混乱数据。你可能会遇到使用 [Latin-1](https://e讲的例子都是基于 UTF-8 编码的数据。由于我们处理的所有数据都是 ASCII(它是 Latin-1 和 UTF-8 的子集),所以没有出现问题。但下面我们试试一个稍微修改过的 uspop.csv 文件,它包含一个无效的 Latin-1 编码的非法 UTF-8 字符。你可以下载它:

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

尽管我提出了这个问题,但还是让我们看看,当我们在新数据上运行我们之前的例子会发生什么:

$ ./csvtutor MA < uspop-latin1.csv
City,State,Population,Latitude,Longitude
CSV parse error: record 3 (line 4, field: 0, byte: 125): invalid utf-8: invalid UTF-8 in field 0 near byte index 0

错误消息表示有异常发生。我们看一下第 4 行是如何处理的:

$ head -n4 uspop-latin1.csv | tail -n1
Õakman,AL,,33.7133333,-87.3886111

在上面的例子中,第一个字符是 Latin-1 字符 Õ,它被编码为字节 0xD5,这是一个无效的 UTF-8。现在 CSV 解析器被阻碍在异常数据这边,我们该怎么办?有两个办法。第一种是修复 CSV 数据,使其变为有效的 UTF-8 数据。无论如何,这是一个不错的方法,比如像 iconv 这种库可以帮助完成数据编码的转换任务。但是,如果你不能或者不想这样做,你可以换一种方法,以一种与编码无关的方式读取 CSV 数据(只要 ASCII 是合法的字符子集)。方法的关键就是使用字节记录,而非 字符串记录

到目前为止,我们实际上还没有深入地讨论过代码中的记录类型,但是现在是时候介绍它们了。有两种方式,StringRecordByteRecord。这两者中的任意一个都可以表示一条记录,一条记录由若干个字段序列组成。StringRecordByteRecord 的唯一区别是 StringRecord 必须是有效的 UTF-8 数据,而 ByteRecord 则包含任意的字节。需要明确的是,这两种类型在内存结构上的表示是一样的。

有了这些知识,我们现在就可以开始理解为什么在运行上面关于不合法的 UTF-8 数据的示例时,我们遇到报错了。也就是说,当我们调用 records 时,我们会得到一个 StringRecord 的迭代器。由于 StringRecord 需要数据是有效的 UTF-8,因此当我们使用不合法的 UTF-8 数据去构建 StringRecord 时,则会导致我们看到那些错误。

要使我们的示例代码能运行,我们需要做的就是从 StringRecord 转换到 ByteRecord。这意味着使用 byte_records 来创建迭代器,而非 records,类似的,头部数据也可能是非法 UTF-8,因此也要使用 byte_headers 替代 headers。变化如下:

fn run() -> Result<(), Box<Error>> {
    let query = match env::args().nth(1) {
        None => return Err(From::from("expected 1 argument, but got none")),
        Some(query) => query,
    };

    let mut rdr = csv::Reader::from_reader(io::stdin());
    let mut wtr = csv::Writer::from_writer(io::stdout());

    wtr.write_record(rdr.byte_headers()?)?;

    for result in rdr.byte_records() {
        let record = result?;
        // `query` 是 `String`,field 现在是 `&[u8]`,所以我们在比较之前,需要将`query` 转换为 `&[u8]`。
        if record.iter().any(|field| field == query.as_bytes()) {
            wtr.write_record(&record)?;
        }
    }

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

编译并运行,现在迭代的结果和我们的首个示例一样,但这次是基于非法 UTF-8 数据运行的。

$ cargo build
$ ./csvtutor MA < uspop-latin1.csv
City,State,Population,Latitude,Longitude
Reading,MA,23441,42.5255556,-71.0958333

对 population 计数进行过滤

在本节中,我们将展示另一个示例程序,它可以读写 CSV 数据,除此之外,不再是处理任意的记录,而是使用 Serde 对具有特殊标记的记录进行序列化和反序列化。

对于这个程序,我们希望能够根据 population 统计的数量来过滤掉一些记录。具体来说,我们像看看哪些记录符合特定的人口阈值。除了使用一个简单的不等式外,我们还需考虑哪些记录缺少人口统计。这时,像 Option<T> 这样的类型就派上用场了,因为编译器会“强迫”我们考虑人口计数缺失的异常情况。

在这个示例中,因为我们使用 Serde,因此如果你没有声明依赖,不要忘记增加 Serde 依赖到 Cargo.toml 文件的 [dependencies] 区块下:

serde = "1"
serde_derive = "1"

现在,代码如下所示:

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

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

// Unlike previous examples, we derive both Deserialize and Serialize. This
// means we'll be able to automatically deserialize and serialize this type.
 #[derive(Debug, Deserialize, Serialize)]
 #[serde(rename_all = "PascalCase")]
struct Record {
    city: String,
    state: String,
    population: Option<u64>,
    latitude: f64,
    longitude: f64,
}

fn run() -> Result<(), Box<Error>> {
    // Get the query from the positional arguments.
    // If one doesn't exist or isn't an integer, return an error.
    let minimum_pop: u64 = match env::args().nth(1) {
        None => return Err(From::from("expected 1 argument, but got none")),
        Some(arg) => arg.parse()?,
    };

    // Build CSV readers and writers to stdin and stdout, respectively.
    // Note that we don't need to write headers explicitly. Since we're
    // serializing a custom struct, that's done for us automatically.
    let mut rdr = csv::Reader::from_reader(io::stdin());
    let mut wtr = csv::Writer::from_writer(io::stdout());

    // Iterate over all the records in `rdr`, and write only records containing
    // a population that is greater than or equal to `minimum_pop`.
    for result in rdr.deserialize() {
        // Remember that when deserializing, we must use a type hint to
        // indicate which type we want to deserialize our record into.
        let record: Record = result?;

        // `map_or` is a combinator on `Option`. It take two parameters:
        // a value to use when the `Option` is `None` (i.e., the record has
        // no population count) and a closure that returns another value of
        // the same type when the `Option` is `Some`. In this case, we test it
        // against our minimum population count that we got from the command
        // line.
        if record.population.map_or(false, |pop| pop >= minimum_pop) {
            wtr.serialize(record)?;
        }
    }

    // CSV writers use an internal buffer, so we should always flush when done.
    wtr.flush()?;
    Ok(())
}

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

如果我们以最小阈值 100000 参数运行这个程序,我们可以看到 3 条匹配的结果。注意,我们可从未显式地写过头部声明,但头部却能够被准确地显示出来。

$ cargo build
$ ./target/debug/csvtutor 100000 < uspop.csv
City,State,Population,Latitude,Longitude
Fontana,CA,169160,34.0922222,-117.4341667
Bridgeport,CT,139090,41.1669444,-73.2052778
Indianapolis,IN,773283,39.7683333,-86.1580556

Performance

待续...

posted @ 2021-02-05 20:46  suhanyujie  阅读(70)  评论(0编辑  收藏  举报