Rust 正则表达式教程:基础与使用 – wiki基地


Rust 正则表达式教程:基础与使用

正则表达式(Regular Expression,简称 Regex 或 RegExp)是一种强大的文本模式匹配工具,广泛应用于字符串搜索、替换、验证和解析。在 Rust 编程语言中,处理正则表达式最常用且性能优异的库是官方推荐的 regex crate。本教程将带你从零开始,深入了解如何在 Rust 中使用 regex crate 来处理字符串,包括基础概念、核心功能、常用语法以及性能优化。

第一章:引言 – 为什么选择 Rust 的 regex crate?

什么是正则表达式?

简单来说,正则表达式是一种描述字符模式的字符串。通过定义一个特定的模式,你可以快速地在一大段文本中查找符合该模式的子串,或者检查整个文本是否符合某个模式。例如,你可以使用正则表达式来查找所有的电子邮件地址、验证用户输入的手机号码格式、从日志文件中提取特定信息等等。

Rust 与正则表达式

Rust 是一种以性能、内存安全和并发性著称的系统级编程语言。在处理字符串和文本数据时,正则表达式是一个不可或缺的工具。Rust 的标准库并没有内置正则表达式引擎,而是将其作为一个外部 crate 提供。这样做的好处是将复杂的功能模块化,用户可以根据需要选择合适的库,同时也避免了标准库过于臃肿。

regex crate 的优势

在 Rust 社区中,有多个可用的正则表达式 crate,但 regex 是其中最流行、功能最完善且性能最高的之一。它由 Rust 核心团队成员维护,并具有以下显著优势:

  1. 高性能: regex crate 使用了先进的正则表达式匹配算法(如 Thompson NFA 的 Pike-Thompson 实现),这使得它的匹配速度非常快,并且保证在输入文本长度上具有线性时间复杂度(O(n)),这对于处理大型文本至关重要。许多其他语言中的正则表达式引擎可能会在某些模式和输入下表现出指数级甚至更差的性能。
  2. 安全性: Rust 的内存安全特性自然延伸到了 regex crate。它在设计上避免了常见的正则表达式安全问题,如 ReDoS (Regular Expression Denial of Service)。
  3. UTF-8 支持: regex crate 完全支持 Unicode 和 UTF-8 编码,这意味着你可以处理包含各种语言字符的文本,而不仅仅是 ASCII 字符。
  4. 丰富的功能: 提供了模式匹配、查找、捕获、替换、迭代等多种功能。
  5. 清晰的 API: 提供了直观易用的 API 接口。

因此,对于大多数 Rust 项目来说,regex crate 是处理正则表达式的首选。

第二章:环境准备 – 安装 regex crate

在开始使用 regex crate 之前,你需要将其添加到你的 Rust 项目依赖中。打开你的项目的 Cargo.toml 文件,并在 [dependencies] 部分添加如下行:

toml
[dependencies]
regex = "1.x" # 使用最新的 1.x 版本,可以查阅 crates.io 获取最新版本号

保存文件后,Cargo 会在下次构建项目时自动下载并编译 regex crate。

bash
cargo build

现在,你就可以在你的 Rust 代码中使用 regex crate 提供的功能了。

第三章:基础入门 – 创建与匹配

使用 regex crate 的基本流程通常是:

  1. 导入 regex::Regex 类型。
  2. 定义一个正则表达式模式字符串。
  3. 使用 Regex::new() 方法编译正则表达式模式,得到一个 Regex 对象。
  4. 使用 Regex 对象的方法(如 is_match, find, captures 等)对目标字符串进行操作。

3.1 导入 Regex 类型

在你的 Rust 源文件(例如 src/main.rs)的开头,添加导入语句:

rust
use regex::Regex;

3.2 编译正则表达式

正则表达式模式是一个字符串。为了高效地进行匹配,regex crate 需要先将这个模式字符串编译成一个内部表示形式。这个过程通过 Regex::new() 方法完成。

Regex::new() 方法接收一个字符串切片(&str)作为参数,表示正则表达式模式。它返回一个 Result<Regex, regex::Error>,因为编译过程可能会因为模式语法错误而失败。

