如何在 Rust 项目中读取 JSON、YAML 和 TOML 文件

22-11-04 banq

在本教程中,您将学习如何从外部源读取 JSON、YAML 和 TOML 文件,以便在您的 Rust 项目中使用。使用 Rust 编程语言读取 JSON 文件、YAML 文件和 TOML 文件。
处理文件可能是软件工程中一个挑剔但不可避免的部分,作为开发人员,您经常需要从外部来源加载信息以在您的项目中使用。

访问文件
首先,我们首先需要创建一个示例文件,我们将通过我们的项目访问该文件。您可以手动创建文件,也可以使用 Rust 标准库提供的 write() 函数。

让我们在终端上使用以下命令启动一个 Rust 启动项目:

//sh
cargo new sample_project

接下来,在我们项目的根目录中创建一个新文件,我们将在其中拥有我们的源代码。

该文件将被调用info.txt,并将仅包含一小段随机文本,如下所示:

// info.txt
Check out more Rust articles on LogRocket Blog


将文件作为字符串读取
首先,我们需要使用use语句导入文件模块。Rust 提供了一个标准库stdcrate,它为fs模块提供文件读取和写入操作。

// rust
use std::fs;
fn main() {
    let file_contents = fs::read_to_string("info.txt")
        .expect("LogRocket: Should have been able to read the file");
    println!("info.txt context =\n{file_contents}");
}

使用上面的代码片段,我们打开并读取位于模块中read_to_string函数中作为参数传递的路径值的文件。fs此外,我们必须指定如果由于某种原因无法打开文件会发生什么;例如,存在权限错误或类似情况。在expect函数中,如果文件无法打开,我们传递要显示的文本。

使用cargo run命令,上面的程序会被编译运行,然后输出我们之前创建的文件的内容。对于此示例,它将具有与 的内容相同的值info.txt。

Serde 框架
Serde是一个用于高效且通用地序列化和反序列化 Rust 数据结构的框架。对于本文的这一部分,我们将使用serdecrate 来解析 JSON 文件和 TOML 文件。

该serde库的基本优势在于它允许您直接将连线数据解析为structs与我们源代码中定义的类型匹配的 Rust。这样,您的项目在源代码编译时就知道每个传入数据的预期类型。

读取 JSON 文件
JSON 格式是一种流行的数据格式,用于存储复杂数据。它是用于在网络上交换有线数据的常用数据格式中的主要数据格式。它在 JavaScript 项目中被广泛使用。

我们可以通过静态类型方法和动态类型方法在 Rust 中解析 JSON 数据。

动态类型的方法最适合您不确定 JSON 数据的格式与源代码中预定义的数据结构的情况,而静态类型的方法在您确定格式时使用JSON 数据。

要开始,您必须安装所有必需的依赖项。

在Cargo.toml文件中,我们将首先添加serde和serde_jsoncrates 作为依赖项。除此之外,确保启用了可选的派生功能,这将帮助我们生成(反)序列化的代码。

//Cargo.toml
<p class="indent">[dependencies]
serde = { version = 1.0, features = [“derived”] }
serde_json = "1.0"


动态解析 JSON
首先,我们编写一个use声明来导入serde_jsoncrate。Value枚举是 crate 的一部分,serde_json它代表任何有效的 JSON 值——它可以是字符串、null、布尔值、数组等。

在根目录中,我们将创建一个 .json 文件来存储任意 JSON 数据,我们将读取数据并将其解析为源代码中定义的有效数据结构。创建一个数据文件夹,然后创建一个 sales.json 文件并使用此JSON 数据对其进行更新。

现在,我们有了有线数据,我们可以使用serde_jsoncrate 更新我们的 main.rs 文件来编写解析 JSON 数据的代码:

//rust
use serde_json::Value;
use std::fs;
fn main() {
    let sales_and_products = {
        let file_content = fs::read_to_string("./data/sales.json").expect("LogRocket: error reading file");
        serde_json::from_str::<Value>(&file_content).expect("LogRocket: error serializing to JSON")
    };
    println!("{:?}", serde_json::to_string_pretty(&sales_and_products).expect("LogRocket: error parsing to JSON"));
}

在上面的代码片段中,我们硬编码了sales.json文件的路径。然后,使用serde_json,我们为 JSON 数据格式提供(反)序列化支持。

根据JSON 格式的规则,from_str它将连续的字节切片作为参数,并从中反序列化类型的实例。Value您可以检查Value类型以了解有关其(反)序列化的更多信息。

这是运行代码片段的输出:

// sh
"{\n  \"products\": [\n    {\n      \"category\": \"fruit\",\n      \"id\": 591,\n      \"name\": \"orange\"\n    },\n    {\n      \"category\": \"furniture\",\n      \"id\": 190,\n      \"name\": \"chair\"\n    }\n  ],\n  \"sales\": [\n    {\n      \"date\": 1234527890,\n      \"id\": \񓠄-7110\",\n      \"product_id\": 190,\n      \"quantity\": 2.0,\n      \"unit\": \"u.\"\n    },\n    {\n      \"date\": 1234567590,\n      \"id\": \񓠄-2871\",\n      \"product_id\": 591,\n      \"quantity\": 2.14,\n      \"unit\": \"Kg\"\n    },\n    {\n      \"date\": 1234563890,\n      \"id\": \񓠄-2583\",\n      \"product_id\": 190,\n      \"quantity\": 4.0,\n      \"unit\": \"u.\"\n    }\n  ]\n}"

在实际项目中,除了显示输出之外,我们还希望访问 JSON 数据中的不同字段、操作数据,甚至尝试将更新的数据存储在另一个文件或同一个文件中。

考虑到这一点,让我们尝试访问sales_and_products变量上的字段并更新其数据并可能将其存储在另一个文件中:

//rust
use serde_json::{Number, Value};
// --snip--

fn main() {
    // --snip--
    if let Value::Number(quantity) = &sales_and_products\["sales"\][1]["quantity"] {
        sales_and_products\["sales"\][1]["quantity"] =
            Value::Number(Number::from_f64(quantity.as_f64().unwrap() + 3.5).unwrap());
    }
    fs::write(
        "./data/sales.json",
        serde_json::to_string_pretty(&sales_and_products).expect("LogRocket: error parsing to JSON"),
    )
    .expect("LogRocket: error writing to file");
}

在上面的代码片段中,我们利用Value::Number变量对sales_and_products\["sales"][1]["quantity"]进行模式匹配,我们期望它是一个数字值。

使用Number结构上的from_f64函数,我们将操作返回的有限f64值,即 quantity.as_f64().unwrap() + 3.5,转换为Number类型,然后我们将其存储回sales_and_products\["sales"\][1]["quantity"]中,更新其值。

(注意:确保使sales_and_products成为一个可变的变量)

然后,使用write函数和文件路径作为参数,我们用调用serde_json::to_string_pretty函数的结果值来创建和更新一个文件。这个结果值将与我们之前在终端上输出的值相同,但有良好的格式化。

静态解析 JSON
另一方面,如果我们完全确定 JSON 文件的结构,我们可以使用一种不同的方法,该方法涉及在我们的项目中使用预定义数据。

这是反对动态解析数据的首选方法。静态版本的源代码在开头声明了三个结构:

// rust
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
struct SalesAndProducts {
    products: Vec<Product>,
    sales: Vec<Sale>
}
#[derive(Deserialize, Serialize, Debug)]
struct Product {
    id: u32,
    category: String,
    name: String
}
#[derive(Deserialize, Serialize, Debug)]
struct Sale {
    id: String,
    product_id: u32,
    date: u64,
    quantity: f32,
    unit: String
}
fn main() {}

第一个结构将JSON对象中的销售和产品字段所包含的内部数据格式分组。其余两个结构定义了存储在JSON对象外部字段中的预期数据格式。

要将JSON字符串解析(读取)到上述结构中,需要使用Deserialize特性。而要将上述结构格式化(即写入)为有效的JSON数据格式,必须有Serialize特性。在终端上简单地打印这个结构(调试跟踪)是Debug trait的用武之地。

我们的主函数的主体应该类似于下面的代码片断。

// rust
use std::fs;
use std::io;
use serde::{Deserialize, Serialize};

// --snip--

fn main() -> Result<(), io::Error> {
    let mut sales_and_products: SalesAndProducts = {
        let data = fs::read_to_string("./data/sales.json").expect("LogRocket: error reading file");
        serde_json::from_str(&data).unwrap()
    };
    sales_and_products.sales[1].quantity += 1.5;
    fs::write("./data/sales.json", serde_json::to_string_pretty(&sales_and_products).unwrap())?;

    Ok(())
}

该函数serde_json::from_str::SalesAndProducts用于解析 JSON 字符串。增加橙子销售数量的代码变得非常简单:

sales_and_products.sales[1].amount += 1.5

与我们的动态方法相比,源文件的其余部分保持不变。

静态解析 TOML
在本节中,我们将专注于读取和解析 TOML 文件。大多数配置文件都可以存储为 TOML 文件格式,并且由于其语法语义,可以轻松地将其转换为字典或 HashMap 等数据结构。由于其语义力求简洁,因此阅读和编写都非常简单。

