Rust 中的文本处理
在 Rust 中创建方便的命令行实用程序。
本文是关于 Rust 中的文本处理,但它也包含模式匹配的快速介绍,这在处理文本时非常方便。
字符串是 Rust 中的一个庞大主题,Rust 拥有两种用于表示字符串的数据类型以及用于格式化字符串的宏支持,这很容易意识到这一点。但是,所有这些也证明了 Rust 在字符串和文本处理方面的强大功能。
除了涵盖一些理论主题外,本文还展示了如何开发一些方便但易于实现的命令行实用程序,这些实用程序可让您处理纯文本文件。如果您有时间,最好尝试一下此处提供的 Rust 代码,并可能开发自己的实用程序。
Rust 和文本Rust 支持两种用于处理字符串的数据类型:String
和 str
。 String
类型用于处理属于您的可变字符串,它具有长度和容量属性。另一方面,str
类型用于处理您想要传递的不可变字符串。您很可能会看到一个 str
变量用作 &str
。简而言之,str
变量作为对某些 UTF-8 数据的引用进行访问。 str
变量通常称为“字符串切片”,甚至更简单地称为“切片”。由于其性质,您无法从现有的 str
变量中添加和删除任何数据。此外,如果您尝试在 &str
变量上调用 capacity()
函数,您将收到类似于以下的错误消息
error[E0599]: no method named `capacity` found for type
↪`&str` in the current scope
一般来说,当您想要将字符串作为函数参数传递或想要拥有字符串的只读版本时,您将想要使用 str
,然后在您想要拥有要拥有的可变字符串时使用 String
变量。
好处是,接受 &str
参数的函数也可以接受 String
参数。(您将在本文后面的 basicOps.rs
程序中看到这样的示例。)此外,Rust 支持 char
类型,它用于表示单个 Unicode 字符,以及字符串字面量,即以双引号开头和结尾的字符串。
最后,Rust 支持所谓的 byte
字符串。您可以按如下方式定义新的 byte
字符串
let a_byte_string = b"Linux Journal";
unwrap()
您几乎肯定无法编写不使用 unwrap()
函数的 Rust 程序,因此让我们在这里看一下它。 Rust 不支持 null
、nil
或 Null
,它使用 Option
类型来表示可能存在或可能不存在的值。如果您确定您想要使用的某些 Option
或 Result
变量具有值,则可以使用 unwrap()
并从该变量中获取该值。
但是,如果该值不存在,您的程序将崩溃。看一下以下 Rust 程序,它保存为 unwrap.rs
use std::net::IpAddr;
fn main() {
let my_ip = "127.0.0.1";
let parsed_ip: IpAddr = my_ip.parse().unwrap();
println!("{}", parsed_ip);
let invalid_ip = "727.0.0.1";
let try_parsed_ip: IpAddr = invalid_ip.parse().unwrap();
println!("{}", try_parsed_ip);
}
这里发生了两件主要的事情。首先,由于 my_ip
是有效的 IPv4 地址,因此 parse().unwrap()
将会成功,并且 parsed_ip
在调用 unwrap()
后将具有有效值。
但是,由于 invalid_ip
不是有效的 IPv4 地址,因此第二次尝试调用 parse().unwrap()
将会失败,程序将崩溃,并且第二个 println!()
宏将不会被执行。执行 unwrap.rs
将验证所有这些
$ ./unwrap
127.0.0.1
thread 'main' panicked at 'called `Result::unwrap()`
↪on an `Err`
value: AddrParseError(())', libcore/result.rs:945:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
这意味着在 Rust 程序中使用 unwrap()
时应格外小心。不幸的是,深入探讨 unwrap()
以及如何避免崩溃情况超出了本文的范围。
println!
和 format!
宏
Rust 支持宏,包括与字符串相关的 println!
和 format!
。
Rust 宏允许您编写编写其他代码的代码,这也称为元编程。尽管宏看起来很像 Rust 函数,但它们与 Rust 函数有一个根本区别:宏可以具有可变数量的参数,而 Rust 函数的签名必须声明其参数并定义每个函数参数的确切类型。
您可能已经知道,println!
宏用于将输出打印到 UNIX 标准输出,而 format!
宏的工作方式与 println!
相同,但它返回一个新的 String
而不是将任何文本写入标准输出。
macros.rs
的 Rust 代码将尝试澄清一些事情
macro_rules! hello_world{
() => {
println!("Hello World!")
};
}
fn double(a: i32) -> i32 {
return a + a
}
fn main() {
// Using the format!() macro
let my_name = "Mihalis";
let salute = format!("Hello {}!", my_name);
println!("{}", salute);
// Using hello_world
hello_world!();
// Using the assert_eq! macro
assert_eq!(double(12), 24);
assert_eq!(double(12), 26);
}
您从 macros.rs
中获得了什么知识?首先,宏定义以 macro_rules!
开头,并且可以在其实现中包含其他宏。请注意,这是一个非常幼稚的宏,实际上并没有做任何有用的事情。其次,您可以看到,当您想使用自己的格式创建自己的字符串时,format!
非常方便。第三,先前创建的 hello_world
宏应调用为 hello_world!()
。最后,这表明 assert_eq!()
宏可以帮助您测试代码的正确性。
编译并运行 macros.rs
会产生以下输出
$ ./macros
Hello Mihalis!
Hello World!
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `24`,
right: `26`', macros.rs:22:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
此外,您可以在此处看到 assert_eq!
宏的优势:当 assert_eq!
宏失败时,它还会打印断言的行号和文件名,这无法使用函数完成。
现在让我们看一下如何在 Rust 中执行基本的文本操作。此示例的 Rust 代码保存在 basicOp.rs
中,如下所示
fn accept(s: &str) {
println!("{}", s);
}
fn main() {
// Define a str
let l_j: &str= "Linux Journal";
// Or
let magazine: &'static str = "magazine";
// Use format! to create a String
let my_str = format!("Hello {} {}!", l_j, magazine);
println!("my_str L:{} C:{}", my_str.len(),
↪my_str.capacity());
// String character by character
for c in my_str.chars() {
print!("{} ", c);
}
println!();
for (i, c) in my_str.chars().enumerate() {
print!("{}:{} ", c, i);
}
println!();
// Convert string to number
let n: &str = "10";
match n.parse::<i32>() {
Ok(n) => println!("{} is a number!", n),
Err(e) => println!("{} is NOT a number!", e),
}
let n1: &str = "10.2";
match n1.parse::<i32>() {
Ok(n1) => println!("{} is a number!", n1),
Err(e) => println!("{}: {}", n1, e),
}
// accept() works with both str and String
let my_str = "This is str!";
let mut my_string = String::from("This is string!");
accept(&my_str);
accept(&my_string);
// my_string has capacity
println!("my_string L:{} C:{}", my_string.len(),
↪my_string.capacity());
my_string.push_str("OK?");
println!("my_string L:{} C:{}", my_string.len(),
↪my_string.capacity());
// Convert String to str
let s_str: &str = &my_string[..];
// Convert str to String
let s_string: String = s_str.to_owned();
println!("s_string: L:{} C:{}", s_string.len(),
↪s_string.capacity());
}
因此,首先您可以看到定义 str
变量和使用 format!
宏创建 String
变量的两种方法。然后,您可以看到两种逐字符迭代字符串的技术。第二种技术还会返回您正在处理的字符串的索引。之后,此示例展示了如何在 parse::<i32>()
的帮助下,在可能的情况下将字符串转换为整数。接下来,您可以看到 accept()
函数接受 &str
和 String
参数,即使它的定义中提到了 &str
参数。接下来,这显示了 String
变量的容量和长度属性,它们是两个不同的事物。 String
的长度是 String
的大小,而 String
的容量是当前为该 String
分配的空间。最后,您可以看到如何将 String
转换为 str
,反之亦然。从 &str
变量获取 String
的其他方法包括使用 .to_string()
、String::from()
、String::push_str()
、format!()
和 .into()
。
执行 basicOp.rs
生成以下输出
$ ./basicOp
my_str L:29 C:32
H e l l o L i n u x J o u r n a l m a g a z i n e !
H:0 e:1 l:2 l:3 o:4 :5 L:6 i:7 n:8 u:9 x:10 :11 J:12 o:13
↪u:14 r:15
n:16 a:17 l:18 :19 m:20 a:21 g:22 a:23 z:24 i:25 n:26 e:27
↪!:28
10 is a number!
10.2: invalid digit found in string
This is str!
This is string!
my_string L:15 C:15
my_string L:18 C:30
s_string: L:18 C:18
查找回文串
现在,让我们看一下一个小型实用程序,该实用程序检查字符串是否为回文。该字符串作为命令行参数提供给程序。 palindrome.rs
的逻辑在 check_palindrome()
函数的实现中找到,该函数的实现如下
pub fn check_palindrome(input: &str) -> bool {
if input.len() == 0 {
return true;
}
let mut last = input.len() - 1;
let mut first = 0;
let my_vec = input.as_bytes().to_owned();
while first < last {
if my_vec[first] != my_vec[last] {
return false;
}
first +=1;
last -=1;
}
return true;
}
这里的关键点是,您使用对 as_bytes().to_owned()
的调用将字符串转换为向量,以便能够将其作为数组访问。之后,您继续从输入字符串的左侧和右侧处理,从每侧处理一个字符,只要两个字符相同或直到您超过字符串的中间位置。在这种情况下,您正在处理回文,因此该函数返回“true”;否则,该函数返回“false”。
使用各种类型的输入执行 palindrome.rs
会生成以下类型的输出
$ ./palindrome 1
1 is a palindrome!
$ ./palindrome
Usage: ./palindrome string
$ ./palindrome abccba
abccba is a palindrome!
$ ./palindrome abcba
abcba is a palindrome!
$ ./palindrome acba
acba is not a palindrome!
模式匹配
模式匹配可能非常方便,但您应谨慎使用它,因为它会在您的软件中造成讨厌的错误。 Rust 中的模式匹配借助 match
关键字进行。匹配语句必须捕获所用变量的所有可能值,因此在块的末尾设置默认分支是一种非常常见的做法。默认分支借助下划线字符定义,下划线字符是“捕获所有”的同义词。在某些罕见的情况下,例如当您检查可以是真或假的条件时,不需要默认分支。模式匹配块可能如下所示
let salute = match a_name
{
"John" => "Hello John!",
"Jim" => "Hello Boss!",
"Jill" => "Hello Jill!",
_ => "Hello stranger!"
};
该块有什么作用?它匹配三个不同的案例之一(如果存在匹配),否则它将转到匹配所有案例,这是最后一个案例。如果您想执行需要使用正则表达式的更复杂的任务,则 regex crate 可能更合适。
Rust 中的wc
版本
现在让我们看一下 wc(1)
命令行实用程序的简化版本的实现。该实用程序的 Rust 版本将另存为 wc.rs,将不支持任何命令行标志,会将每个命令行参数都视为文件,并且可以处理多个文本文件。 wc.rs 的 Rust 版本如下
use std::env;
use std::io::{BufReader, BufRead};
use std::fs::File;
fn main() {
let mut lines = 0;
let mut words = 0;
let mut chars = 0;
let args: Vec<_> = env::args().collect();
if args.len() == 1 {
println!("Usage: {} text_file(s)", args[0]);
return;
}
let n_args = args.len();
for x in 1..n_args {
let mut total_lines = 0;
let mut total_words = 0;
let mut total_chars = 0;
let input_path = ::std::env::args().nth(x).unwrap();
let file = BufReader::new(File::open(&input_path)
↪.unwrap());
for line in file.lines() {
let my_line = line.unwrap();
total_lines = total_lines + 1;
total_words += my_line.split_whitespace().count();
total_chars = total_chars + my_line.len() + 1;
}
println!("\t{}\t{}\t{}\t{}", total_lines, total_words,
↪total_chars, input_path);
lines += total_lines;
words += total_words;
chars += total_chars;
}
if n_args-1 != 1 {
println!("\t{}\t{}\t{}\ttotal", lines, words, chars);
}
}
首先,您应该知道 wc.rs
正在使用缓冲输入来处理其文本文件。除此之外,程序的逻辑位于内部 for
循环中,该循环逐行读取每个输入文件。对于读取的每一行,它都会计算字符和单词。计算一行的字符就像调用 len()
函数一样简单。计算一行的单词需要使用 split_whitespace()
拆分该行,并计算生成的迭代器中的元素数量。
您应该考虑的另一件事是在处理完文件后重置 total_lines
、total_words
和 total_chars
计数器。 lines
、words
和 chars
变量保存从所有已处理的文本文件中读取的行数、单词数和字符总数。
执行 wc.rs
会生成以下类型的输出
$ rustc wc.rs
$ ./wc
Usage: ./wc text_file(s)
$ ./wc wc.rs
40 124 1114 wc.rs
$ ./wc wc.rs palindrome.rs
40 124 1114 wc.rs
39 104 854 palindrome.rs
79 228 1968 total
$ wc wc.rs palindrome.rs
40 124 1114 wc.rs
39 104 854 palindrome.rs
79 228 1968 total
执行的最后一个命令 wc(1)
是为了验证 wc.rs
输出的正确性。
作为练习,您可以尝试创建一个单独的函数来计算文本文件的行数、单词数和字符数。
匹配包含给定字符串的行在本节中,您将看到如何显示文本文件中与给定字符串匹配的行,文件名和字符串都将作为命令行参数提供给实用程序,该实用程序名为 match.rs
。以下是 match.rs 的 Rust 代码
use std::env;
use std::io::{BufReader,BufRead};
use std::fs::File;
fn main() {
let mut total_lines = 0;
let mut matched_lines = 0;
let args: Vec<_> = env::args().collect();
if args.len() != 3 {
println!("{} filename string", args[0]);
return;
}
let input_path = ::std::env::args().nth(1).unwrap();
let string_to_match = ::std::env::args().nth(2).unwrap();
let file = BufReader::new(File::open(&input_path).unwrap());
for line in file.lines() {
total_lines += 1;
let my_line = line.unwrap();
if my_line.contains(&string_to_match) {
println!("{}", my_line);
matched_lines += 1;
}
}
println!("Lines processed: {}", total_lines);
println!("Lines matched: {}", matched_lines);
}
所有繁重的工作都由 contains()
函数完成,该函数检查当前正在处理的行是否包含所需的字符串。除此之外,其余的 Rust 代码非常简单。
构建和执行 match.rs
会生成如下输出
$ ./match tabSpace.rs t2s
fn t2s(input: &str, n: i32) {
t2s(&input_path, n_space);
Lines processed: 56
Lines matched: 2
$ ./match tabSpace.rs doesNotExist
Lines processed: 56
Lines matched: 0
在制表符和空格之间转换
接下来,让我们开发一个命令行实用程序,该实用程序可以在文本文件中将制表符转换为空格,反之亦然。每个制表符都替换为四个空格字符,反之亦然。
此实用程序至少需要两个命令行参数:第一个参数应指示您是要将制表符替换为空格还是反之亦然。之后,您应该提供至少一个文本文件的路径。该实用程序将根据您的需要处理任意数量的文本文件,就像本文前面介绍的 wc.rs
实用程序一样。
您可以在以下两个 Rust 函数中找到 tabSpace.rs
的逻辑
fn t2s(input: &str) {
let file = BufReader::new(File::open(&input).unwrap());
for line in file.lines() {
let my_line = line.unwrap();
let new_line = my_line.replace("\t", " ");
println!("{}", new_line);
}
}
fn s2t(input: &str) {
let file = BufReader::new(File::open(&input).unwrap());
for line in file.lines() {
let my_line = line.unwrap();
let new_line = my_line.replace(" ", "\t");
println!("{}", new_line);
}
}
所有工作都由 replace()
完成,它将第一个模式的每次出现都替换为第二个模式。 replace()
函数的返回值是输入字符串的更改版本,该版本将显示在您的屏幕上。
执行 tabSpace.rs
会创建如下输出
$ ./tabSpace -t basicOp.rs > spaces.rs
Processing basicOp.rs
$ mv spaces.rs basicOp.rs
$ ./tabSpace -s basicOp.rs > tabs.rs
Processing basicOp.rs
$ ./tabSpace -t tabs.rs > spaces.rs
Processing tabs.rs
$ diff spaces.rs basicOp.rs
先前的命令验证了 tabSpace.rs
的正确性。首先,basicOp.rs
中的任何制表符都将转换为空格并另存为 spaces.rs
,然后将其用作新的 basicOps.rs
。然后,basicOps.rs
的空格将转换为制表符并保存在 tabs.rs
中。最后,处理 tabs.rs
文件,并将其所有制表符都转换为空格 (spaces.rs
)。 spaces.rs
的最后一个版本应与 basicOps.rs
完全相同。
在 tabSpace.rs
中添加对可变大小制表符的支持将是一个非常有趣的练习。简而言之,制表符的空格数应该是一个变量,该变量将作为命令行参数提供给该实用程序。
那么,Rust 擅长文本处理和一般文本处理吗?是的,它是!此外,应该清楚的是,文本处理与文件 I/O 以及(有时)与模式匹配和正则表达式密切相关。
了解有关 Rust 中文本处理的更多信息的唯一合理方法是自行尝试,因此不要再浪费时间,立即尝试一下。
资源