“`rust
use regex::Regex;

fn main() {
// 定义一个简单的模式:查找 “Rust”
let pattern = “Rust”;

// 编译正则表达式
let re = Regex::new(pattern);

match re {
    Ok(regex) => {
        println!("正则表达式编译成功!");
        // 接下来就可以使用 regex 对象进行匹配了
    }
    Err(err) => {
        eprintln!("正则表达式编译失败: {}", err);
        // 处理错误,例如退出程序或使用默认逻辑
    }
}

}
“`

在实际开发中,特别是在示例代码中,为了简洁,你可能会看到使用 .unwrap().expect() 来处理 Result

“`rust
use regex::Regex;

fn main() {
let pattern = “Rust”;
// 使用 .unwrap(),如果编译失败会 panic
let re = Regex::new(pattern).unwrap();
println!(“正则表达式编译成功!”);

// 或者使用 .expect() 提供更友好的错误消息
let pattern_invalid = "["; // 这是一个无效的正则表达式
let re_invalid = Regex::new(pattern_invalid).expect("Failed to compile regex"); // 程序会在这里 panic 并打印消息

}
“`

重要提示: 编译正则表达式 (Regex::new()) 是一个相对耗时的操作。如果你的程序需要多次使用同一个正则表达式,你应该只编译一次,然后复用得到的 Regex 对象。我们将在后面的章节讨论如何有效地管理 Regex 对象的生命周期,特别是在多线程或函数内部使用时。

3.3 检查是否匹配 (is_match)

最简单的使用场景是检查一个字符串是否包含某个模式。Regex 对象提供了 is_match() 方法,它接收一个字符串切片,如果字符串中存在至少一个匹配项,则返回 true,否则返回 false

“`rust
use regex::Regex;

fn main() {
let re = Regex::new(r”Rust”).unwrap(); // r”” 表示原始字符串,避免反斜杠转义问题

let text1 = "Hello, Rust!";
let text2 = "Hello, world!";

println!("'{}' 是否包含 'Rust'? {}", text1, re.is_match(text1)); // 输出: true
println!("'{}' 是否包含 'Rust'? {}", text2, re.is_match(text2)); // 输出: false

}
“`

注意模式字符串前的 r。在 Rust 中,r"..." 表示一个原始字符串(raw string),其中的内容不会被 Rust 编译器解析转义字符。这对于包含大量反斜杠(如文件路径或正则表达式)的字符串非常有用,可以避免写成 \\ 的麻烦。在正则表达式中,反斜杠 \ 经常用于转义特殊字符或表示字符类,因此使用原始字符串是最佳实践。

3.4 查找第一个匹配项 (find)

如果你想获取第一个匹配到的子串及其在原字符串中的位置,可以使用 find() 方法。它返回一个 Option<Match>。如果找到匹配项,返回 Some(Match),其中 Match 结构体包含匹配子串的起始和结束字节索引以及子串本身;如果没有找到,返回 None

“`rust
use regex::Regex;

fn main() {
let re = Regex::new(r”\d+”).unwrap(); // 查找一个或多个数字

let text = "User ID: 12345, Transaction ID: 67890";

match re.find(text) {
    Some(match_) => {
        println!("找到第一个匹配项:");
        println!("  匹配文本: {}", match_.as_str()); // 获取匹配的子串
        println!("  起始位置 (字节): {}", match_.start()); // 获取匹配的起始字节索引
        println!("  结束位置 (字节): {}", match_.end());   // 获取匹配的结束字节索引 (不包含)
        println!("  匹配在原字符串中的部分: '{}'", &text[match_.start()..match_.end()]); // 验证
    }
    None => {
        println!("没有找到匹配项.");
    }
}

}
“`

3.5 捕获匹配项 (captures)

正则表达式的一个非常强大的功能是“捕获组”(Capture Groups)。通过在模式中使用括号 (),你可以将模式的一部分标记为一个捕获组。captures() 方法可以找到第一个完整的匹配项,并同时提取所有捕获组的内容。它返回一个 Option<Captures>

Captures 结构体可以像一个数组一样访问捕获的内容。索引 0 总是代表整个匹配项,索引 1 代表第一个捕获组的内容,索引 2 代表第二个捕获组的内容,以此类推。每个捕获项也是一个 Match 结构体。

