Rust 如何正确解析 yaml 文件

437 阅读11分钟

一、简介

serde_yaml 是 Rust 语言中一个用于序列化和反序列化 YAML 数据的库。它以其高性能和灵活性而广受欢迎,特别适用于需要处理 YAML 配置文件的项目。在这篇博客中,我们将探讨 serde_yaml 的基本用法,并提供一些实用的示例代码。

二、使用场景

serde_yaml 常用于以下场景:

  • 配置管理:许多应用程序使用 YAML 文件来存储配置参数,serde_yaml 可以轻松解析这些文件。
  • 数据交换:在系统之间传递数据时,YAML 格式因其可读性而常被选用。
  • 文档生成:一些工具使用 YAML 来描述文档结构,serde_yaml 可以帮助生成和解析这些文档。

三、基本使用

开始使用 serde_yaml 之前,先创建一个 serde_yaml_test 项目文件夹,在文件夹中使用 cargo init 初始化 rust 项目,然后在项目的 Cargo.toml 文件中添加依赖:

serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9.34"

要使用最新版本的 serde 的话可以使用命令行来添加依赖,如:

 cargo add serde --features="derive"
 cargo add serde_yaml

依赖的包都准备完成后,就来看看几个示例,演示如何读取、解析和生成 YAML 文件。

1、读取和解析 YAML 文件

以下是一个基本示例,展示了如何使用 serde_yaml 读取和解析一个名为 以下是一个基本示例,展示了如何使用 serde_yaml 读取和解析一个名为 hello.yml 的文件: 的文件:

# 在项目根目录创建一个 hello.yml 文件
touch hello.yml
# 此时目录结构为
#|-serde_yaml_test
#   |--src
#   |--target
#   |--.gitignore
#   |--Cargo.lock
#   |--Cargo.toml
#   |--hello.yml
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;
use serde_yaml;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut file = File::open("hello.yml")?; // 打开文件 hello.yml
    let mut contents = String::new(); // 创建一个字符串
    file.read_to_string(&mut contents)?; // 读取文件内容到字符串缓冲区
    println!("YAML file contents:\n{}", contents); // 打印原始的YAML字符串
    // 将YAML字符串反序列化为BTreeMap
    let deserialized_map: BTreeMap<String, serde_yaml::Value> = serde_yaml::from_str(&contents)?;
    println!("Deserialized map: {:#?}", deserialized_map);  // 打印反序列化后的数据结构
    if let Some(name) = deserialized_map.get("name").and_then(|v| v.as_str()) {
        println!("name: {}", name); // 打印 name
    } else {
        println!("The key 'name' was not found or is not a string.");
    }
    Ok(())
}
// 输出结果:
//    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
//     Running `target\debug\serde_yaml_test.exe`
// YAML file contents:
// name: data
// Deserialized map: {
//     "name": String("data"),
// }
// name: data

2、序列化数据结构为 YAML

示例展示了如何将一个 Rust 数据结构序列化为 YAML 格式的字符串:

use std::collections::BTreeMap;
use serde_yaml;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 创建一个 BTreeMap 数据结构
    let mut data = BTreeMap::new();
    data.insert("name".to_string(), serde_yaml::Value::String("Alice".to_string()));
    data.insert("age".to_string(), serde_yaml::Value::Number(serde_yaml::Number::from(30)));
    // 将数据结构序列化为 YAML 字符串
    let yaml_string = serde_yaml::to_string(&data)?;
    // 打印序列化后的 YAML 字符串
    println!("Serialized YAML:\n{}", yaml_string);
    Ok(())
}

3、使用自定义结构体进行反序列化

使用自定义结构体来解析 YAML 数据会更加方便和直观。以下示例展示了如何定义一个结构体并解析 YAML 数据:

use serde::{Deserialize, Serialize};
use serde_yaml;

#[derive(Debug, Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 定义一个 YAML 字符串
    let yaml_str = "
name: Bob
age: 25
";
    // 将 YAML 字符串反序列化为 Person 结构体
    let person: Person = serde_yaml::from_str(yaml_str)?;
    // 打印反序列化后的结构体
    println!("Deserialized person: {:?}", person);
    Ok(())
}

4、从文件流中直接解析

有时,为了优化内存使用,可以直接从文件流中进行解析:

use serde::{Deserialize, Serialize};
use std::fs::File;

#[derive(Debug, Serialize, Deserialize)]
struct Config {
    database_url: String,
    port: u16,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 打开配置文件
    let file = File::open("config.yml")?;
    // 从文件流中直接反序列化
    let config: Config = serde_yaml::from_reader(file)?;
    // 打印反序列化后的配置
    println!("Config: {:?}", config);
    Ok(())
}

四、复杂 YAML 文件解析

基于以上案例,来尝试解析一个复杂的 yaml 文件,既然要复杂,那就尝试解析一下 docker-compose.yml文件吧!先来准备一个 yaml 文件,放在项目根目录下,如下:

version: "3.9"
services:
  redis:
    image: redis:latest
    restart: always
    privileged: true
    ports:
      - 6379:6379
    volumes:
      - ./data/redis/data:/data
      - ./redis/etc:/usr/local/etc/redis
    command: redis-server /usr/local/etc/redis/redis.conf
  nginx:
    image: nginx:latest
    restart: always
    privileged: true
    volumes:
      - ./nginx/etc/nginx:/etc/nginx
      - ./var/www:/var/www/html
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - redis
networks:
  mynet1:
    driver: overlay
    attachable: false

再写代码之前,先把 rust 引入的包先贴出来,后面贴代码时可以少写一点啰嗦代码,只关注核心逻辑,看起来更清晰易懂,这是整个项目所依赖的包,也没有很多

use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::fs::File;
use std::io::Read;
use serde::{Deserialize, Serialize};

1.固定字段解析

整个 yml 文件看下来,并没有很多,但算是一个比较全面的 docker-compose 配置文件了,虽然没有用到所有的配置字段,触类旁通,再多的配置字段都能轻车熟路的解析吧,先一级一级的看,一级有 version services networks,通过以上,就可以定义出需要的结构体了:

#[derive(Debug, Serialize, Deserialize)]
struct DockerYaml {
    version: String,
    services: HashMap<String, ServiceCfg>,
}

这里的 services 的类型用的是 HashMap,因为 services 名称是一个变量,而且数量不固定,比如还可以增加 mysql 服务,所以 services 是一个动态,services 里面又引入了 ServiceCfg 类型,现在继续看 services 下一级的配置字段,其中 image 是必须要存在的配置,那 ServiceCfg 结构体可以定义成:

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
    image: String,
}

好了,就这么简单,先写个 main 函数把代码跑起来看看吧!

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut file = File::open("docker-compose.yml")?; // 打开文件
    let mut contents = String::new(); // 创建一个字符串
    file.read_to_string(&mut contents)?; // 读取文件内容到字符串缓冲区
    // println!("读取到的 YAML 内容:\n{}", contents); // 打印原始的YAML字符串
    // 将 YAML 字符串反序列化为 DockerYaml 结构体
    let docker_yaml: DockerYaml = serde_yaml::from_str(&contents)?;
    // 打印反序列化后的结构体
    println!("反序列化后的结构体: {:?}", docker_yaml);
    // 读取出 services
    let services = &docker_yaml.services;
    for (serve, config) in services {
        println!("services name: {}", serve);
        println!("services config: {:?}", config);
    }
    Ok(())
}
// 打印的内容:
// 反序列化后的结构体: DockerYaml { version: "3.9", services: {"nginx": ServiceCfg { image: "nginx:latest" }, "redis": ServiceCfg { image: "redis:latest" }} }
// services name: nginx
// services config: ServiceCfg { image: "nginx:latest" }
// services name: redis
// services config: ServiceCfg { image: "redis:latest" }

正常打印了,现在可以完善 ServiceCfg 这个结构体了

2.动态字段解析

docker-compose 配置的 services 中,大部分配置都可选的,如果不配置,就会有个默认值,先不关心这个默认值是什么,但起码要实现一个不配置会有默认值的功能,带着疑问,先去写一下代码,

把 ServiceCfg 结构体修改成以下的样子:

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
    image: String,
    restart: String,
}

只是添加了一个 restart 配置项,main 函数不变,cargo run 一下发现正常打印了如下:

反序列化后的结构体: DockerYaml { version: "3.9", services: {"nginx": ServiceCfg { image: "nginx:latest", restart: "always" }, "redis": ServiceCfg { image: "redis:latest", restart: "always" }} }
services name: nginx
services config: ServiceCfg { image: "nginx:latest", restart: "always" }
services name: redis
services config: ServiceCfg { image: "redis:latest", restart: "always" }

restart 配置项正常读取到了,好像一切都很完美,但 restart 配置是个可选配置,那在 docker-compose.yml  文件中随便挑一个 restart 配置注释掉看看

version: "3.9"
services:
  redis:
    image: redis:latest
    # restart: always
    privileged: false

再次运行 cargo run 就会发现报错了,报错很明显,缺少 restart 这个配置

Error: Error("services.redis: missing field `restart`", line: 4, column: 5)
error: process didn't exit successfully: `target\debug\serde_yaml_test.exe` (exit code: 1)

要解决这个问题很简单,serde 提供了一个字段属性可以快速设置这个字段的默认值,

#[serde(default)] 如果反序列化时该值不存在,使用Default::default()生成默认值

在 ServiceCfg.restart 中加上就可以,所以把 ServiceCfg 结构体修改成以下的样子:

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
    image: String,
    #[serde(default)]
    restart: String,
}
// 再次执行 cargo run 后会得到以下输出
// 反序列化后的结构体: DockerYaml { version: "3.9", services: {"nginx": ServiceCfg { image: "nginx:latest", restart: "always" }, "redis": ServiceCfg { image: "redis:latest", restart: "" }} }
// services name: nginx
// services config: ServiceCfg { image: "nginx:latest", restart: "always" }
// services name: redis
// services config: ServiceCfg { image: "redis:latest", restart: "" }

自此,可选配置的默认值问题就解决了,但又有个问题,ports 和 volumes 该使用什么数据类型来承载呢,其实 yml 文件中,所有以 - 开头的,都表示同一级下有多个值,就那 redis 的 volumes 来说,他表示要映射本地多个数据卷到容器中,具体多少个?不知道,那这种结构是不是有点像可变长度的数组,在 rust 中最适合这种结构的类型是不是就是 Vec 了,基于此最终完善 ServiceCfg 结构体的内容如下:

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
    image: String,
    #[serde(default)]
    restart: String,
    #[serde(default)]
    privileged: bool,
    #[serde(default)]
    ports: Vec<String>,
    #[serde(default)]
    volumes: Vec<String>,
    #[serde(default)]
    command: String,
    #[serde(default)]
    depends_on: Vec<String>,
}

现在可以随意注释掉 services 中的除 image 配置外的所有配置了,而且解析也不会报错,也有默认值了,把 networks 配置也一并实现了吧

#[derive(Debug, Serialize, Deserialize)]
struct DockerYaml {
    version: String,
    services: HashMap<String, ServiceCfg>,
    #[serde(default)]
    networks: HashMap<String, NetworkCfg>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
    image: String,
    #[serde(default)]
    restart: String,
    #[serde(default)]
    privileged: bool,
    #[serde(default)]
    ports: Vec<String>,
    #[serde(default)]
    volumes: Vec<String>,
    #[serde(default)]
    command: String,
    #[serde(default)]
    depends_on: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct NetworkCfg {
    #[serde(default)]
    driver: Option<String>,
    #[serde(default)]
    attachable: Option<bool>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut file = File::open("docker-compose.yml")?; // 打开文件
    let mut contents = String::new(); // 创建一个字符串
    file.read_to_string(&mut contents)?; // 读取文件内容到字符串缓冲区
    // println!("读取到的 YAML 内容:\n{}", contents); // 打印原始的YAML字符串
    // 将 YAML 字符串反序列化为 DockerYaml 结构体
    let docker_yaml: DockerYaml = serde_yaml::from_str(&contents)?;
    // 打印反序列化后的结构体
    println!("反序列化后的结构体: {:?}", docker_yaml);
    // 读取出 services
    let services = &docker_yaml.services;
    for (serve, config) in services {
        println!("services name: {}", serve);
        println!("services config: {:?}", config);
    }
    // 读取出 networks
    let networks = &docker_yaml.networks;
    for (serve, config) in networks {
        println!("networks name: {}", serve);
        println!("networks config: {:?}", config);
    }
    // 将数据结构序列化为 YAML 字符串
    let yaml_string = serde_yaml::to_string(&docker_yaml)?;
    println!("序列化为 YAML 字符串:\n{}", yaml_string); // 打印序列化后的 YAML 字符串
    Ok(())
}

因为 networks 配置也是一个可选配置,所以在 DockerYaml 结构体中要加上 #[serde(default)]字段属性,至此,读取 yml 文件算是完成了,但看上面的 main 函数,把反序列化的结构体再转换为 yml 文件,就会有点点小问题,实际上上面代码执行完后会生成一个跟读取的 yml 文件不一样内容的 yml 字符串出来,如果期望读取的什么 yml 生成的 yml 也一模一样怎么办?

3.序列化时剔除默认配置

需要可选字段在未设置时在反序列时有生成默认值,但在序列化时把默认值去除,可以使用 serde 的 #[serde(skip_serializing_if = "path")] 调用一个函数来确定是否跳过序列化该字段。给定的函数必须可以作为fn(&T)->bool调用,尽管它可能是T上的泛型。例如skip_serializing_if="Option::is_none"如果值是 None,那么就会跳过。

现在来改造一下上面的 ServiceCfg :

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
    image: String,
    #[serde(skip_serializing_if = "String::is_empty", default)]
    restart: String,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    privileged: Option<bool>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    ports: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    volumes: Vec<String>,
    #[serde(skip_serializing_if = "String::is_empty", default)]
    command: String,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    depends_on: Vec<String>,
}

