在本教程中,您将学习如何从外部源读取 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 [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 [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 中读取文件有所帮助。