“`rust
use regex::Regex;

fn main() {
// 捕获年、月、日
let re = Regex::new(r”(\d{4})-(\d{2})-(\d{2})”).unwrap();

let text = "今天是 2023-10-27,明天是 2023-10-28。";

match re.captures(text) {
    Some(caps) => {
        println!("找到第一个日期:");
        println!("  整个匹配: {}", caps.get(0).unwrap().as_str()); // 整个匹配项
        println!("  年份: {}", caps.get(1).unwrap().as_str());   // 第一个捕获组 (年份)
        println!("  月份: {}", caps.get(2).unwrap().as_str());   // 第二个捕获组 (月份)
        println!("  日期: {}", caps.get(3).unwrap().as_str());   // 第三个捕获组 (日期)

        // 也可以通过索引直接访问 Match,然后获取字符串或位置
        let year_match = caps.get(1).unwrap();
        println!("  年份子串位置: {}..{}", year_match.start(), year_match.end());
    }
    None => {
        println!("没有找到匹配的日期.");
    }
}

}
“`

使用 unwrap() 是因为我们知道 captures 方法返回 Some,并且捕获组的索引是有效的。在实际应用中,如果不能确定捕获组是否存在(例如使用了可选捕获 (...)?),应该使用 get(index) 返回的 Option<Match> 并进行适当的错误处理或模式匹配。

第四章:正则表达式语法快速入门

要有效地使用 regex crate,理解正则表达式的常用语法至关重要。以下是一些最基本和常用的语法元素:

  • 字面字符: 大多数字符(如字母、数字)都匹配它们本身。
    • abc 匹配字符串 “abc”。
  • 元字符 (Metacharacters): 具有特殊含义的字符。
    • . (点): 匹配除换行符 \n 以外的任意单个字符。
    • ^ (脱字符): 匹配字符串的开始位置。
    • $ (美元符号): 匹配字符串的结束位置。
    • | (竖线): 逻辑或,匹配 | 符号前或后的模式。例如 cat|dog 匹配 “cat” 或 “dog”。
    • ? (问号): 匹配前面的元素零次或一次(可选)。
    • * (星号): 匹配前面的元素零次或多次。
    • + (加号): 匹配前面的元素一次或多次。
    • () (圆括号): 用于分组和捕获。
    • [] (方括号): 定义字符集。
    • \ (反斜杠): 转义字符,将特殊字符转义为字面字符,或将字面字符转义为特殊序列。
  • 转义 (Escaping): 如果你想匹配一个元字符本身,需要在它前面加上反斜杠 \。例如,要匹配字面点号 .,你需要使用 \.;要匹配反斜杠 \,你需要使用 \\。在 Rust 中使用原始字符串 r"..." 可以简化这里的书写。
  • 字符集 ([]): 匹配方括号中列出的任意单个字符。
    • [abc] 匹配 “a”, “b”, 或 “c”。
    • [0-9] 匹配任意一个数字。
    • [a-zA-Z] 匹配任意一个英文字母(大写或小写)。
    • [^abc] 匹配除了 “a”, “b”, 和 “c” 以外的任意单个字符(注意 ^[] 内的特殊含义)。
    • [a-zA-Z0-9_] 匹配字母、数字或下划线。
  • 预定义字符类: 一些常用的字符集有简写形式。
    • \d: 匹配任意一个数字 (等同于 [0-9])。
    • \D: 匹配任意一个非数字字符 (等同于 [^0-9])。
    • \w: 匹配任意一个“词语字符”(字母、数字或下划线)(等同于 [a-zA-Z0-9_])。
    • \W: 匹配任意一个非“词语字符” (等同于 [^a-zA-Z0-9_])。
    • \s: 匹配任意一个空白字符(空格、制表符、换行符等)。
    • \S: 匹配任意一个非空白字符。
  • Unicode 字符类 (\p{...}): regex crate 支持通过 Unicode 属性匹配字符。
    • \p{Lu}: 匹配任意一个大写字母。
    • \p{N}: 匹配任意一个数字。
    • \p{Emoji}: 匹配任意一个 Emoji 字符。
    • \p{Han}: 匹配任意一个汉字。
    • \P{...}: 匹配不具有该属性的字符。例如 \P{N} 匹配任意非数字字符。
    • 你可以查阅 regex crate 的文档获取完整的 Unicode 属性列表。
  • 量词 (Quantifiers): 控制前面的元素出现的次数。
    • ?: 零次或一次。
    • *: 零次或多次。
    • +: 一次或多次。
    • {n}: 精确匹配 n 次。
    • {n,}: 匹配 n 次或更多次。
    • {n,m}: 匹配至少 n 次,至多 m 次。
  • 贪婪与非贪婪匹配 (Greedy vs. Lazy): 默认情况下,量词是“贪婪”的,它们会尽可能多地匹配字符。例如,模式 <.*> 匹配字符串 <p><b>Hello</b></p> 时,会匹配整个 <p><b>Hello</b></p>,而不是只匹配 <p><b>。通过在量词后面加上 ?,可以使其变为“非贪婪”或“懒惰”模式,此时它们会尽可能少地匹配字符。例如,模式 <.*?> 在同一字符串中会匹配 <p>,然后继续匹配到 <b>,再匹配 </b>,最后匹配 </p>
    • ??: 零次或一次,非贪婪。
    • *?: 零次或多次,非贪婪。
    • +?: 一次或多次,非贪婪。
    • {n,m}?: 至少 n 次,至多 m 次,非贪婪。
    • {n,}?: n 次或更多次,非贪婪。
  • 边界匹配 (Anchors): 不匹配实际字符,而是匹配位置。
    • ^: 匹配行的开始位置 (在多行模式下,也匹配换行符后的位置)。
    • $: 匹配行的结束位置 (在多行模式下,也匹配换行符前的位置)。
    • \b: 匹配单词边界。单词边界是指一个单词字符(\w)和一个非单词字符(\W)之间的位置,或者一个单词字符和字符串的开始/结束之间的位置。
    • \B: 匹配非单词边界。

