zoukankan      html  css  js  c++  java
  • 【译】用 Rust 实现 csv 解析-part4

    分隔符,引号和可变长度的记录

    在这一节中,我们将暂时抛开 uspop.csv 数据集,而是展示如何读取一些不太“干净”的 CSV 数据。这个 CSV 数据使用 ; 作为分隔符,带有转义的引号 "(不是"")并且拥有可变长度的记录。下面是一些示例数据,如果你知道 WWE 的话,可以看出其中是一些 WWE 摔跤手及其出生年份的名单:

    $ cat strange.csv
    ""Hacksaw" Jim Duggan";1987
    "Bret "Hit Man" Hart";1984
    # We're not sure when Rafael started, so omit the year.
    Rafael Halperin
    ""Big Cat" Ernie Ladd";1964
    ""Macho Man" Randy Savage";1985
    "Jake "The Snake" Roberts";1986
    

    要读取这些 CSV 数据,我们按照下面的思路来:

    • 1.不读取 header,因为没有 header 部分
    • 2.将分隔符由 , 改为 ;
    • 3.将双引号(如 "")的后半部分转义(如"
    • 4.支持灵活的可变长度记录,因为其中可能会有年份缺省。
    • 5.忽略行首是 # 的行

    所有这些(也许有更多其它的)都可以通过配置 ReaderBuilder 来实现,如下方示例:

    fn run() -> Result<(), Box<Error>> {
        let mut rdr = csv::ReaderBuilder::new()
            .has_headers(false)
            .delimiter(b';')
            .double_quote(false)
            .escape(Some(b'\'))
            .flexible(true)
            .comment(Some(b'#'))
            .from_reader(io::stdin());
        for result in rdr.records() {
            let record = result?;
            println!("{:?}", record);
        }
        Ok(())
    }
    

    现在重新编译项目并以 strange.csv 作为输入参数来运行它:

    $ cargo build
    $ ./target/debug/csvtutor < strange.csv
    StringRecord([""Hacksaw" Jim Duggan", "1987"])
    StringRecord(["Bret "Hit Man" Hart", "1984"])
    StringRecord(["Rafael Halperin"])
    StringRecord([""Big Cat" Ernie Ladd", "1964"])
    StringRecord([""Macho Man" Randy Savage", "1985"])
    StringRecord(["Jake "The Snake" Roberts", "1986"])
    

    你应该多尝试一些其他设置。可能会发生一些有趣的事:

    • 1.如果你删除了 escape 设置,解析时不会报 CSV 错误。相反,记录仍然被正常解析。这是 CSV 解析器的一个特性。即使它得到的数据有一点错误,它仍然能进行一定程度上的解析。因为考虑到实际使用时,CSV 数据可能有一定的错误。这是一个有用的特性。
    • 2.如果删除了 delimiter 设置,解析仍然成功,尽管每个记录只有一个字段。
    • 3.如果删掉 flexible 设置,读取器将会打印前两条记录(因为它们有相同数量的字段),但在第三条记录上返回解析错误,因为它只有一个字段。

    这涵盖了你希望在 CSV 读取器上的所有配置项,尽管还有一些其他的配置。如,你可以将记录终止符从换行改为其他字符。(默认情况下,终止符是 CRLF,它将 分别视为单个记录的终止符。)相关详情,可以参考 ReaderBuilder

    基于 Serde 读取

    我们的库最重要的特性之一是对 Serde 的支持。Serde 是一个自动将数据序列化和反序列化的 Rust 框架。简单地说,通过它,我们不是将记录作为字符串数组来处理,而是特定类型数据的数组。

    我们看看 uspop.csv 中的示例数据:

    City,State,Population,Latitude,Longitude
    Davidsons Landing,AK,,65.2419444,-165.2716667
    Kenai,AK,7610,60.5544444,-151.2583333
    

    数据记录中有些字段可以作为字符串处理,如 (City, State),另一些则类似于数值。如,Population 那一列,看起来它包含整形值,同时 LatitudeLongitude 则看起来包含小数。如果我们想要将这些字段转换为“适当”的类型,那么我们需要做大量的工作。下面是一些相关操作的示例:

    fn run() -> Result<(), Box<Error>> {
        let mut rdr = csv::Reader::from_reader(io::stdin());
        for result in rdr.records() {
            let record = result?;
    
            let city = &record[0];
            let state = &record[1];
            // 一些记录会丢失 population 的计数,所以如果我们无法解析一个数值,则视 population 列的统计为丢失,而不是返回一个错误。
            let pop: Option<u64> = record[2].parse().ok();
            // 我们太幸运了!所有记录中的 Latitudes 和 longitudes 列都是正常的。因此,如果有一条记录不能解析,则犯规返回错误。
            let latitude: f64 = record[3].parse()?;
            let longitude: f64 = record[4].parse()?;
    
            println!(
                "city: {:?}, state: {:?}, 
                 pop: {:?}, latitude: {:?}, longitude: {:?}",
                city, state, pop, latitude, longitude);
        }
        Ok(())
    }
    

    这里的问题是我们需要手动解析每一个字段的数据,这些算是劳动密集型和重复型的工作。而 Serde 可以让这个过程自动化。例如,我们可以将每一条记录反序列化为元组类型:(String, String, Option<u64>, f64, f64)

    // 这里引入了一些类型别名,这样我们可以便利地使用我们的记录类型。
    type Record = (String, String, Option<u64>, f64, f64);
    
    fn run() -> Result<(), Box<Error>> {
        let mut rdr = csv::Reader::from_reader(io::stdin());
        // 这里不再是一个 `records` 方法的迭代器,而是 `deserialize` 方法的迭代器。
        for result in rdr.deserialize() {
            // 我们必须告诉 Serde 我们想要的反序列化后的目标类型。
            let record: Record = result?;
            println!("{:?}", record);
        }
        Ok(())
    }
    

    运行代码,可以看到形如下面的输出:

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

    以这种方式使用 Serde 有个缺点,那就是必须按顺序精确匹配记录中的每一个字段。如果 CSV 数据中有头记录,这可能是一个问题,因为你可能倾向于将每个值存在一个命名了的字段中,而不是一个数值编号的字段中。实现这一点的一种方法是将记录反序列化为 HashMapBTreeMap。特别地,下一个例子所展示的,与之前示例有所不同,那就是使用类型别名的定义和使用关键字 use 从标准库中导入的 HashMap:

    use std::collections::HashMap;
    
    // 这里使用类型别名,以便于我们引用记录的类型。
    type Record = HashMap<String, String>;
    
    fn run() -> Result<(), Box<Error>> {
        let mut rdr = csv::Reader::from_reader(io::stdin());
        for result in rdr.deserialize() {
            let record: Record = result?;
            println!("{:?}", record);
        }
        Ok(())
    }
    

    运行这个程序后显示的结果和之前类似,不同的是每条记录会以 map 的方式打印:

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

    如果你需要读取带有头记录的 CSV 数据,但确切的结构需要运行时才确定,那么下面这个方法更有效。然而,在我们的例子中,我们已知 uspop.csv 中的数据结构。特别地,使用 HashMap 方法,当我们将每条记录反序列化为 (String, String, Option<u64>, f64, f64) 时,我们丢失了前面示例中每个字段的类型说明。能否有一种方法来识别字段对应的头名称,并分配每个字段一个确定的类型呢?答案是肯定的,但是首先我们要引入一个名为 serde_derive 的 crate。你可以通过将它添加到你的依赖声明 Cargo.toml 文件的[dependencies] 中:

    serde = "1"
    serde_derive = "1"
    

    将这些 crate 添加到我们的项目后,我们现在可以定义记录的结构体。然后,我们让 Serde 自动实现从 CSV 记录填充数据到结构体实例中的粘合代码。下一个示例将展示具体操作。注意不要忽视 extern crate 行!

    extern crate csv;
    extern crate serde;
    // 这可以让我们可以编写 `#[derive(Deserialize)]` 声明。
     #[macro_use]
    extern crate serde_derive;
    
    use std::error::Error;
    use std::io;
    use std::process;
    
    // 我们无需 derive `Debug`(它无需 Serde),但对于所有类型来说,把它加上是一个不错的习惯。
    //
    // 注意结构体中的字段名不是按照 CSV 数据中的顺序!
     #[derive(Debug, Deserialize)]
     #[serde(rename_all = "PascalCase")]
    struct Record {
        latitude: f64,
        longitude: f64,
        population: Option<u64>,
        city: String,
        state: String,
    }
    
    fn run() -> Result<(), Box<Error>> {
        let mut rdr = csv::Reader::from_reader(io::stdin());
        for result in rdr.deserialize() {
            let record: Record = result?;
            println!("{:?}", record);
            // 如果你不喜欢所有的内容缩卷到一行,可以试试下面这个打印:
            // println!("{:#?}", record);
        }
        Ok(())
    }
    
    fn main() {
        if let Err(err) = run() {
            println!("{}", err);
            process::exit(1);
        }
    }
    

    编译并运行这个程序,可以看到跟之前类似的输出:

    $ cargo build
    $ ./target/debug/csvtutor < uspop.csv
    Record { latitude: 65.2419444, longitude: -165.2716667, population: None, city: "Davidsons Landing", state: "AK" }
    Record { latitude: 60.5544444, longitude: -151.2583333, population: Some(7610), city: "Kenai", state: "AK" }
    Record { latitude: 33.7133333, longitude: -87.3886111, population: None, city: "Oakman", state: "AL" }
    

    同样,我们根本不需要改变 run 函数,我们仍然使用 deserialize 迭代器遍历记录,这是我们在本节开始时用到的迭代器。在这个例子中唯一变化的是 新定义的Record 类型和两个新的 extern crate 语句。我们的记录的类型现在是我们定义的自定义类型,而不是类型别名,因此,Serde 默认情况下不知道如何对它反序列化。但是有一个特殊的编译器插件 serde_derive,它可以在编译时读取你声明的结构体,并生成代码,将 CSV 记录反序列化为 Record 值。若要查看没有自动派生会发生什么,请将 #[derive(Debug, Deserialize)] 改为 #[derive(Debug)]

    在这个例子中,还有一点需要注意,那就是使用 #[serde(rename_all = "PascalCase")]。这个指令将帮助 Serde 把你的结构体的字段映射到 CSV 数据的头部名称。如果你还记得,我们的头记录如下:

    City,State,Population,Latitude,Longitude
    

    注意,每个名称首字母是大写的,但结构体中的字段没有。#[serde(rename_all = "PascalCase")] 注解指令通过解析 PascalCase 中的每个字段来解决该问题,其中字段的第一个字母是大写的。如果我们没有告诉 Serde 关于名称重映射的信息,那么程序将会退出,并报异常:

    $ ./target/debug/csvtutor < uspop.csv
    CSV deserialize error: record 1 (line: 2, byte: 41): missing field `latitude`
    

    我们本可以通过其他方法解决这个问题。如,我们可以使用首大写字母的标识符作为字段名:

    #[derive(Debug, Deserialize)]
    struct Record {
        Latitude: f64,
        Longitude: f64,
        Population: Option<u64>,
        City: String,
        State: String,
    }
    

    然而,这违反了 Rust 的命名风格。(事实上,Rust 编译器甚至会给你警告,你的命名不符合约定!)

    解决这个问题的另一个方法是要求 Serde 单独重命名每个字段。当字段到头部的映射不一致时,这个方法很有用:

    #[derive(Debug, Deserialize)]
    struct Record {
        #[serde(rename = "Latitude")]
        latitude: f64,
        #[serde(rename = "Longitude")]
        longitude: f64,
        #[serde(rename = "Population")]
        population: Option<u64>,
        #[serde(rename = "City")]
        city: String,
        #[serde(rename = "State")]
        state: String,
    }
    

    To read more about renaming fields and about other Serde directives, please consult the Serde documentation on attributes.

    要阅读更多关于重命名字段和 Serde 的指令信息,可以参考 Serde 属性文档

  • 相关阅读:
    apache安全—用户访问控制
    hdu 3232 Crossing Rivers 过河(数学期望)
    HDU 5418 Victor and World (可重复走的TSP问题,状压dp)
    UVA 11020 Efficient Solutions (BST,Splay树)
    UVA 11922 Permutation Transformer (Splay树)
    HYSBZ 1208 宠物收养所 (Splay树)
    HYSBZ 1503 郁闷的出纳员 (Splay树)
    HDU 5416 CRB and Tree (技巧)
    HDU 5414 CRB and String (字符串,模拟)
    HDU 5410 CRB and His Birthday (01背包,完全背包,混合)
  • 原文地址:https://www.cnblogs.com/ishenghuo/p/13973467.html
Copyright © 2011-2022 走看看