Rust Regex 入门指南:驯服文本的利器
在软件开发中,处理文本是一项常见的任务。无论是解析用户输入、验证数据格式、从日志文件中提取信息,还是对字符串进行复杂的搜索和替换,我们经常需要一种强大而灵活的工具来描述和匹配文本模式。正则表达式(Regular Expressions,简称 Regex 或 Regexp)正是这样一种工具。
Rust 语言通过其官方生态中的 regex
crate 提供了对正则表达式的优秀支持。这个 crate 以其高性能、内存安全和符合标准的语法而闻名,是处理 Rust 中文本模式匹配的首选库。
本篇文章将带你深入了解如何在 Rust 中使用 regex
crate,从最基本的匹配开始,逐步探索更高级的功能,如捕获组、迭代查找和文本替换。无论你之前是否接触过正则表达式,本指南都将为你提供一个坚实的基础。
什么是正则表达式?为什么在 Rust 中使用它?
简单来说,正则表达式是一种用来描述字符串模式的强大语法。它使用一系列特殊字符和构造来定义你想要查找或匹配的文本序列。例如,\d+
可以匹配一个或多个数字,^[a-zA-Z]+$
可以匹配只包含字母的整行字符串。
正则表达式的应用场景非常广泛:
- 数据验证: 检查邮箱地址、电话号码、URL、日期等格式是否正确。
- 文本解析: 从非结构化或半结构化文本(如日志、配置文件)中提取特定信息。
- 搜索和过滤: 在大量文本中查找符合特定模式的行或段落。
- 文本替换: 根据匹配的模式替换文本中的一部分。
- 代码高亮和静态分析: 许多编辑器和工具使用正则表达式来识别语言结构。
在 Rust 中使用 regex
crate 的优势:
- 高性能:
regex
crate 内部使用了先进的算法,对于大型文本和复杂的模式,其性能通常非常出色。它避免了回溯(backtracking)的潜在性能陷阱,许多其他语言的 regex 引擎会受到回溯的影响。 - 安全: 作为 Rust crate,它继承了 Rust 语言的内存安全特性,避免了许多 C/C++ 等语言中常见的与字符串和内存操作相关的错误。
- 符合标准:
regex
crate 支持 Perl 兼容正则表达式(PCRE)的大部分常用语法,这意味着如果你熟悉其他语言的正则表达式,上手会非常快。 - 良好的错误处理: Rust 的
Result
类型强制你处理正则表达式编译过程中可能出现的错误,避免在运行时才发现无效模式。 - Unicode 支持: 对 Unicode 有良好的支持,可以正确处理各种语言的字符。
准备工作:添加 regex
crate
要在你的 Rust 项目中使用 regex
crate,你需要将其添加到项目的 Cargo.toml
文件中。打开你的 Cargo.toml
文件,并在 [dependencies]
部分添加以下行:
toml
[dependencies]
regex = "1" # 使用最新版本,或者指定你需要的版本
保存文件后,Cargo 将在你下次构建或运行项目时自动下载并编译 regex
crate。
“`bash
cargo build
或者
cargo run
“`
基本匹配:检查字符串是否包含模式
最简单的用例是检查一个字符串中是否包含某个正则表达式模式。regex
crate 提供了 is_match
方法来完成这个任务。
首先,你需要导入 Regex
类型:
rust
use regex::Regex;
然后,你可以创建一个 Regex
对象,并调用其 is_match
方法。创建 Regex
对象通常使用 Regex::new()
函数,它接收一个字符串切片作为正则表达式模式。Regex::new()
返回一个 Result<Regex, Error>
,因为正则表达式模式可能无效,导致编译失败。在简单的示例中,我们经常使用 .unwrap()
或 .expect()
来快速获取 Regex
对象,但在生产代码中,你通常需要更健壮的错误处理。
“`rust
use regex::Regex;
fn main() {
// 定义一个简单的模式:查找 “Rust”
let re = Regex::new(r”Rust”).unwrap(); // 使用 r”” 原始字符串避免转义问题
let text1 = "Hello, Rust!";
let text2 = "Hello, world!";
// 检查 text1 中是否包含模式
if re.is_match(text1) {
println!("'Rust' found in '{}'", text1);
} else {
println!("'Rust' not found in '{}'", text1);
}
// 检查 text2 中是否包含模式
if re.is_match(text2) {
println!("'Rust' found in '{}'", text2);
} else {
println!("'Rust' not found in '{}'", text2);
}
}
“`
关于 r""
原始字符串: 注意我们在 Regex::new
中使用了 r"..."
语法。这称为原始字符串(raw string)。在 Rust 中,反斜杠 \
是一个转义字符(例如 \n
表示换行)。正则表达式中大量使用反斜杠来表示特殊字符(例如 \d
表示数字,\s
表示空白字符)。如果没有原始字符串,你需要写成 \\d
或 \\s
来匹配字面上的 \
后面跟 d
或 s
,这会让正则表达式变得非常难以阅读。原始字符串会忽略内部的反斜杠转义,使得正则表达式模式可以直接复制粘贴进来。强烈推荐在定义正则表达式模式时使用原始字符串。
运行上面的代码,你会看到输出:
'Rust' found in 'Hello, Rust!'
'Rust' not found in 'Hello, world!'
这证明了 is_match
成功地判断了字符串中是否存在匹配的模式。
创建 Regex 对象与错误处理
正如前面提到的,Regex::new()
返回 Result<Regex, Error>
。在实际应用中,你不能仅仅依赖 .unwrap()
,因为无效的正则表达式会导致程序崩溃。你需要优雅地处理这种可能的错误。
以下是几种处理 Regex::new
返回的 Result
的方法:
-
使用
match
: 这是最基本的处理Result
的方式。“`rust
use regex::Regex;
use regex::Error; // 导入 Error 类型fn create_regex(pattern: &str) -> Result
{
// Regex::new 返回 Result
Regex::new(pattern)
}fn main() {
let pattern = r”^\d+$”; // 只包含数字的整行match create_regex(pattern) { Ok(re) => { println!("Regex compiled successfully!"); let text = "12345"; if re.is_match(text) { println!("'{}' matches pattern '{}'", text, pattern); } else { println!("'{}' does not match pattern '{}'", text, pattern); } }, Err(err) => { eprintln!("Failed to compile regex '{}': {}", pattern, err); } } let invalid_pattern = r"[invalid pattern"; // 这是一个无效的模式 match create_regex(invalid_pattern) { Ok(re) => { println!("Regex compiled successfully! (unexpected for invalid pattern)"); // ... 使用 re }, Err(err) => { eprintln!("Failed to compile regex '{}': {}", invalid_pattern, err); } }
}
``
match
这个例子展示了如何使用来区分成功 (
Ok) 和失败 (
Err`) 的情况。 -
使用
?
运算符: 如果你在一个返回Result
的函数内部,可以使用?
运算符来传播错误。这是 Rust 中处理Result
的惯用方式。“`rust
use regex::Regex;
use regex::Error;// 注意函数的返回类型是 Result,允许使用 ?
fn check_number_string(text: &str) -> Result{
let re = Regex::new(r”^\d+$”)?; // ? 会在这里处理 Result。如果 Err,则直接从函数返回 Err
Ok(re.is_match(text)) // 如果 Ok,则继续执行并返回 Ok
}fn main() {
let text1 = “12345”;
let text2 = “abc”;match check_number_string(text1) { Ok(is_match) => { println!("'{}' is a number string: {}", text1, is_match); }, Err(err) => { eprintln!("Error checking string: {}", err); } } match check_number_string(text2) { Ok(is_match) => { println!("'{}' is a number string: {}", text2, is_match); }, Err(err) => { eprintln!("Error checking string: {}", err); } } // 尝试使用一个会导致 regex 编译失败的模式 (不会在这里直接发生,因为模式是硬编码的,但概念一样) // 如果 check_number_string 接收 pattern 作为参数,这里会看到错误 // 例如: check_number_string_with_pattern("123", "[") 就会返回 Err
}
// 示例:接收 pattern 的函数
fn check_string_with_pattern(text: &str, pattern: &str) -> Result{
let re = Regex::new(pattern)?; // Pattern 编译失败会通过 ? 返回 Err
Ok(re.is_match(text))
}
``
?` 运算符极大地简化了错误处理流程。
重要的性能提示: 编译正则表达式 (Regex::new
) 可能是一项相对耗时的操作。如果你的程序需要多次使用同一个正则表达式,你应该只编译它一次,然后在需要的地方重用编译好的 Regex
对象,而不是在每次需要匹配时都重新编译。在 main
函数或者一个只需要编译一次的函数中使用 lazy_static
或 once_cell
(或 Rust 1.70+ 内置的 std::sync::OnceLock
)来初始化全局或静态的 Regex
对象是常见的模式。
“`rust
// 需要在 Cargo.toml 中添加 once_cell 或 lazy_static
// [dependencies]
// once_cell = “1.19”
use regex::Regex;
use once_cell::sync::OnceCell; // 或者 lazy_static::lazy_static
static EMAIL_RE: OnceCell
fn get_email_regex() -> &’static Regex {
EMAIL_RE.get_or_init(|| {
// 这个闭包只会在第一次调用 get_or_init 时执行
// 邮箱模式通常很复杂,这里只是一个简化示例
Regex::new(r”^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$”).unwrap()
})
}
fn main() {
let re = get_email_regex(); // 获取静态编译好的 Regex 对象
let email1 = "[email protected]";
let email2 = "invalid-email";
println!("'{}' is a valid email: {}", email1, re.is_match(email1));
println!("'{}' is a valid email: {}", email2, re.is_match(email2));
// 在程序的其他地方再次调用 get_email_regex() 将直接返回已编译的 Regex 对象,不会重新编译
let another_re_instance = get_email_regex();
assert!(std::ptr::eq(re, another_re_instance)); // 验证它们是同一个对象
}
``
get_email_regex` 函数被多次调用。
这种模式确保了正则表达式只编译一次,即使
查找匹配的文本:find
和 find_iter
is_match
只能告诉你是否存在匹配,但它不告诉你匹配在哪里或者匹配的内容是什么。如果你需要找到匹配的具体位置或文本,可以使用 find
或 find_iter
方法。
find()
:查找 第一个 匹配项。如果找到,返回Some(Match)
;否则返回None
。Match
结构体包含匹配文本的起始和结束字节索引,以及一个获取匹配文本切片的方法as_str()
。find_iter()
:查找 所有 不重叠的匹配项,并返回一个迭代器。
“`rust
use regex::Regex;
fn main() {
let re = Regex::new(r”\d+”).unwrap(); // 匹配一个或多个数字
let text = "User ID: 12345, Order ID: 67890";
// 使用 find() 查找第一个匹配
match re.find(text) {
Some(mat) => {
println!("Found first match: '{}'", mat.as_str());
println!("Start index: {}, End index: {}", mat.start(), mat.end());
},
None => {
println!("No numbers found in the text.");
}
}
println!("---");
// 使用 find_iter() 查找所有匹配
println!("Finding all matches:");
for mat in re.find_iter(text) {
println!(" - '{}' (Indices: {}-{})", mat.as_str(), mat.start(), mat.end());
}
let no_match_text = "No numbers here.";
println!("---");
println!("Finding all matches in '{}':", no_match_text);
for mat in re.find_iter(no_match_text) {
println!(" - '{}' (Indices: {}-{})", mat.as_str(), mat.start(), mat.end());
}
// 如果没有匹配,循环将不会执行
}
“`
运行结果:
“`
Found first match: ‘12345’
Start index: 10, End index: 15
Finding all matches:
– ‘12345’ (Indices: 10-15)
– ‘67890’ (Indices: 26-31)
Finding all matches in ‘No numbers here.’:
“`
find
适用于你只需要找到第一个匹配的情况,而 find_iter
则非常适合需要处理文本中所有符合模式的部分。
捕获组:提取匹配的部分内容
正则表达式除了匹配整个模式外,还可以定义“捕获组”(capturing groups),用圆括号 ()
包围起来。捕获组可以让你从匹配的文本中提取出特定的部分。
captures()
:查找 第一个 匹配项,并返回一个Captures
对象。如果找到,返回Some(Captures)
;否则返回None
。Captures
对象包含了整个匹配以及所有捕获组的内容。captures_iter()
:查找 所有 不重叠的匹配项,并返回一个迭代器,每次迭代产生一个Captures
对象。
Captures
对象可以像数组一样访问:
* captures[0]
:代表整个匹配的文本。
* captures[n]
:代表第 n
个捕获组(从 1 开始计数)的文本。
* captures.get(n)
:更安全的方式,返回 Option<Match>
,如果捕获组不存在或未匹配到内容,则返回 None
。
“`rust
use regex::Regex;
fn main() {
// 模式:(日期) (时间) – 消息
// 捕获组 1: 日期 (\d{4}-\d{2}-\d{2})
// 捕获组 2: 时间 (\d{2}:\d{2}:\d{2})
// 捕获组 3: 消息 (.+)
let re = Regex::new(r”^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+-\s+(.+)$”).unwrap();
let log_line1 = "2023-10-27 10:30:00 - User logged in.";
let log_line2 = "2023-10-27 10:31:00 - Failed login attempt.";
let log_line3 = "Just a regular message."; // 不匹配模式
// 使用 captures() 查找第一个匹配并提取信息
println!("Processing log line: '{}'", log_line1);
match re.captures(log_line1) {
Some(caps) => {
// caps[0] 是整个匹配
println!("Full match: {}", caps[0].as_str());
// caps[1] 是第一个捕获组 (日期)
println!("Date: {}", caps[1].as_str());
// caps[2] 是第二个捕获组 (时间)
println!("Time: {}", caps[2].as_str());
// caps[3] 是第三个捕获组 (消息)
println!("Message: {}", caps[3].as_str());
// 使用 get() 访问捕获组,更安全
if let Some(msg) = caps.get(3) {
println!("Message (using get()): {}", msg.as_str());
}
},
None => {
println!("Line did not match the log pattern.");
}
}
println!("---");
println!("Processing log line: '{}'", log_line3);
match re.captures(log_line3) {
Some(_) => { // 不会发生
println!("Line matched the log pattern unexpectedly.");
},
None => {
println!("Line did not match the log pattern.");
}
}
}
“`
运行结果:
“`
Processing log line: ‘2023-10-27 10:30:00 – User logged in.’
Full match: 2023-10-27 10:30:00 – User logged in.
Date: 2023-10-27
Time: 10:30:00
Message: User logged in.
Message (using get()): User logged in.
Processing log line: ‘Just a regular message.’
Line did not match the log pattern.
“`
这个例子展示了如何使用 captures
来从符合特定格式的文本中提取出结构化的数据。
迭代捕获组:captures_iter
captures_iter
类似于 find_iter
,但它返回的是 Captures
对象的迭代器,让你能够处理文本中所有匹配模式的段落,并从每个段落中提取捕获组。
“`rust
use regex::Regex;
fn main() {
// 匹配 key=”value” 的模式
// 捕获组 1: key ([a-zA-Z_]+)
// 捕获组 2: value (“[^”]“) 或者更精确的 ([^”]) 捕获引号内的内容
let re = Regex::new(r#”([a-zA-Z_]+)=”([^”]*)””#).unwrap(); // 使用 #”…”# 原始字符串,允许包含引号
let config_string = r#"
username=”admin”
password=”secure_password_123″
host=”localhost”
port=”8080″
active=”true”
“#;
println!("Parsing configuration string:");
for caps in re.captures_iter(config_string) {
// caps[1] 是 key
// caps[2] 是 value
// 注意,get(n) 是更安全的访问方式,我们在这里省略了错误检查以保持简洁
let key = caps.get(1).unwrap().as_str();
let value = caps.get(2).unwrap().as_str();
println!(" - Key: {}, Value: {}", key, value);
}
// 示例:使用命名捕获组
// (注意,Rust 的 regex crate 支持命名捕获组)
let re_named = Regex::new(r#"(?P<key>[a-zA-Z_]+)="(?P<value>[^"]*)""#).unwrap();
println!("\nParsing configuration string with named captures:");
for caps in re_named.captures_iter(config_string) {
// 通过名字访问捕获组
let key = caps.name("key").unwrap().as_str();
let value = caps.name("value").unwrap().as_str();
println!(" - Key: {}, Value: {}", key, value);
}
}
“`
运行结果:
“`
Parsing configuration string:
– Key: username, Value: admin
– Key: password, Value: secure_password_123
– Key: host, Value: localhost
– Key: port, Value: 8080
– Key: active, Value: true
Parsing configuration string with named captures:
– Key: username, Value: admin
– Key: password, Value: secure_password_123
– Key: host, Value: localhost
– Key: port, Value: 8080
– Key: active, Value: true
“`
使用 captures_iter
和命名捕获组 ((?P<name>...)
) 可以让你的代码更具可读性,特别是当捕获组数量较多时,通过名字访问比通过索引访问更清晰。
文本替换:replace
和 replace_all
正则表达式不仅用于查找和提取,还可以用于替换文本。regex
crate 提供了 replace
和 replace_all
方法来实现这一功能。
replace(text, rep)
:查找 第一个 匹配项,并将其替换为rep
指定的字符串。返回一个 新的Cow<'_, str>
(Copy-on-Write)智能指针,它可能是原始字符串的借用,也可能是修改后的字符串的拥有权。replace_all(text, rep)
:查找 所有 不重叠的匹配项,并将它们全部替换为rep
指定的字符串。同样返回Cow<'_, str>
.
rep
参数是一个字符串切片或实现了 Replacer
trait 的类型。在简单的替换中,你可以直接使用字符串字面量。如果你的替换字符串需要引用捕获组的内容,可以使用 $n
(第 n 个捕获组) 或 ${name}
(名为 name
的捕获组) 语法。
“`rust
use regex::Regex;
fn main() {
let re_numbers = Regex::new(r”\d+”).unwrap();
let text_numbers = “Items: 100, Total: 250”;
// 替换第一个匹配项
let replaced_first = re_numbers.replace(text_numbers, "???");
println!("Replace first: {}", replaced_first); // Items: ???, Total: 250
// 替换所有匹配项
let replaced_all = re_numbers.replace_all(text_numbers, "???");
println!("Replace all: {}", replaced_all); // Items: ???, Total: ???
println!("---");
// 使用捕获组进行替换
// 模式:(\w+) (\w+) (将姓和名分开)
// 替换:$2, $1 (将名和姓对调,并加上逗号)
let re_name = Regex::new(r"(\w+)\s+(\w+)").unwrap();
let name = "John Doe";
let formatted_name = re_name.replace(name, "$2, $1");
println!("Formatted name: {}", formatted_name); // Doe, John
// 多个捕获组替换
// 模式:(\d{4})-(\d{2})-(\d{2}) (年月日)
// 替换:$3/\/\ (日月年格式)
let re_date = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
let date_str = "Today's date is 2023-10-27 and tomorrow is 2023-10-28.";
let formatted_dates = re_date.replace_all(date_str, "$3/\/\");
println!("Formatted dates: {}", formatted_dates); // Today's date is 27/10/2023 and tomorrow is 28/10/2023.
println!("---");
// 使用命名捕获组进行替换
let re_named_date = Regex::new(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})").unwrap();
let date_str_named = "Date format: 2024-01-15";
let formatted_named_date = re_named_date.replace(date_str_named, "${day}/${month}/${year}");
println!("Formatted named date: {}", formatted_named_date); // Date format: 15/01/2024
}
“`
运行结果:
“`
Replace first: Items: ???, Total: 250
Replace all: Items: ???, Total: ???
Formatted name: Doe, John
Formatted dates: Today’s date is 27/10/2023 and tomorrow is 28/10/2023.
Formatted named date: Date format: 15/01/2024
“`
replace_all
特别有用,例如,从 HTML 文本中移除所有的标签,或者批量修改文件内容。
更多高级功能(简述)
-
正则表达式标志 (Flags): 你可以在模式字符串的开头使用
(?flags)
来启用特定的标志,或者使用RegexBuilder
。例如,(?i)
使匹配不区分大小写,(?m)
启用多行模式 (^
和$
匹配每行的开头和结尾而不仅仅是整个字符串的开头和结尾)。
“`rust
use regex::RegexBuilder;let re_case_insensitive = Regex::new(r”(?i)rust”).unwrap();
println!(“Case-insensitive match: {}”, re_case_insensitive.is_match(“RuSt”)); // truelet re_multiline = RegexBuilder::new(r”^Start”).multiline(true).build().unwrap();
let text_multi = “Line 1\nStart Line 2\nLine 3″;
// ^ 匹配整个字符串开头
let re_singleline = Regex::new(r”^Start”).unwrap();
println!(“Multiline match: {}”, re_multiline.is_match(text_multi)); // true
println!(“Singleline match: {}”, re_singleline.is_match(text_multi)); // false (因为 Start 不在整个字符串的开头)
* **非捕获组 `(?:...)`:** 如果你只需要将一部分模式组合在一起进行量词或其他操作,但不需要单独捕获这部分内容,可以使用非捕获组。这可以提高一点性能并避免在捕获组列表中出现不必要的项。
rust
let re_non_capturing = Regex::new(r”(?:abc)+\d+”).unwrap(); // 匹配一个或多个 “abc” 后面跟一个或多个数字
let text = “abcabc123”;
println!(“Non-capturing group match: {}”, re_non_capturing.is_match(text)); // true
// 如果使用 captures(), 不会有额外的捕获组
if let Some(caps) = re_non_capturing.captures(text) {
println!(“Full match: {}”, caps[0].as_str());
// println!(“Group 1: {}”, caps[1].as_str()); // 这会 panic,因为没有第一个捕获组
}
``
(?>…)
* **原子组:** 阻止回溯,可以提高性能,但也可能改变匹配行为。这是更高级的优化技巧。
(?=…)
* **肯定/否定前瞻/后顾断言,
(?!…),
(?<=…),
(?<!…):** 这些是零宽断言,它们匹配一个位置而不是字符。例如,
\w+(?=:)` 匹配后面跟着冒号的单词,但不包含冒号本身。
编写好的正则表达式
编写清晰、正确且高效的正则表达式本身就是一门艺术。以下是一些建议:
- 从简单开始: 先写一个简单的模式,然后逐步添加复杂性。
- 使用原始字符串
r""
: 避免反斜杠转义的困扰。 - 使用在线工具: 有许多在线正则表达式测试工具 (如 regex101.com, regexper.com – 用于可视化) 可以帮助你构建和调试正则表达式。
- 理解基本元字符:
.
(任意字符),*
(零次或多次),+
(一次或多次),?
(零次或一次),[]
(字符集),|
(或),()
(分组/捕获),^
(行的开始),$
(行的结束) 等。 - 理解量词的贪婪性:
*
,+
,?
默认是贪婪的,会匹配尽可能多的字符。在量词后面加上?
(如*?
,+?
,??
) 可以使其变为非贪婪的,匹配尽可能少的字符。 - 优先使用特定字符类:
\d
(数字),\s
(空白),\w
(单词字符) 比.
更精确和高效。 - 谨慎使用
.
:.
会匹配几乎所有字符,包括换行符(除非使用单行模式(?s)
),这可能导致意外的匹配结果。 - 不要过度使用正则表达式: 对于简单的字符串查找或前缀/后缀检查,Rust 标准库的方法(如
contains
,starts_with
,ends_with
,find
,replace
)通常更高效和可读。正则表达式适用于模式复杂、需要灵活匹配或提取特定部分的情况。 - 为复杂的模式添加注释: 在模式内部使用
(?#comment)
或使用(?x)
标志启用自由间隔模式(允许在模式中使用空白和#
注释)。
总结与下一步
恭喜你!你已经学习了在 Rust 中使用 regex
crate 的基础知识,包括:
- 添加
regex
依赖。 - 创建和编译
Regex
对象,并处理可能的错误。 - 使用
is_match
检查是否存在匹配。 - 使用
find
和find_iter
查找匹配的位置和文本。 - 使用
captures
和captures_iter
以及捕获组 (()
和(?P<name>...)
) 提取文本的特定部分。 - 使用
replace
和replace_all
进行文本替换。 - 了解了一些高级功能和编写正则表达式的技巧。
正则表达式是一个功能强大的工具,掌握它需要时间和实践。本指南为你打开了 Rust 中使用正则表达式的大门。接下来的学习方向可以是:
- 深入学习正则表达式的各种语法和元字符。
- 查阅
regex
crate 的官方文档,了解更多方法和细节。 - 尝试解决一些实际的文本处理问题,将你学到的知识付诸实践。
- 探索更高级的正则表达式概念,如回溯控制、条件匹配等(尽管
regex
crate 的设计避免了许多回溯问题,但理解这些概念有助于更好地编写模式)。
希望这篇指南能帮助你有效地在 Rust 项目中利用正则表达式的强大能力!