掌握这些基本语法,你就可以构建大多数常见的正则表达式模式了。

第五章:更多用法 – 查找所有匹配项与替换

除了检查匹配和查找第一个匹配,regex crate 还提供了查找所有匹配项以及进行文本替换的功能。

5.1 查找所有匹配项 (find_iter)

find_iter() 方法返回一个迭代器,该迭代器会产生输入字符串中所有不重叠的匹配项(Match 结构体)。

“`rust
use regex::Regex;

fn main() {
let re = Regex::new(r”\d+”).unwrap(); // 查找所有数字序列

let text = "Items: 10, Price: 25.50, Quantity: 3, Total: 76.50";

println!("找到所有数字:");
for mat in re.find_iter(text) {
    println!("  匹配文本: {}, 位置: {}..{}", mat.as_str(), mat.start(), mat.end());
}

}
“`

5.2 查找所有捕获项 (captures_iter)

类似地,captures_iter() 方法返回一个迭代器,产生输入字符串中所有不重叠的完整匹配项及其对应的捕获组(Captures 结构体)。

“`rust
use regex::Regex;

fn main() {
// 查找所有日期,并捕获年、月、日
let re = Regex::new(r”(\d{4})-(\d{2})-(\d{2})”).unwrap();

let text = "日期列表:2023-10-27, 2024-01-15, 2025-05-30.";

println!("找到所有日期及其组成部分:");
for caps in re.captures_iter(text) {
    let full_match = caps.get(0).unwrap().as_str();
    let year = caps.get(1).unwrap().as_str();
    let month = caps.get(2).unwrap().as_str();
    let day = caps.get(3).unwrap().as_str();

    println!("  完整日期: {}, 年: {}, 月: {}, 日: {}", full_match, year, month, day);
}

}
“`

5.3 文本替换 (replace, replace_all)

regex crate 提供了强大的文本替换功能。replace()replace_all() 方法用于替换匹配到的子串。

  • replace(text, rep): 只替换 第一个 匹配项。rep 可以是一个字符串切片,或者一个闭包。
  • replace_all(text, rep): 替换 所有 不重叠的匹配项。rep 也可以是一个字符串切片,或者一个闭包。

rep 是一个字符串切片时,你可以使用 $N${N} 来引用第 N 个捕获组的内容,使用 $0${0} 引用整个匹配项。

