处理文件是软件工程中一个棘手但不可避免的部分,作为开发人员,经常需要从外部源加载信息以在项目中使用。在这篇文章中,我们将学习如何用Rust编程语言读取JSON文件、YAML文件和TOML文件。
使用以下命令创建一个Rust新项目:
cargo new sample_project
使用Serde框架解析JSON
Serde是一个框架,用于高效、通用地序列化和反序列化Rust数据结构。在本文的这一部分中,我们将使用serde crate来解析JSON文件。Serde库的基本优点是,它允许你直接将数据解析为与源代码中类型匹配的Rust结构体。这样,你的项目在编译源代码时就知道传入数据的预期类型。
解析JSON文件
JSON格式是一种流行的用于存储复杂数据的文件格式,它是用于在web上交换数据的常用格式,并且在JavaScript项目中广泛使用。我们可以通过静态类型方法和动态类型方法在Rust中解析JSON数据。动态类型方法最适合于不确定JSON数据格式是否符合源代码中预定义的数据结构体的情况,而静态类型方法则适用于确定JSON数据格式的情况。
要开始使用,必须安装所有必需的依赖项。首先,我们在Cargo.toml文件中添加serde和serde_json crate作为依赖项。除此之外,确保启用了可选的派生功能,这将帮助我们生成(反)序列化的代码:
[dependencies]
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0"
动态解析JSON
首先,我们编写一个use声明来导入serde_json crate。Value enum是serde_json crate的一部分,它代表任何有效的JSON值——可以是字符串、空值、布尔值、数组等。
在根目录中,我们将创建一个.json文件来存储任意JSON数据,我们将读取数据并将其解析为源代码中定义的结构体。创建一个data文件夹,然后创建一个sales.json文件并使用此json数据更新它:
{
"products": [
{
"id": 591,
"category": "fruit",
"name": "orange"
},
{
"id": 190,
"category": "furniture",
"name": "chair"
}
],
"sales": [
{
"id": "2020-7110",
"product_id": 190,
"date": 1234527890,
"quantity": 2.0,
"unit": "u."
},
{
"id": "2020-2871",
"product_id": 591,
"date": 1234567590,
"quantity": 2.14,
"unit": "Kg"
},
{
"id": "2020-2583",
"product_id": 190,
"date": 1234563890,
"quantity": 4.0,
"unit": "u."
}
]
}
现在我们有了数据,在main.rs文件中写入以下代码:
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")
);
}
from_str将一个连续的字节片段作为参数,并根据JSON格式的规则,从中反序列化一个Value类型的实例。在实际项目中,除了显示输出外,我们还希望访问JSON数据中的不同字段,操作数据,甚至尝试将更新的数据存储在相同或不同的文件中。
考虑到这一点,让我们尝试访问sales_and_products变量上的字段,更新其数据,并可能将其存储在另一个文件中:
use serde_json::{Number, Value};
use std::fs;
fn main() {
let mut 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")
};
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函数,将操作quantity.as_f64().unwrap() + 3.5返回的f64值转换回Number类型,然后将其存储回sales_and_products中["sales"][1]["quantity"]并更新了它的值(注意:确保sales_and_products是一个可变变量)。
然后,使用write函数和文件路径作为参数,使用serde_json::to_string_pretty函数的结果值创建并更新一个文件。这个结果值将与我们之前在终端上输出的相同,但格式良好。
静态解析JSON
另一方面,如果我们确定JSON文件的结构,我们可以利用不同的方法,包括在项目中使用预定义的数据。
静态版本的源代码在开头声明了三个结构体:
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
}
main.rs的代码如下:
use std::{fs, io};
use serde::{Deserialize, Serialize};
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(())
}
与我们的动态方法相比,源文件的其余部分保持不变。
静态解析TOML
在本节中,我们将重点讨论读取和解析TOML文件。大多数配置文件都可以以TOML文件格式存储,并且由于其语法语义,可以很容易地将其转换为Map或HashMap之类的数据结构。因为它的语义力求简洁,所以读起来和写起来都很简单。
创建一个在data/config.toml文件,内容如下:
[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"
我们将静态地读取和解析这个TOML文件,这意味着我们知道TOML文件的结构,并且我们将在本节中使用预定义的数据。
首先,在Cargo.toml文件中加入toml依赖项:
[dependencies]
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0"
toml = "0.8.12"
我们代码将包含以下结构体:
#![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
}
我们定义了每个结构体来映射到TOML文件中的内容,结构体中的每个字段映射到表/头下的键/值对。接下来,我们在main函数体中使用serde, serde_json和toml crate来读取和解析config.toml文件:
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);
}
输出结果:
[postgresql].database: Rust2021
我们还可以使用serde_json轻松地将config变量解析为JSON值:
fn main() {
......
let serialized = serde_json::to_string(&config).expect("LogRocket: error serializing to json");
println!("{}", serialized);
}
静态解析YAML
项目中使用的另一个流行的配置文件格式是YAML文件。在本节中,我们将静态地读取和解析Rust项目中的YAML文件。
创建一个data/configuration.yaml文件,内容如下:
application_port: 8000
database:
host: "127.0.0.1"
port: 5432
username: "postgres"
password: "password"
database_name: "newsletter"
我们将使用config crate来解析YAML文件,我们需要定义必要的结构体来充分解析YAML文件的内容:
#[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,
}
接下来,我们在main函数中读取和解析YAML文件:
fn main() -> Result<(), config::ConfigError> {
let mut settings = config::Config::default();
let Settings {
database,
application_port,
}: Settings = {
settings.merge(config::File::with_name("data/configuration"))?;
settings.try_deserialize()?
};
println!("{}", database.connection_string());
println!("{}", application_port);
Ok(())
}
impl DatabaseSettings {
pub fn connection_string(&self) -> String {
format!(
"postgres://{}:{}@{}:{}/{}",
self.username, self.password, self.host, self.port, self.database_name
)
}
}
结果输出:
postgres://postgres:password@127.0.0.1:5432/newsletter
8000
让我们来分解一下上面的代码:
1.我们使用字段类型的默认值初始化Config结构体。
2.使用config::File::with_name函数,我们搜索并定位具有该名称和配置的YAML文件。根据文档的定义,我们使用Config结构体上的merge函数合并配置属性源。
3.将YAML文件内容解析为我们定义的Settings结构体
4.格式化并返回Postgres连接字符串
总结
我们研究了Serde crate、toml crate和config crate,以及Serde如何在帮助我们将不同的文件格式(如YAML、JSON或TOML)解析为Rust程序可以理解的数据结构体。