Expressions régulières en Rust : guide détaillé
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — библиотекарь, и у тебя есть огромное хранилище с миллионами книг. Однажды приходит читатель и говорит: «Мне нужны все книги, где на обложке есть слово "Приключения", а внутри — хотя бы одна глава про пиратов, и чтобы книга была написана не позднее 2000 года».
Если бы ты просматривал каждую книгу вручную, это заняло бы недели. Но у тебя есть волшебная лупа, которая умеет читать описания книг со скоростью света. Ты просто говоришь лупе: «Найди все книги, где в названии есть слово "Приключения", в содержании — "пираты", а год издания начинается на "199" или меньше». Лупа мгновенно просматривает все записи и выдает тебе список.
Регулярные выражения (Regular Expressions, или сокращенно regex) — это и есть такая «волшебная лупа» для текста. Это специальный язык шаблонов, который позволяет:
- Находить фрагменты текста по сложным условиям
- Проверять, соответствует ли строка определенному формату (например, email или номер телефона)
- Заменять части текста по шаблону
- Разбивать текст на части по указанным разделителям
В реальной жизни мы постоянно используем регулярные выражения, даже не замечая этого. Когда ты ищешь файлы по маске *.docx в проводнике Windows, когда вводишь в поисковике site:example.com "ключевая фраза" — это всё разновидности регулярных выражений.
Важно: Регулярные выражения — это не особенность какого-то одного языка программирования. Это универсальный инструмент, который есть в Python, JavaScript, Rust, Go, Java и даже в текстовых редакторах вроде VS Code или Sublime Text. Различается только синтаксис подключения и некоторые детали, но логика везде одинаковая.
Теория: как это работает изнутри
Чтобы понять, как работают регулярные выражения, нужно разобраться с тремя ключевыми понятиями: конечный автомат, символьные классы и квантификаторы.
Конечный автомат — «мозг» регулярного выражения
Представь, что у тебя есть робот, который стоит в начале строки и держит в руках карту с маршрутом. Карта — это твоё регулярное выражение. Робот читает строку символ за символом и сверяет их с картой. Если символ подходит — он делает шаг вперёд по строке и по карте. Если не подходит — возвращается назад и пробует другой путь.
Этот процесс называется backtracking (возврат). Именно из-за него регулярные выражения могут работать медленно на очень больших текстах, если шаблон составлен неэффективно.
Символьные классы — кирпичики шаблона
Любое регулярное выражение строится из символьных классов. Вот основные из них:
| Символ | Значение | Пример |
|---|---|---|
. |
Любой символ, кроме перевода строки | к.т найдёт «кот», «кит», «к_т» |
d |
Любая цифра (0-9) | dd найдёт «42», «00», «99» |
w |
Любая буква, цифра или подчёркивание | w+ найдёт целое слово |
s |
Любой пробельный символ (пробел, табуляция, перевод строки) | s+ найдёт все пробелы подряд |
[abc] |
Любой символ из перечисленных | [кмн]от найдёт «кот», «мот», «нот» |
[^abc] |
Любой символ, кроме перечисленных | [^к]от найдёт «рот», «бот», но не «кот» |
Квантификаторы — счётчики повторений
Квантификаторы указывают, сколько раз должен повториться предыдущий символ или группа:
| Квантификатор | Значение | Пример |
|---|---|---|
* |
0 или больше раз | ко*т найдёт «кт», «кот», «коот», «кооот» |
+ |
1 или больше раз | ко+т найдёт «кот», «коот», но не «кт» |
? |
0 или 1 раз | ко?т найдёт «кт» и «кот» |
{n} |
Ровно n раз | ко{2}т найдёт только «коот» |
{n,m} |
От n до m раз | ко{1,3}т найдёт «кот», «коот», «кооот» |
Якоря — привязка к позиции
Якоря не ищут символы, они указывают на позицию в строке:
^— начало строки$— конец строки— граница слова
Например, ^Привет найдёт слово «Привет» только если оно стоит в самом начале строки. А мир$ — только если слово «мир» стоит в конце.
Синтаксис в Rust: все формы и варианты
В Rust работа с регулярными выражениями осуществляется через крейт regex. Это не встроенная возможность языка, а внешняя библиотека, которую нужно добавить в проект. Почему так? Потому что Rust стремится к минимализму: в ядре языка только то, что нужно для работы с памятью и производительностью. Всё остальное — библиотеки.
Установка и подключение
Чтобы использовать регулярные выражения в Rust, добавь в файл Cargo.toml:
[dependencies]
regex = "1"
А в коде подключи так:
// Подключаем крейт regex в наш проект
// use — это ключевое слово для импорта
use regex::Regex;
Основные методы
У структуры Regex есть несколько ключевых методов. Вот они в таблице:
| Метод | Что делает | Возвращает |
|---|---|---|
Regex::new(r"...") |
Создаёт новое регулярное выражение | Result<Regex, Error> |
is_match(text) |
Проверяет, есть ли хотя бы одно совпадение | bool |
find(text) |
Находит первое совпадение | Option<Match> |
find_iter(text) |
Находит все совпадения (возвращает итератор) | Matches |
captures(text) |
Находит совпадения с захватом групп | Option<Captures> |
replace(text, replacement) |
Заменяет первое совпадение | String |
replace_all(text, replacement) |
Заменяет все совпадения | String |
Обрати внимание на букву r перед строкой:r"..."— это «сырая» строка (raw string). В ней не нужно экранировать обратные слеши. Без неё пришлось бы писать\dвместоd, что очень неудобно. Всегда используй сырые строки для регулярных выражений в Rust!
Группы захвата
Группы захвата — это способ «вытащить» часть найденного текста. Они обозначаются круглыми скобками (). Например, если мы ищем дату в формате «день-месяц-год», мы можем захватить каждую часть отдельно:
// Создаём регулярное выражение с тремя группами захвата
// (d{2}) — захватываем ровно две цифры (день)
// (d{2}) — захватываем ровно две цифры (месяц)
// (d{4}) — захватываем ровно четыре цифры (год)
let re = Regex::new(r"(d{2})-(d{2})-(d{4})").unwrap();
// Текст, в котором будем искать
let text = "Сегодня 15-03-2024, а завтра будет 16-03-2024";
// Находим первое совпадение с захватом групп
if let Some(caps) = re.captures(text) {
// caps.get(0) — это всё найденное выражение целиком
// caps.get(1) — первая группа (день)
// caps.get(2) — вторая группа (месяц)
// caps.get(3) — третья группа (год)
println!("Найдена дата: {}", &caps[0]); // Выведет: 15-03-2024
println!("День: {}", &caps[1]); // Выведет: 15
println!("Месяц: {}", &caps[2]); // Выведет: 03
println!("Год: {}", &caps[3]); // Выведет: 2024
}
Примеры кода — от простого к сложному
Пример 1: Проверка, содержит ли строка цифры
// Самый простой пример: проверяем, есть ли в строке хотя бы одна цифра
use regex::Regex;
fn main() {
// Создаём регулярное выражение: d — любая цифра
// unwrap() — говорим Rust: "я уверен, что выражение корректно, дай мне его"
let re = Regex::new(r"d").unwrap();
let test_string = "Привет, мне 25 лет";
// is_match возвращает true или false
if re.is_match(test_string) {
println!("В строке есть цифры!");
} else {
println!("Цифр нет.");
}
// А в этой строке цифр нет
let test_string2 = "Привет, мир!";
println!("Есть цифры? {}", re.is_match(test_string2)); // Выведет: false
}
Пример 2: Поиск всех email-адресов в тексте
use regex::Regex;
fn main() {
// Создаём шаблон для поиска email-адресов
// [w.]+ — один или больше символов: буквы, цифры, подчёркивание или точка
// @ — символ собаки
// [w.]+ — домен (например, gmail.com)
// . — экранированная точка (просто точка, а не "любой символ")
// w+ — доменная зона (com, ru, org)
let email_re = Regex::new(r"[w.]+@[w.]+.w+").unwrap();
let text = "Мои контакты: ivan@example.com и petr@mail.ru.
Также можно писать на support@my-site.org";
// find_iter возвращает итератор по всем совпадениям
for email in email_re.find_iter(text) {
// email.as_str() — получаем текст совпадения
println!("Найден email: {}", email.as_str());
}
// Выведет:
// Найден email: ivan@example.com
// Найден email: petr@mail.ru
// Найден email: support@my-site.org
}
Пример 3: Извлечение чисел из строки
use regex::Regex;
fn main() {
// Ищем все числа: одна или больше цифр подряд
let re = Regex::new(r"d+").unwrap();
let text = "В 2024 году было 365 дней, 52 недели и 12 месяцев";
println!("Найденные числа:");
for number in re.find_iter(text) {
// parse::() — превращаем строку в число
// Это может не сработать, если число слишком большое
let num: i32 = number.as_str().parse().unwrap();
println!(" {}", num);
}
// Выведет:
// 2024
// 365
// 52
// 12
}
Пример 4: Проверка корректности номера телефона
use regex::Regex;
fn main() {
// Шаблон для российского номера телефона:
// ^ — начало строки
// +7 — символы +7
// d{3} — ровно 3 цифры (код оператора)
// d{3} — ещё 3 цифры
// d{2} — 2 цифры
// d{2} — последние 2 цифры
// $ — конец строки
let phone_re = Regex::new(r"^+d{11}$").unwrap();
let phones = vec![
"+79261234567", // правильный
"89261234567", // без плюса
"+7926123456", // короткий
"+7abc1234567", // буквы вместо цифр
];
for phone in phones {
if phone_re.is_match(phone) {
println!("{} — корректный номер", phone);
} else {
println!("{} — НЕ корректный номер", phone);
}
}
// Выведет:
// +79261234567 — корректный номер
// 89261234567 — НЕ корректный номер
// +7926123456 — НЕ корректный номер
// +7abc1234567 — НЕ корректный номер
}
Пример 5: Замена текста по шаблону
use regex::Regex;
fn main() {
// Заменяем все вхождения слова "плохой" на "хороший"
let re = Regex::new(r"плохой").unwrap();
let text = "Это был плохой день. Плохой фильм и плохая погода.";
// replace_all заменяет все совпадения
let result = re.replace_all(text, "хороший");
println!("Было: {}", text);
println!("Стало: {}", result);
// Выведет:
// Было: Это был плохой день. Плохой фильм и плохая погода.
// Стало: Это был хороший день. Плохой фильм и плохая погода.
// Обрати внимание: "Плохой" с большой буквы не заменился!
// Регулярные выражения по умолчанию чувствительны к регистру
}
Пример 6: Замена с учётом регистра и групп
use regex::RegexBuilder;
fn main() {
// RegexBuilder позволяет настроить флаги
// (?i) — флаг, отключающий чувствительность к регистру
let re = RegexBuilder::new(r"(?i)плохw+")
.build()
.unwrap();
let text = "Это был ПЛОХОЙ день. плохой фильм и Плохая погода.";
// Заменяем все варианты слова "плохой" на "отличный"
let result = re.replace_all(text, "отличный");
println!("Было: {}", text);
println!("Стало: {}", result);
// Выведет:
// Было: Это был ПЛОХОЙ день. плохой фильм и Плохая погода.
// Стало: Это был отличный день. отличный фильм и отличная погода.
}
Пример 7: Разбор сложной строки с группами захвата
use regex::Regex;
fn main() {
// Разбираем строку с информацией о человеке
// Формат: "Имя: Иван, Возраст: 25, Город: Москва"
let re = Regex::new(
r"Имя:s*(w+),s*Возраст:s*(d+),s*Город:s*(w+)"
).unwrap();
let text = "Имя: Иван, Возраст: 25, Город: Москва";
if let Some(caps) = re.captures(text) {
println!("Полная строка: {}", &caps[0]); // Всё совпадение
println!("Имя: {}", &caps[1]); // Первая группа
println!("Возраст: {}", &caps[2]); // Вторая группа
println!("Город: {}", &caps[3]); // Третья группа
// Можем преобразовать возраст в число
let age: i32 = caps[2].parse().unwrap();
println!("Через 10 лет будет: {}", age + 10);
}
// Выведет:
// Полная строка: Имя: Иван, Возраст: 25, Город: Москва
// Имя: Иван
// Возраст: 25
// Город: Москва
// Через 10 лет будет: 35
}
Пример 8: Поиск повторяющихся слов
use regex::Regex;
fn main() {
// Ищем повторяющиеся слова, идущие подряд
// (w+) — захватываем слово
// s+ — один или больше пробелов
// 1 — обратная ссылка на первую группу (то же самое слово)
let re = Regex::new(r"(w+)s+1").unwrap();
let text = "Это это очень странный текст с с повторяющимися словами словами";
println!("Найденные повторы:");
for match_found in re.find_iter(text) {
println!(" '{}'", match_found.as_str());
}
// Выведет:
// 'это это'
// 'с с'
// 'словами словами'
}
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаком с Python, тебе будет проще освоить регулярные выражения в Rust. Но есть важные отличия:
| Характеристика | Python | Rust |
|---|---|---|
| Подключение | Встроенный модуль re |
Внешний крейт regex |
| Сырые строки | r"..." — опционально |
r"..." — настоятельно рекомендуется |
| Обработка ошибок | Выбрасывает исключение | Возвращает Result |
| Производительность | Хорошая | Отличная (компилируется в машинный код) |
| Группы захвата | group(1) |
&caps[1] |
| Замена | re.sub(pattern, repl, text) |
re.replace(text, repl) |
| Флаги | Флаги как аргументы: re.IGNORECASE |
Флаги в шаблоне: (?i) или через RegexBuilder |
Вот как выглядит один и тот же пример на двух языках:
// Rust
use regex::Regex;
fn main() {
let re = Regex::new(r"d+").unwrap();
let text = "Возраст: 25 лет";
if let Some(m) = re.find(text) {
println!("Найдено: {}", m.as_str());
}
}
# Python
import re
text = "Возраст: 25 лет"
match = re.search(r"d+", text)
if match:
print(f"Найдено: {match.group()}")
Главное отличие: В Rust всё явное. Ты всегда видишь, где может возникнуть ошибка (unwrap), где может не быть совпадения (Option). В Python многое скрыто «под капотом». Rust даёт больше контроля, но требует больше кода.
Типичные ошибки и подводные камни
Ошибка 1: Забыл про экранирование специальных символов
Символы ., *, +, ?, (, ), [, ], {, }, ^, $, |, имеют специальное значение. Если нужно найти их как обычные символы — поставь перед ними обратный слеш.
// Неправильно: точка означает "любой символ"
let wrong = Regex::new(r"к.т").unwrap(); // Найдёт "кот", "кит", "к_т"
// Правильно: экранированная точка означает просто точку
let correct = Regex::new(r"к.т").unwrap(); // Найдёт только "к.т"
Ошибка 2: Жадность квантификаторов
По умолчанию квантификаторы * и + «жадные» — они захватывают максимально возможную строку. Чтобы сделать их «ленивыми», добавь ? после квантификатора.
let text = "<b>жирный</b> и <b>тоже жирный</b>";
// Жадный режим: захватит от первого < до последнего >
let greedy = Regex::new(r"<.+>").unwrap();
println!("Жадный: {}", greedy.find(text).unwrap().as_str());
// Выведет: <b>жирный</b> и <b>тоже жирный</b>
// Ленивый режим: захватит минимально возможное
let lazy = Regex::new(r"<.+?>").unwrap();
println!("Ленивый: {}", lazy.find(text).unwrap().as_str());
// Выведет: <b>
Ошибка 3: Использование unwrap без проверки
unwrap() — это как сказать Rust: «Я уверен, что здесь нет ошибки, дай мне значение». Если ошибка всё же есть — программа упадёт. Всегда проверяй регулярное выражение на валидность:
// Плохо: если выражение некорректно, программа упадёт
let re = Regex::new(r"[незакрытый класс").unwrap();
// Хорошо: обрабатываем ошибку
match Regex::new(r"[незакрытый класс") {
Ok(re) => println!("Всё хорошо, используем: {:?}", re),
Err(e) => println!("Ошибка в регулярном выражении: {}", e),
}
Ошибка 4: Забыл про флаг (?i) для регистронезависимости
По умолчанию регулярные выражения чувствительны к регистру. Если нужно искать без учёта регистра, используй флаг (?i) в начале шаблона или RegexBuilder.
Ошибка 5: Слишком сложные выражения
Иногда проще написать несколько простых проверок, чем одно гигантское регулярное выражение. Например, вместо того чтобы парсить email одной строкой, можно сначала проверить наличие @, потом проверить домен, потом длину.
Лучшие практики и советы профессионалов
1. Компилируй регулярные выражения один раз
Создание регулярного выражения — дорогая операция. Если ты используешь одно и то же выражение много раз (например, в цикле), создай его один раз и переиспользуй:
// Плохо: создаём новое выражение на каждой итерации
for text in texts {
let re = Regex::new(r"d+").unwrap();
if re.is_match(text) { ... }
}
// Хорошо: создаём один раз до цикла
let re = Regex::new(r"d+").unwrap();
for text in texts {
if re.is_match(text) { ... }
}
2. Используй lazy_static для глобальных выражений
Если регулярное выражение используется во многих местах программы, создай его как глобальную константу с помощью крейта lazy_static или once_cell:
// Добавь в Cargo.toml: once_cell = "1"
use once_cell::sync::Lazy;
use regex::Regex;
// Глобальное регулярное выражение, которое компилируется один раз
static EMAIL_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"[w.]+@[w.]+.w+").unwrap()
});
fn main() {
let text = "test@example.com";
// Используем как обычную переменную
if EMAIL_RE.is_match(text) {
println!("Это email!");
}
}
3. Тестируй регулярные выражения отдельно
Регулярные выражения сложно отлаживать. Используй онлайн-тестеры (например, regex101.com) для проверки шаблона перед тем, как вставлять его в код. В Rust также можно написать тесты:
#[cfg(test)]
mod tests {
use super::*;
use regex::Regex;
#[test]
fn test_email_pattern() {
let re = Regex::new(r"[w.]+@[w.]+.w+").unwrap();
assert!(re.is_match("user@example.com"));
assert!(re.is_match("test@mail.ru"));
assert!(!re.is_match("not_an_email"));
assert!(!re.is_match("@example.com"));
}
}
4. Избегай катастрофического возврата (catastrophic backtracking)
Некоторые шаблоны могут работать очень медленно на определённых входных данных. Например, (a+)+b на строке из множества символов «a» без «b» в конце заставит движок перебирать миллионы комбинаций. Если замечаешь, что программа «зависает» на больших текстах — проверь регулярные выражения.
5. Документируй сложные выражения
Регулярные выражения — это «write-only» код: написать легко, прочитать — сложно. Всегда комментируй, что делает каждое выражение:
// Шаблон для поиска дат в формате ДД.ММ.ГГГГ или ДД-ММ-ГГГГ
// (d{2}) — день (2 цифры)
// [.-/] — разделитель (точка, дефис или слеш)
// (d{2}) — месяц (2 цифры)
// [.-/] — разделитель
// (d{4}) — год (4 цифры)
let date_re = Regex::new(r"(d{2})[.-/](d{2})[.-/](d{4})").unwrap();
Вопросы для самопроверки
-
Что такое регулярное выражение и для чего оно используется?
Ответ: Это шаблон для поиска и обработки текста. Позволяет находить строки по сложным условиям, проверять формат данных, заменять части текста.
-
Какой символ в регулярных выражениях означает «любая цифра»?
Ответ:
d. Например,dd