“`rust
use regex::Regex;

fn main() {
let re_numbers = Regex::new(r”\d+”).unwrap();
let text_numbers = “Score: 100, Time: 60”;

// 替换第一个数字
let replaced_first = re_numbers.replace(text_numbers, "N/A");
println!("替换第一个: {}", replaced_first); // 输出: Score: N/A, Time: 60

// 替换所有数字
let replaced_all = re_numbers.replace_all(text_numbers, "X");
println!("替换所有: {}", replaced_all); // 输出: Score: X, Time: X

// 使用捕获组进行替换 (替换日期格式)
let re_date = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
let text_date = "今天是 2023-10-27.";

// 将 YYYY-MM-DD 格式替换为 DD/MM/YYYY
// $1 是年,$2 是月,$3 是日
let formatted_date = re_date.replace_all(text_date, "$3/\/\");
println!("格式化日期: {}", formatted_date); // 输出: 今天是 27/10/2023.

}
“`

rep 是一个闭包时,闭包会接收一个 Captures 对象作为参数,并需要返回一个 Cow<'_, str> (Clone-on-Write) 类型的值,表示替换后的字符串。这使得你可以根据捕获的内容动态地生成替换文本。

“`rust
use regex::Regex;
use std::borrow::Cow;

fn main() {
// 查找所有单词并转换为大写
let re_word = Regex::new(r”\w+”).unwrap();
let text_words = “hello world rust”;

let uppercased = re_word.replace_all(text_words, |caps: &Captures| {
    // caps.get(0) 是整个匹配项
    let word = caps.get(0).unwrap().as_str();
    // 将单词转换为大写,并返回 Cow::Owned 或 Cow::Borrowed
    Cow::Owned(word.to_uppercase())
});
println!("单词转大写: {}", uppercased); // 输出: HELLO WORLD RUST

// 查找数字并将其值加一 (更复杂的动态替换)
let re_number = Regex::new(r"\d+").unwrap();
let text_calc = "Value: 10, Count: 5";

let incremented = re_number.replace_all(text_calc, |caps: &Captures| {
    let num_str = caps.get(0).unwrap().as_str();
    if let Ok(num) = num_str.parse::<i32>() {
        // 成功解析为数字,计算新值并返回字符串
        Cow::Owned((num + 1).to_string())
    } else {
        // 解析失败,保持原样或返回错误指示
        Cow::Borrowed(num_str) // 或者 Cow::Owned("Error".to_string())
    }
});
println!("数字加一: {}", incremented); // 输出: Value: 11, Count: 6

}
“`

使用闭包进行替换非常灵活,可以处理各种复杂的替换逻辑。

第六章:高级主题与性能考虑

6.1 模式修饰符/标志 (Flags)

正则表达式引擎通常支持一些标志来修改匹配行为,如忽略大小写、多行匹配等。regex crate 支持在模式字符串的开头使用 (?flags) 的形式来启用这些标志,或者使用 RegexBuilder 来设置。

常用的标志:

  • i: 忽略大小写 (case-insensitive)。
  • m: 多行模式 (multiline)。^ 匹配每行的开头,$ 匹配每行的结尾(在换行符后/前)。
  • s: 单行模式 (dotall)。使 . 匹配包括换行符在内的所有字符。
  • x: 忽略模式中的空白和 # 后面的注释 (extended/verbose)。

使用 (?flags):

“`rust
use regex::Regex;

fn main() {
// (?i) 启用忽略大小写
let re = Regex::new(r”(?i)rust”).unwrap();

let text1 = "Hello, Rust!";
let text2 = "hello, ruSt!";

println!("'{}' 是否包含忽略大小写的 'rust'? {}", text1, re.is_match(text1)); // true
println!("'{}' 是否包含忽略大小写的 'rust'? {}", text2, re.is_match(text2)); // true

}
“`

使用 RegexBuilder (更推荐的方式,特别是当需要设置多个标志或更精细的控制时):

“`rust
use regex::RegexBuilder;

fn main() {
let re = RegexBuilder::new(r”rust”)
.case_insensitive(true) // 忽略大小写
.build() // 编译
.unwrap();

let text1 = "Hello, Rust!";
let text2 = "hello, ruSt!";

println!("'{}' 是否包含忽略大小写的 'rust'? {}", text1, re.is_match(text1)); // true
println!("'{}' 是否包含忽略大小写的 'rust'? {}", text2, re.is_match(text2)); // true

}
“`

RegexBuilder 提供了更多选项,例如 multi_line(), dot_matches_new_line(), ignore_whitespace(), max_size(), max_levels_NFA() 等,可以用于精细控制正则表达式引擎的行为和资源使用。