这里有个细节是 privileged 属性由原来的 bool 类型换成了 Option<bool>, 其中缘由就是 bool 类型不好做类型默认值判断, 像其他的 String,Vec,Option 都有对应的函数给 skip_serializing_if 做默认值判断,换成 Option 后,解析时如果遇到未配置时就会使用 None 代替原来的 bool 默认值,因为 Option 类型的默认值就是 None ,后面使用起来也是很方便的,实际上所有的可选字段配置都可以使用 Option 类型包起来,比如: depends_on : Option<Vec<String>>,只不过这样写后默认值就不是空的 Vec 了,而是 None,而值存在时就是 Some(Vec[]) 了。

现在还剩一个问题,就是 networks 如果没有配置,但生成的 yml 也会默认带上 networks: {},这个可以使用 HashMap::is_empty 来判断,

#[derive(Debug, Serialize, Deserialize)]
struct DockerYaml {
    version: String,
    services: HashMap<String, ServiceCfg>,
    #[serde(skip_serializing_if = "HashMap::is_empty", default)]
    networks: HashMap<String, NetworkCfg>,
}

那如果是自己定义的类型呢,难道要给自定义类型增加一个判断是否为空的方法吗?可以,但没必要,除非其他逻辑有用到,可以定义一个函数来判断类型是否为空,比如:

#[derive(Debug, Serialize, Deserialize)]
struct DockerYaml {
    version: String,
    services: HashMap<String, ServiceCfg>,
    #[serde(skip_serializing_if = "is_default", default)]
    networks: HashMap<String, NetworkCfg>,
}
// 调用这个函数来确定是否跳过序列化该字段
fn is_default<T: Default + PartialEq>(v: &T) -> bool {
    v == &T::default()
}
// 因为泛型 T 在函数中用到比较运算,所以要给泛型添加 PartialEq 约束,
// 用于判断是否是默认值,所以 T 要有 default 方法,故也要加上 Default 约束
// 其中 T 表示 HashMap,如果 HashMap 的泛型约束有 PartialEq, 那就表示 HashMap 里面
// 的类型也同样具有相应的约束,所以 NetworkCfg 也要实现 PartialEq trait,
// 而 Default trait 是默认实现的,因为 NetworkCfg  里所有的成员都实现了 Default trait
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // 这行可以省略 Default
struct NetworkCfg {
    #[serde(default)]
    driver: Option<String>,
    #[serde(default)]
    attachable: Option<bool>,
}

五、总结

至此,解析 yml 文件算是大功告成了,当然这里只是介绍一小部分 serde 字段属性,但也可以应付大部分场景了,其他的比如字段别名,序列化时处理逻辑,反序列化时处理逻辑,就留在以后去探索吧。