regex
Создание и компиляция шаблонов: Regex::new
, обработка ошибок
Группы захвата и извлечение данных
Итерация по совпадениям
Замена текста
Производительность: кэширование Regex
, ленивая компиляция
Примеры
Упражнение: Извлечение email-адресов из текстового файла
Сегодня мы погрузимся в увлекательный мир регулярных выражений (regular expressions, или regex) — мощного инструмента для обработки текста, поиска совпадений, извлечения данных и замены строк. Мы разберём всё от основ до продвинутых техник, используя библиотеку regex
в Rust, и завершим лекцию практическим упражнением. Приступим!
Регулярные выражения — это шаблоны, описывающие набор строк. Они используются для поиска, проверки, извлечения или замены текста на основе заданных правил. Например, вы можете найти все email-адреса в тексте, проверить, соответствует ли строка формату даты, или заменить все вхождения слова на другое.
Rust — язык, ориентированный на производительность и безопасность. Регулярные выражения позволяют эффективно обрабатывать текстовые данные, что полезно в задачах парсинга логов, валидации ввода, анализа данных и многого другого.
Синтаксис регулярных выражений в Rust основан на стандарте Perl (PCRE, Perl-Compatible Regular Expressions), но с некоторыми ограничениями для обеспечения производительности. PCRE — это широко используемый стандарт, и вы можете найти его полную документацию здесь: PCRE Documentation. Вот ключевые элементы синтаксиса:
abc
— ищет точное совпадение "abc"..
— любой символ (кроме новой строки, если не включён многострочный режим).*
— 0 или более повторений (жадный захват).+
— 1 или более повторений (жадный захват).?
— 0 или 1 повторение.|
— логическое "или".[a-z]
— любой символ от 'a' до 'z' (зависит от регистра).[^0-9]
— любой символ, кроме цифр.\d
— любая цифра (эквивалент [0-9]
).\D
— любой символ, кроме цифры (эквивалент [^0-9]
).\w
— любой буквенно-цифровой символ или подчёркивание (эквивалент [a-zA-Z0-9_]
).\W
— любой символ, кроме буквенно-цифровых и подчёркивания (эквивалент [^a-zA-Z0-9_]
).\s
— любой пробельный символ (пробел, табуляция, новая строка и т.д.).\S
— любой непробельный символ.(abc)
— захватывающая группа: сохраняет "abc" для последующего извлечения.(?:abc)
— незахватывающая группа: группирует "abc" для шаблона, но не сохраняет.^
— начало строки (или начало текста в однострочном режиме).$
— конец строки (или конец текста в однострочном режиме).\b
— граница слова (между \w
и \W
).\B
— не граница слова.*
, +
, {n,m}
— по умолчанию "жадные" (захватывают максимально возможное количество символов).*?
, +?
, {n,m}?
— "не жадные" (захватывают минимально возможное количество символов)..
не включает новую строку, а ^
и $
привязаны к началу/концу всего текста.(?m)
: ^
и $
работают для каждой строки, а .
всё ещё исключает \n
(для включения используйте (?s)
).Пример: ^\d{2}-\w+$
— строка начинается с двух цифр, затем дефис, затем одно или более буквенно-цифровых символов до конца строки.
a.*b
в "a123b456b" найдёт "a123b456b" (всё до последнего "b").a.*?b
найдёт "a123b" (до первого "b").^abc$
на "abc\ndef" не сработает.(?m)
: ^abc$
найдёт "abc" в первой строке.regex
Для работы с регулярными выражениями в Rust используется crate regex
. Добавьте его в ваш Cargo.toml
:
[dependencies]
regex = "1.10"
После этого выполните cargo build
, чтобы скачать и скомпилировать библиотеку.
Библиотека предоставляет структуру Regex
, которая компилирует шаблон и позволяет выполнять операции с текстом. Вот ключевые методы:
is_match
— проверяет, есть ли совпадение в строке.find
— возвращает первое совпадение.captures
— извлекает группы захвата из первого совпадения.Пример:
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap(); // Ищет одну или более цифр
let text = "Rust 2025";
println!("Есть совпадение? {}", re.is_match(text)); // true
if let Some(mat) = re.find(text) {
println!("Найдено: {}", mat.as_str()); // "2025"
}
}
r
перед строкой в Regex::new(r"asd")
означает "сырая строка".
Это упрощает работу с регулярными выражениями, позволяя писать \
без экранирования.
Без r
тебе пришлось бы писать \\d
, \\s
, \\b
, удваивая слэши, чтобы они дошли до библиотеки regex
в правильном виде.
Сырые строки избавляют от этой необходимости, делая код чище и понятнее.
Regex::new
, обработка ошибокRegex
Метод Regex::new
принимает строку с шаблоном и возвращает Result<Regex, Error>
. Если шаблон синтаксически неверен, вы получите ошибку.
use regex::Regex;
fn main() {
match Regex::new(r"[a-z+") {
Ok(_) => println!("Шаблон корректен"),
Err(e) => println!("Ошибка: {}", e), // Ошибка: незакрытая скобка
}
}
Всегда обрабатывайте ошибки, чтобы ваш код был надёжным. Используйте unwrap()
только в примерах или если вы уверены в корректности шаблона.
use regex::Regex;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let re = Regex::new(r"\d+")?;
println!("Шаблон скомпилирован!");
Ok(())
}
Группы, заключённые в ()
, позволяют извлечь части совпадения. Например, в шаблоне (\d+)-(\w+)
первая группа — цифры, вторая — слово. Есть два типа групп:
(abc)
— сохраняет совпадение для последующего использования (например, через caps[1]
).(?:abc)
— используется только для группировки в шаблоне, но не сохраняется в результатах.use regex::Regex;
fn main() {
// Захватывающая группа
let re_capture = Regex::new(r"(\w+)-(\d+)").unwrap();
let text = "dog-123";
if let Some(caps) = re_capture.captures(text) {
println!("Группа 1 (захват): {}", &caps[1]); // "dog"
println!("Группа 2 (захват): {}", &caps[2]); // "123"
}
// Незахватывающая группа
let re_non_capture = Regex::new(r"(?:\w+)-(\d+)").unwrap();
if let Some(caps) = re_non_capture.captures(text) {
println!("Группа 1 (захват): {}", &caps[1]); // "123"
// caps[2] недоступно, т.к. (?:\w+) не захватывает
}
}
Представь, что ты ищешь в магазине "коробку с конфетами" или просто "конфеты". Захватывающая группа (коробка)-(конфеты)
— это как если ты записываешь и коробку, и конфеты. Незахватывающая (?:коробка)-(конфеты)
— это когда тебе важны только конфеты, а коробка нужна лишь для поиска, но ты её не запоминаешь.
captures
Метод captures
возвращает структуру Captures
, из которой можно извлечь группы по индексу или имени.
use regex::Regex;
fn main() {
let re = Regex::new(r"(\d+)-(\w+)").unwrap();
let text = "123-rust";
if let Some(caps) = re.captures(text) {
println!("Полное совпадение: {}", &caps[0]); // "123-rust"
println!("Первая группа: {}", &caps[1]); // "123"
println!("Вторая группа: {}", &caps[2]); // "rust"
}
}
Вы можете дать группам имена с помощью синтаксиса (?P<имя>шаблон)
:
use regex::Regex;
fn main() {
let re = Regex::new(r"(?P<number>\d+)-(?P<word>\w+)").unwrap();
let text = "456-code";
if let Some(caps) = re.captures(text) {
println!("Число: {}", caps.name("number").unwrap().as_str()); // "456"
println!("Слово: {}", caps.name("word").unwrap().as_str()); // "code"
}
}
find_iter
Для поиска всех совпадений используйте find_iter
:
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
let text = "Year 2023, version 15";
for mat in re.find_iter(text) {
println!("Найдено: {}", mat.as_str()); // "2023", "15"
}
}
captures_iter
Для извлечения групп из всех совпадений:
use regex::Regex;
fn main() {
let re = Regex::new(r"(\d+)-(\w+)").unwrap();
let text = "123-rust 456-code";
for caps in re.captures_iter(text) {
println!("Число: {}, Слово: {}", &caps[1], &caps[2]);
}
// Вывод:
// Число: 123, Слово: rust
// Число: 456, Слово: code
}
replace
и replace_all
Методы replace
заменяет первое совпадение, а replace_all
— все совпадения.
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
let text = "Year 2023, version 15";
let replaced = re.replace(text, "XX");
println!("Первая замена: {}", replaced); // "Year XX, version 15"
let replaced_all = re.replace_all(text, "XX");
println!("Полная замена: {}", replaced_all); // "Year XX, version XX"
}
Можно ссылаться на группы с помощью $n
или $имя
:
use regex::Regex;
fn main() {
let re = Regex::new(r"(\d+)-(\w+)").unwrap();
let text = "123-rust";
let result = re.replace(text, "$2-$1");
println!("Результат: {}", result); // "rust-123"
}
Regex
, ленивая компиляцияRegex
Компиляция шаблона — дорогая операция. Создавайте объект Regex
один раз и переиспользуйте его:
use regex::Regex;
fn process_text(text: &str, re: &Regex) {
if re.is_match(text) {
println!("Совпадение найдено!");
}
}
fn main() {
let re = Regex::new(r"\d+").unwrap();
process_text("Rust 2025", &re);
process_text("No numbers", &re);
}
lazy_regex
Для ещё большей оптимизации используйте crate lazy_regex
, который компилирует шаблон только при первом использовании:
[dependencies]
lazy_regex = "3.1"
use lazy_regex::regex;
fn main() {
let re = regex!(r"\d+"); // Компиляция отложена
println!("{}", re.is_match("Rust 2025")); // true
}
Допустим, у нас есть лог:
2025-03-28 10:15:32 ERROR: Disk full
2025-03-28 10:16:01 INFO: Backup started
Извлечём дату и сообщение:
use regex::Regex;
fn main() {
let log = "2025-03-28 10:15:32 ERROR: Disk full\n2025-03-28 10:16:01 INFO: Backup started";
let re = Regex::new(r"(?m)^(\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2} (\w+): (.+)$").unwrap();
for caps in re.captures_iter(log) {
println!("Дата: {}, Уровень: {}, Сообщение: {}", &caps[1], &caps[2], &caps[3]);
}
}
Примечание: Флаг (?m)
делает ^
и $
привязанными к началу и концу каждой строки.
Проверка формата email:
use regex::Regex;
fn is_valid_email(email: &str) -> bool {
let re = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
re.is_match(email)
}
fn main() {
println!("{}", is_valid_email("user@example.com")); // true
println!("{}", is_valid_email("invalid@.com")); // false
}
use regex::Regex;
fn main() {
let text = "<tag>content</tag><tag>more</tag>";
let greedy = Regex::new(r"<tag>.*</tag>").unwrap();
let lazy = Regex::new(r"<tag>.*?</tag>").unwrap();
println!("Жадный: {:?}", greedy.find(text).unwrap().as_str()); // "<tag>content</tag><tag>more</tag>"
println!("Не жадный: {:?}", lazy.find(text).unwrap().as_str()); // "<tag>content</tag>"
}
Напишите программу, которая читает текстовый файл и извлекает все email-адреса, используя регулярные выражения. Сохраните их в вектор и выведите.
Создайте файл emails.txt
с содержимым:
Contact: user1@example.com
Invalid: not-an-email
Support: support@domain.co.uk, sales@domain.com
Код:
use regex::Regex;
use std::fs;
fn extract_emails(file_path: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let content = fs::read_to_string(file_path)?;
let re = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap();
let emails: Vec<String> = re.find_iter(&content)
.map(|mat| mat.as_str().to_string())
.collect();
Ok(emails)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let emails = extract_emails("emails.txt")?;
if emails.is_empty() {
println!("Email-адреса не найдены.");
} else {
println!("Найденные email-адреса:");
for email in emails {
println!("- {}", email);
}
}
Ok(())
}
fs::read_to_string
.find_iter
.Result
.Найденные email-адреса:
- user1@example.com
- support@domain.co.uk
- sales@domain.com
Мы подробно разобрали работу с регулярными выражениями в Rust: от расширенного синтаксиса (включая \w
, \W
, жадность и многострочность) и базовых методов до продвинутых техник, таких как группы захвата, итерация и замена. Вы узнали, как оптимизировать производительность и решать практические задачи. Теперь вы готовы применять эти знания в своих проектах! Попробуйте усложнить упражнение, добавив валидацию email или поддержку более сложных форматов.