6.2 非捕获组 ((?:...))

如果你只需要使用括号 () 来进行分组(例如,为了应用量词或 | 逻辑或),但不需要捕获组的内容,可以使用非捕获组 (?:...)。这可以稍微提高性能,并避免在 Captures 结果中产生不必要的捕获槽。

“`rust
use regex::Regex;

fn main() {
// 捕获协议和域名,但只分组端口 (可选)
let re = Regex::new(r”(https?)://([\w.]+)(?::(\d+))?”).unwrap(); // (?:…) 是非捕获组

let url1 = "https://www.example.com";
let url2 = "http://localhost:8080";

if let Some(caps) = re.captures(url1) {
    println!("URL1 协议: {}", caps.get(1).unwrap().as_str()); // 协议
    println!("URL1 域名: {}", caps.get(2).unwrap().as_str()); // 域名
    println!("URL1 端口: {:?}", caps.get(3).map(|m| m.as_str())); // 端口 (Option<&str>)
}

 if let Some(caps) = re.captures(url2) {
    println!("URL2 协议: {}", caps.get(1).unwrap().as_str());
    println!("URL2 域名: {}", caps.get(2).unwrap().as_str());
    println!("URL2 端口: {:?}", caps.get(3).map(|m| m.as_str()));
}

}
“`

在上面的例子中,(https?) 是捕获组 1,([\w\.]+) 是捕获组 2,(?:(\d+))? 是一个非捕获组 (?:...) 内部包含一个可选的捕获组 (\d+)。外层的 (?:...) 本身不创建捕获槽,但它允许我们将 :(\d+) 整体作为一个可选的部分 ?。内部的 (\d+) 是捕获组 3,即使它在非捕获组内部。

6.3 性能优化 – 编译一次,重复使用

如前所述,编译正则表达式是一个相对昂贵的操作。在 Rust 中,如果你的正则表达式在程序的生命周期内是固定的,你应该只编译一次,然后在需要的地方重复使用同一个 Regex 对象。

对于需要多次使用同一个 Regex 的函数或方法,可以将 Regex 对象作为参数传递,或者将其存储在结构体的字段中。

对于在整个程序中多次、多处使用的全局或静态正则表达式,最好的方法是使用 lazy_staticonce_cell crate 来确保它只被编译一次,并且可以安全地在程序的任何地方访问。once_cell 是 Rust 1.56 版本后标准库提供的 OnceCell 的更通用版本,通常是创建静态变量的首选方式。

首先,将 once_cell 添加到 Cargo.toml:

toml
[dependencies]
regex = "1.x"
once_cell = "1.x" # 或 "1.x.x" for specific version

然后,使用 once_cell::sync::OnceCell (或 lazy_static) 来存储编译好的 Regex 对象:

“`rust
use regex::Regex;
use once_cell::sync::OnceCell;

// 定义一个静态 OnceCell,用于存储 Regex 对象
static EMAIL_RE: OnceCell = OnceCell::new();

fn is_valid_email(email: &str) -> bool {
// 获取或初始化 Regex 对象
let re = EMAIL_RE.get_or_init(|| {
// 这是一个简单的邮箱格式校验正则表达式示例
// 真实世界的邮箱格式非常复杂,此模式仅用于演示
Regex::new(r”^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$”).unwrap()
});

// 使用编译好的 Regex 对象进行匹配
re.is_match(email)

}

fn main() {
let email1 = “[email protected]”;
let email2 = “invalid-email”;

println!("'{}' is valid email? {}", email1, is_valid_email(email1));
println!("'{}' is valid email? {}", email2, is_valid_email(email2));

// 再次调用,不会重新编译
println!("'[email protected]' is valid email? {}", is_valid_email("[email protected]"));

}
“`

EMAIL_RE.get_or_init(|| ...) 会在第一次调用时执行闭包来编译正则表达式,并将其存储在 OnceCell 中。后续调用 get_or_init 时,会直接返回已存储的 Regex 对象的引用,而不会再次执行编译闭包。这种模式既保证了编译的单次性,又提供了延迟初始化(只在第一次需要时编译)。

如果你在使用 Rust 1.56 之前的版本,或者更喜欢 lazy_static 的宏语法,也可以使用 lazy_static crate:

“`rust

[macro_use]

extern crate lazy_static;
use regex::Regex;

lazy_static! {
static ref PHONE_RE: Regex = Regex::new(r”^\d{3}-\d{3}-\d{4}$”).unwrap();
}

fn is_valid_us_phone(phone: &str) -> bool {
PHONE_RE.is_match(phone)
}

fn main() {
let phone1 = “123-456-7890”;
let phone2 = “1234567890”;

println!("'{}' is valid US phone? {}", phone1, is_valid_us_phone(phone1));
println!("'{}' is valid US phone? {}", phone2, is_valid_us_phone(phone2));

}
“`

lazy_static 宏会在第一次访问 PHONE_RE 时执行其后的表达式(Regex::new(...)),并将其存储为一个静态变量。

6.4 regex crate 的限制

尽管 regex crate 功能强大且性能优异,但为了保证线性时间复杂度,它有意地省略了一些在其他正则表达式引擎中常见的特性:

  • 没有回溯 (No Backtracking): regex crate 使用的是一种不依赖回溯的匹配算法。这意味着某些依赖复杂回溯才能工作的模式(在其他引擎中可能导致性能问题)在这里可能无法表达或需要不同的写法。
  • 不支持任意 Lookaround (Lookahead/Lookbehind): regex crate 不直接支持 Lookahead ((?=...), (?!...)) 和 Lookbehind ((?<=...), (?<!...)) 断言。这是保证性能的关键决策。在很多情况下,你可以通过重新组织模式或在匹配后进行额外的字符串处理来达到类似的目的。例如,要匹配紧跟在 “prefix” 后面的 “target”,你不能使用 prefix(?=target),但你可以匹配 prefix(target) 并检查第二个捕获组,或者匹配 prefixtarget 并检查匹配是否以 “prefix” 开头。
  • 不支持条件表达式 ((?(condition)yes|no)) 等复杂结构: 这些特性通常也依赖于回溯。

对于绝大多数常见的正则表达式任务,regex crate 提供的功能已经足够,并且其线性的性能保证使其成为处理大量文本数据的理想选择。如果你确实需要这些高级特性,你可能需要考虑使用其他 crate,但请注意权衡其潜在的性能和安全风险。

第七章:错误处理

我们已经看到 Regex::new() 返回一个 Result<Regex, regex::Error>。在真实的应用程序中,不应该简单地使用 .unwrap().expect(),而应该适当地处理潜在的错误。

regex::Error 枚举表示编译正则表达式时可能发生的错误,例如语法错误、超出大小限制等。

“`rust
use regex::Regex;
use regex::Error; // 导入具体的错误类型

fn create_regex(pattern: &str) -> Result {
Regex::new(pattern)
}

fn main() {
let valid_pattern = r”\d+”;
let invalid_pattern = r”[“; // 缺少闭合括号,无效

match create_regex(valid_pattern) {
    Ok(re) => {
        println!("成功编译正则表达式: {}", valid_pattern);
        // 使用 re ...
    }
    Err(err) => {
        eprintln!("编译正则表达式 '{}' 失败: {}", valid_pattern, err);
    }
}

match create_regex(invalid_pattern) {
    Ok(re) => {
         println!("成功编译正则表达式: {}", invalid_pattern); // 这不会发生
    }
    Err(err) => {
        eprintln!("编译正则表达式 '{}' 失败: {}", invalid_pattern, err);
        // 可以根据错误类型进行更精细的处理
        // 例如,检查 err 是否是 Syntax 错误
         if let Error::Syntax(_) = err {
             eprintln!("这是一个语法错误。");
         }
    }
}

}
“`

通过返回 Result 并在调用处使用 match? 运算符来处理错误,可以使你的代码更加健壮。

第八章:实践示例

让我们结合之前学到的知识,看一些更贴近实际的例子。

示例 1: 从日志行中提取信息

假设你有一些格式化的日志行,你想从中提取时间和消息内容。

text
[2023-10-27 10:30:15] INFO: User logged in.
[2023-10-27 10:30:20] ERROR: Database connection failed.

我们可以设计一个正则表达式来匹配这种模式并捕获关键信息。