我们将静态读取和解析这个TOML 文件。这意味着我们知道 TOML 文件的结构,我们将在本节中使用预定义的数据。

我们的源代码将包含以下structs映射;解析时更正TOML文件的内容:

// rust
#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::fs;

#[derive(Deserialize, Debug, Serialize)]
struct Input {
    xml_file: String,
    json_file: String,
}

#[derive(Deserialize, Debug, Serialize)]
struct Redis {
    host: String,
}

#[derive(Deserialize, Debug, Serialize)]
struct Sqlite {
    db_file: String
}

#[derive(Deserialize, Debug, Serialize)]
struct Postgresql {
    username: String,
    password: String,
    host: String,
    port: String,
    database: String
}

#[derive(Deserialize, Debug, Serialize)]
struct Config {
    input: Input,
    redis: Redis,
    sqlite: Sqlite,
    postgresql: Postgresql
}

fn main() {}

仔细看上面的代码片段,你会发现我们定义了每个结构来映射到 TOML 文件中的每个表/标题,并且结构中的每个字段都映射到表/标题下的键/值对。

接下来,使用serde、serde_json和tomlcrates,我们将编写读取和解析主函数主体中的 TOML 文件的代码。

// rust
// --snip--
fn main() {
    let config: Config = {
        let config_text = fs::read_to_string("./data/config.toml").expect("LogRocket: error reading file");
        toml::from_str(&config_text).expect("LogRocket: error reading stream")
    };
    println!("[postgresql].database: {}", config.postgresql.database); 
}
输出:

// sh
<p class="indent">[postgresql].database: Rust2018

上述代码片段的不同之处在于toml::from_str函数,它试图解析String我们使用函数读取的值fs::read_to_string。该toml::from_str函数以Config结构为指导,知道该值的String期望值。

作为奖励,我们可以config使用以下代码行轻松地将上述变量解析为 JSON 值:

// rust
// --snip--
fn main() {
    // --snip--
    let _serialized = serde_json::to_string(&config).expect("LogRocket: error serializing to json");
    println!("{}", serialized);
}
输出:

// sh
{"input":{"xml_file":"../data/sales.xml","json_file":"../data/sales.json"},"redis":{"host":"localhost"},"sqlite":{"db_file":"../data/sales.db"},"postgresql":{"username":"postgres","password":"post","host":"localhost","port":"5432","database":"Rust2018"}}


静态解析 YAML
项目中使用的另一个流行的配置文件是 YAML 文件格式。在本节中,我们静态地在 Rust 项目中读取和解析 YAML 文件。我们将使用这个YAML 文件作为本节的示例。

我们将使用 config crate 来解析 YAML 文件,作为第一种方法,我们将定义必要的结构来充分解析 YAML 文件的内容。

// rust
#[derive(serde::Deserialize)]
pub struct Settings {
    pub database: DatabaseSettings,
    pub application_port: u16,
}
#[derive(serde::Deserialize)]
pub struct DatabaseSettings {
    pub username: String,
    pub password: String,
    pub port: u16,
    pub host: String,
    pub database_name: String,
}
fn main() {}


接下来,我们将在main函数中读取并解析 YAML 文件。

// rust
// --snip--
fn main() -> Result<(), config::ConfigError> {
    let mut settings = config::Config::default(); // --> 1
      let Settings{database, application_port}: Settings = {
        settings.merge(config::File::with_name("configuration"))?; // --> 2
        settings.try_into()? // --> 3
      };

      println!("{}", database.connection_string());
      println!("{}", application_port);
      Ok(())
}

impl DatabaseSettings {
    pub fn connection_string(&self) -> String { // --> 4 
        format!(
            "postgres://{}:{}@{}:{}/{}",
            self.username, self.password, self.host, self.port, self.database_name
        )
    }
}

上面的代码片段比以前的例子有更多的活动部分,所以让我们解释一下每一部分。
  • 我们使用字段类型的默认值来初始化配置结构。你可以检查Config结构以查看默认字段
  • 使用config::File::with_name函数,我们搜索并找到一个YAML文件,其名称为configuration。根据文档的定义,我们使用Config结构上的merge函数来合并配置属性源。
  • 使用前一行代码的源,我们尝试将YAML文件内容解析为我们定义的Settings结构
  • 这是在DatabaseSettings结构上定义的一个实用函数,用于格式化并返回Postgres连接字符串


成功执行上述示例将输出:

// sh
postgres://postgres:password@127.0.0.1:5432/newsletter
8000


结论
在整篇文章中,我们探讨了如何在 Rust 项目中读取不同的文件。Rust 标准库提供了各种执行文件操作的方法,特别是读/写操作,我希望这篇文章对向您展示如何在 Rust 中读取文件有所帮助。