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 属性文档

  • 相关阅读:
    grape入门
    半个小时写的一个二叉搜索树,实现了增,删,查功能
    Struts2 MVC 同 Spring MVC 的比较
    阿里巴巴 2016 java 实习岗位笔试题(昨天出炉)
    自己用20分钟java实现的单向链表(含有增删改查操作)
    关于 古人劝学 --写的真心是好 真的有收获
    JDK动态代理堆栈图详解--干货
    论闷声挣大钱与网红现象
    spring beanfactory --实现自己简单的 bean工厂类
    Spring IOC example one
  • 原文地址:https://www.cnblogs.com/ishenghuo/p/13973467.html
Copyright © 2011-2022 走看看