“`rust
use regex::Regex;

fn main() {
// 匹配格式 [YYYY-MM-DD HH:MM:SS] LEVEL: Message
// 捕获:1: 日期时间, 2: 级别, 3: 消息
let log_re = Regex::new(r”^[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})] (\w+): (.*)$”).unwrap();

let log_lines = vec![
    "[2023-10-27 10:30:15] INFO: User logged in.",
    "[2023-10-27 10:30:20] ERROR: Database connection failed.",
    "This is not a log line.",
];

println!("解析日志行:");
for line in log_lines {
    if let Some(caps) = log_re.captures(line) {
        let timestamp = caps.get(1).unwrap().as_str();
        let level = caps.get(2).unwrap().as_str();
        let message = caps.get(3).unwrap().as_str();
        println!("  时间: {}, 级别: {}, 消息: {}", timestamp, level, message);
    } else {
        println!("  跳过非日志格式的行: {}", line);
    }
}

}
“`

这个例子展示了如何结合 captures 和基本的正则表达式语法来解析结构化的文本数据。

示例 2: 查找和修改 URL 中的特定参数

假设你想找到 URL 中所有的 id=... 参数,并可能修改它们。

“`rust
use regex::Regex;
use std::borrow::Cow;

fn main() {
// 查找 id= 后面的数字
let id_re = Regex::new(r”id=(\d+)”).unwrap();

let url = "https://example.com/page?id=123&name=test&id=456";

println!("查找所有 ID:");
for mat in id_re.find_iter(url) {
     // mat 是整个 "id=..." 的匹配项
     let full_match = mat.as_str();
     // 可以通过 caps() 方法来获取这个匹配项中的捕获组
     if let Some(caps) = id_re.captures(full_match) {
         let id_value = caps.get(1).unwrap().as_str();
         println!("  找到 ID 参数: {}, 值为: {}", full_match, id_value);
     }
}

// 将所有 id 值加 100
let modified_url: Cow<str> = id_re.replace_all(url, |caps: &Captures| {
    let id_str = caps.get(1).unwrap().as_str();
    if let Ok(id) = id_str.parse::<i32>() {
        Cow::Owned(format!("id={}", id + 100)) // 替换为 id=新值
    } else {
         // 解析失败,保持原样
        caps.get(0).unwrap().as_str().into() // 返回整个匹配项
    }
});
println!("修改后的 URL: {}", modified_url);
// 输出: 修改后的 URL: https://example.com/page?id=223&name=test&id=556

}
“`

这个例子结合了 find_iterreplace_all (带闭包) 来查找特定模式并基于捕获的值进行复杂的修改。

第九章:总结与下一步

恭喜你!你已经学习了 Rust 中 regex crate 的基础和核心用法,包括:

  • 为什么选择 regex crate。
  • 如何安装和导入。
  • 如何编译正则表达式 (Regex::new)。
  • 如何进行基本匹配 (is_match, find, captures)。
  • 常用的正则表达式语法元素。
  • 如何查找所有匹配项 (find_iter, captures_iter)。
  • 如何进行文本替换 (replace, replace_all,包括使用捕获组和闭包)。
  • 如何使用模式修饰符/标志 (RegexBuilder)。
  • 性能优化策略 (编译一次,使用 once_celllazy_static)。
  • 错误处理。
  • 通过实践示例巩固知识。

正则表达式是一个非常强大且灵活的工具,熟练掌握它能极大地提高你处理文本数据的效率。Rust 的 regex crate 为你提供了这个能力,同时保证了高性能和安全性。

下一步,你可以:

  1. 练习: 尝试解决一些字符串处理问题,使用正则表达式来实现,比如校验电话号码、解析 CSV 文件、查找特定的代码模式等。
  2. 深入语法: 正则表达式的语法远不止这些,还有零宽断言 (Lookaround – 虽然 regex crate 不完全支持任意形式,但理解概念有益)、反向引用 (Backreferences – regex crate 支持编号和命名的反向引用用于替换字符串) 等。查阅更全面的正则表达式语法教程。
  3. 查阅 regex 文档: regex crate 的官方文档非常详细,是深入了解其高级功能(如命名捕获组 (?P<name>...)RegexBuilder 的所有选项、Unicode 支持的细节等)的最佳资源。
  4. 了解其他 crate: 如果你的需求无法通过 regex 满足(例如必须使用复杂的 Lookaround),可以了解其他 Rust regex crate,但请注意其性能和安全特性。

希望这篇教程能帮助你踏上使用 Rust 正则表达式的旅程!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部