2023 年 07 月 11 日
在之前的程序中,文本分割符皆以硬编码的方式出现,导致程序灵活性较差。rzeo 项目解析的文本,我倾向为下例所示形式:
以下 Rust 程序
@ hello world #
fn main() {
println!("Hello world!");
}
@
可在终端打印「Hello world!」。
但是对于其他 rzeo 的用户而言,未必喜欢使用 @
和
#
之类的符号。为了最大程度兼容所有人的偏好,rzeo
使用的文本分割符需以配置文件的方式进行定义,在其运行时方知文本分割符的具体形式。
rzeo 的配置文件采用 YAML 语言撰写,例如:
border: '\n[ \t]*@[ \t\n]*'
code_snippet_neck: '[ \t]*#[ \t]*\n'
假设 rzeo 配置文件为 rzeo.conf,编写一个程序从该文件获取分割符。
首先,考虑如何使用 Rust 标准库提供的文件读写功能,读取配置文件 rzeo.conf,并输出其内容,以熟悉文件读写功能的基本用法。
以下代码可读取 rzeo.conf 文件并逐行打印其内容:
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() -> Result<(), std::io::Error> {
let f = File::open("rzeo.conf")?;
let reader = BufReader::new(f);
for line in reader.lines() {
println!("{}", line?);
}
return Ok(());
}
BufRead
是特性。BufReader
是实现了
BufRead
特性的结构体类型,用于将硬盘中的文件内容读入到内存缓冲区以降低硬盘读取次数。需要注意的是,File
的 open
方法和 BufReader
的 lines
方法皆返回 Result<T, std::io::Error>
类型,T
为 String
类型——Rust
的又一种字符串类型,相当于 Vec<char>
类型。在此不对
String
给予讲解,可在使用中逐渐熟悉其用法。
以下代码可将 rzeo.conf 文件中的内容写入另一个文件:
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
fn main() -> Result<(), std::io::Error> {
let f = File::open("rzeo.conf")?;
let mut g = File::create("foo.txt")?;
let reader = BufReader::new(f);
for line in reader.lines() {
let content = line?;
.write_all(content.as_bytes())?;
g.write_all("\n".as_bytes())?;
g}
return Ok(());
}
注意,File
的 write_all
方法,其参数类型为
&[u8]
,即字节数组切片,故而需要使用
&str
或 String
类型的
as_bytes
将字符串转化为字节数组切片。write_all
的返回值是 Result<()>
类型,需使用
unwrap
解包或使用 ?
进行错误传播。
为了保持不同操作系统中文件路径的兼容性,Rust
标准库提供了一种特殊的字符串类型 std::path::Path
以及一些用于处理文件路径的方法。为了程序的可移植性,建议使用
std::path::Path
代替普通的字符串作为文件路径。
以下代码演示了 std::path::Path
的基本用法:
use std::path::Path;
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() -> Result<(), std::io::Error> {
let path = Path::new("/tmp/rzeo.conf");
println!("{:?}", path);
let f = File::open(path)?;
let reader = BufReader::new(f);
for line in reader.lines() {
println!("{}", line?);
}
return Ok(());
}
读取 rzeo.conf 文件并不困难,困难的是对其内容的解析。当前的 rzeo.conf 文件中的内容,仅仅用到了 YAML 最为基础的语法——键值对,即便如此,要对其予以解析,免不了要写许多代码。Rust 第三方库 serde 能够实现 Rust 语言的值与特定格式的数据文件的交换,即值的序列化(Serialize)和反序列化(Deserialize)。
serde 只是一个框架,对于特定格式的数据文件,需要引入 serde 的相应实现。下面使用 cargo 构建一个项目,引入 serde 和 serde_yaml 库,实现 Rust 结构体的序列化。
首先,使用 cargo 建立新项目并进入项目目录:
$ cargo new foo
$ cd foo
然后使用 cargo add
命令添加 serde(同时开启 serde 的
derive 特性)和 serde_yaml 库:
$ cargo add -F derive serde
$ cargo add serde_yaml
上述命令可在项目根目录下的 Cargo.toml 文件的
[dependencies]
部分添加以下内容:
serde = { version = "1.0.171", features = ["derive"] }
serde_yaml = "0.9.22"
随着 serde 和 serde_yaml 库的更新,等你看到这份文档时,也动手搭建这个项目时,库的版本号应该是与上述内容不同。
编辑上一节构建的 foo 项目的 src/main.rs 文件,令其内容为
use std::fs::File;
#[derive(serde::Serialize)]
struct Foo<'a> {
: u32,
id: &'a str
data}
fn main() -> Result<(), std::io::Error> {
let foo = Foo { id: 1, data: "Hello world!" };
let f = File::create("foo.yml")?;
serde_yaml::to_writer(f, &foo)?;
return Ok(());
}
执行以下命令,编译并运行程序:
$ cargo run
但是上述的 main.rs 中存在错误,导致 Rust 编译器报错。错误的原因是
File::create
和 sert_yaml::to_writer
返回的
Result<T, E>
类型不一致,导致无法给出
main
函数的返回值类型的正确定义。对于上述代码快速而脏的修复是
serde_yaml::to_writer(f, &foo).unwrap();
即放弃 serde_yaml::to_write
的错误进行传播。
上述程序通过编译,运行结果是在当前目录创建 foo.yml 文件,其内容为
id: 1
data: Hello world!
以下代码实现了 YAML 文件 foo.yml 的反序列化:
use std::fs::File;
#[derive(Debug, serde::Deserialize)]
struct Foo {
: u32,
id: String
data}
fn main() -> Result<(), std::io::Error> {
let f = File::open("foo.yml")?;
let foo: Foo = serde_yaml::from_reader(f).unwrap();
println!("{:?}", foo);
return Ok(());
}
foo.yml 中的内容被转换为 Rust 结构体类型 Foo
的实例
foo
。需要注意的是,上述 Foo
的
data
域,其类型不再是 &str
,而是
String
,原因 serde_yaml::from_reader
方法并不占有数据,导致存储反序列化结果的结构体实例中的引用无效。
现在,定义一个结构体类型 Separator,用于存储从 rzeo.conf 中获取的分割符:
#[derive(Debug, serde::Deserialize)]
struct Separator {
: String,
border: String
code_snippet_neck}
使用以下代码便可解析 rzeo.conf 相应的信息并将解析结果作为
Separator
类型的值:
fn main() -> Result<(), std::io::Error> {
let f = std::fs::File::open("rzeo.conf")?;
let foo: Separator = serde_yaml::from_reader(f).unwrap();
println!("{:?}", foo);
return Ok(());
}
至此,本章开头提出的问题便得以解决。
有一些采用 C 语言编写的库也能够实现对 YAML 文件的解析,例如能够支持 C 语言结构体的 YAML 序列化和反序列化的库 libcyaml,在 Ubuntu 系统可使用以下命令安装该库:
$ sudo apt install libyaml-dev libcyaml-dev
以下是反序列化 rzeo.conf 文件的 C 程序:
#include <stdlib.h>
#include <stdio.h>
#include <cyaml/cyaml.h>
typedef struct {
char *border;
char *code_snippet_neck;
} Foo;
/* 构造结构体类型 Foo 与 rzeo.conf 之间的联系 */
static const cyaml_schema_field_t top_mapping[] = {
(
CYAML_FIELD_STRING_PTR"border", CYAML_FLAG_POINTER, Foo, border, 0, CYAML_UNLIMITED),
(
CYAML_FIELD_STRING_PTR"code_snippet_neck", CYAML_FLAG_POINTER, Foo,
, 0, CYAML_UNLIMITED),
code_snippet_neck
CYAML_FIELD_END};
static const cyaml_schema_value_t top = {
(CYAML_FLAG_POINTER, Foo, top_mapping)
CYAML_VALUE_MAPPING};
/* 解析器设置 */
static const cyaml_config_t config = {
.log_fn = cyaml_log,
.mem_fn = cyaml_mem,
.log_level = CYAML_LOG_WARNING
};
/* 反序列化 */
int main(void) {
*foo;
Foo = cyaml_load_file("rzeo.conf", &config, &top,
cyaml_err_t err (cyaml_data_t **)&foo, NULL);
if (err != CYAML_OK) {
(stderr, "ERROR: %s\n", cyaml_strerror(err));
fprintfreturn EXIT_FAILURE;
}
("border: %s\n", foo->border);
printf("code_snippet_neck: %s\n", foo->code_snippet_neck);
printf(&config, &top, foo, 0);
cyaml_freereturn 0;
}
使用以下命令编译上述程序:
$ gcc -o foo foo.c $(pkg-config --cflags --libs libcyaml)
运行程序:
$ ./foo
border: \n[ \t]*@[ \t\n]*
code_snippet_neck: [ \t]*#[ \t]*\n
Rust 第三方库对 YAML 序列化和反序列化的支持优于 C 的第三方库。必须要承认,C 语言在文本处理方面,若想变得更为优雅,最好的办法是先基于它实现一门小巧的动态语言(例如 Lua 语言),由后者负责处理文本。