regex для Rust: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Библиотека regex для Rust: полное руководство

1. Что это и зачем нужна

Библиотека regex — это стандартный и самый популярный инструмент для работы с регулярными выражениями в языке Rust. Она предоставляет мощный, безопасный и высокопроизводительный движок для поиска, извлечения и замены текста по шаблонам. В отличие от многих других реализаций, regex в Rust спроектирована с акцентом на безопасность памяти (memory safety) и отсутствие неопределённого поведения (undefined behavior), что критически важно для системного программирования.

Зачем она нужна? Регулярные выражения — это универсальный язык для описания текстовых шаблонов. С их помощью можно решать огромный спектр задач: валидация email-адресов и URL, парсинг логов, извлечение данных из HTML/JSON, поиск и замена в коде, разбор пользовательского ввода и многое другое. Библиотека regex делает это быстро и надёжно. Она использует алгоритм на основе автоматов (Thompson NFA), который гарантирует линейное время выполнения относительно длины входной строки, что защищает от "катастрофического отката" (catastrophic backtracking), свойственного некоторым другим движкам (например, PCRE или Python's re).

Ключевые особенности библиотеки: полная поддержка синтаксиса регулярных выражений (якоря, квантификаторы, группы, lookahead/lookbehind, Unicode), работа с произвольными байтовыми последовательностями, возможность компиляции выражений один раз и многократного использования, а также поддержка итераторов для поиска всех совпадений. Библиотека является частью экосистемы Rust и активно поддерживается сообществом.

2. Установка

Для установки библиотеки regex используется менеджер пакетов cargo, входящий в состав Rust. Вам нужно добавить зависимость в файл Cargo.toml вашего проекта.

Точная команда для добавления через терминал:

cargo add regex

Или вручную добавьте в раздел [dependencies] файла Cargo.toml:

[dependencies]
regex = "1"

После этого выполните cargo build, чтобы загрузить и скомпилировать библиотеку. Версия "1" означает, что будет использована последняя стабильная версия из серии 1.x.

3. Быстрый старт — минимальный рабочий пример

Давайте напишем простейшую программу, которая проверяет, содержит ли строка слово "hello" (без учёта регистра).

use regex::Regex;

fn main() {
    // Компилируем регулярное выражение
    // (?i) — флаг, отключающий чувствительность к регистру
    let re = Regex::new(r"(?i)hello").unwrap();

    // Строка для поиска
    let text = "Hello, world!";

    // Проверяем, есть ли совпадение
    if re.is_match(text) {
        println!("Найдено совпадение!");
    } else {
        println!("Совпадений нет.");
    }
}

Этот код выведет: Найдено совпадение!. Обратите внимание на .unwrap() — он используется здесь для простоты, но в реальном коде стоит обрабатывать ошибки компиляции регулярного выражения (например, если шаблон некорректен).

4. Основные методы

Ниже перечислены ключевые методы и функции библиотеки regex. Все примеры предполагают, что у вас есть импорт use regex::Regex;.

4.1. Regex::new(pattern: &str) -> Result<Regex, Error>

Что делает: Компилирует строку pattern в регулярное выражение. Возвращает Result, который нужно обработать на случай ошибки в синтаксисе.

let re = Regex::new(r"^\d{3}-\d{2}-\d{4}$").expect("Неверный regex");
// re теперь готов к использованию

4.2. is_match(&self, text: &str) -> bool

Что делает: Проверяет, есть ли хотя бы одно совпадение шаблона в строке text. Возвращает true или false.

let re = Regex::new(r"\d+").unwrap();
assert!(re.is_match("abc123def"));
assert!(!re.is_match("abcdef"));

4.3. find(&self, text: &str) -> Option<Match>

Что делает: Находит первое совпадение шаблона в строке. Возвращает Option<Match>, где Match содержит начальную и конечную позиции совпадения.

let re = Regex::new(r"\b\w{5}\b").unwrap(); // слова из 5 букв
let text = "Hello, world!";
if let Some(mat) = re.find(text) {
    println!("Найдено: '{}' на позиции {}..{}", mat.as_str(), mat.start(), mat.end());
}
// Выведет: Найдено: 'Hello' на позиции 0..5

4.4. find_iter(&self, text: &str) -> Matches

Что делает: Возвращает итератор по всем непересекающимся совпадениям шаблона в строке. Каждый элемент итератора — это Match.

let re = Regex::new(r"\d+").unwrap();
let text = "Цена: 100 рублей, скидка: 20%.";
for mat in re.find_iter(text) {
    println!("Число: {}", mat.as_str());
}
// Выведет:
// Число: 100
// Число: 20

4.5. captures(&self, text: &str) -> Option<Captures>

Что делает: Выполняет поиск и возвращает группы захвата (capture groups) для первого совпадения. Группы задаются круглыми скобками в шаблоне. Возвращает Option<Captures>.

let re = Regex::new(r"(\w+)@(\w+)\.(\w+)").unwrap();
let text = "email: user@example.com";
if let Some(caps) = re.captures(text) {
    println!("Полный email: {}", caps.get(0).map(|m| m.as_str()).unwrap_or(""));
    println!("Имя пользователя: {}", caps.get(1).map(|m| m.as_str()).unwrap_or(""));
    println!("Домен: {}", caps.get(2).map(|m| m.as_str()).unwrap_or(""));
    println!("TLD: {}", caps.get(3).map(|m| m.as_str()).unwrap_or(""));
}
// Выведет:
// Полный email: user@example.com
// Имя пользователя: user
// Домен: example
// TLD: com

4.6. captures_iter(&self, text: &str) -> CaptureMatches

Что делает: Возвращает итератор по всем непересекающимся совпадениям, каждое из которых представлено объектом Captures (все группы захвата).

let re = Regex::new(r"(\w+): (\d+)").unwrap();
let text = "apple: 5, banana: 10, cherry: 15";
for caps in re.captures_iter(text) {
    let name = caps.get(1).unwrap().as_str();
    let value = caps.get(2).unwrap().as_str();
    println!("{} = {}", name, value);
}
// Выведет:
// apple = 5
// banana = 10
// cherry = 15

4.7. replace(&self, text: &str, rep: &str) -> String

Что делает: Заменяет первое совпадение шаблона на строку rep. В строке замены можно использовать ссылки на группы захвата: $1, $2 и т.д., а также $name для именованных групп.

let re = Regex::new(r"(\w+)@(\w+)\.(\w+)").unwrap();
let text = "email: user@example.com";
let result = re.replace(text, "$1 [at] $2 [dot] $3");
println!("{}", result);
// Выведет: email: user [at] example [dot] com

4.8. replace_all(&self, text: &str, rep: &str) -> String

Что делает: Заменяет все непересекающиеся совпадения шаблона на строку rep. Работает аналогично replace, но для всех совпадений.

let re = Regex::new(r"\d+").unwrap();
let text = "Числа: 1, 2, 3, 4, 5";
let result = re.replace_all(text, "NUM");
println!("{}", result);
// Выведет: Числа: NUM, NUM, NUM, NUM, NUM

4.9. shortest_match(&self, text: &str) -> Option<usize>

Что делает: Находит длину самого короткого совпадения, начинающегося с начала строки. Полезно для оптимизации, когда нужно только проверить, что строка начинается с определённого шаблона, и узнать его длину.

let re = Regex::new(r"a+").unwrap();
let text = "aaab";
if let Some(len) = re.shortest_match(text) {
    println!("Самое короткое совпадение начинается с начала и имеет длину {}", len);
}
// Выведет: Самое короткое совпадение начинается с начала и имеет длину 3

4.10. split(&self, text: &str) -> Split

Что делает: Разбивает строку text на части по совпадениям с шаблоном. Возвращает итератор по строкам, которые находятся между совпадениями.

let re = Regex::new(r"[,\s]+").unwrap(); // разделители: запятая или пробелы
let text = "apple, banana, cherry";
for part in re.split(text) {
    println!("{}", part);
}
// Выведет:
// apple
// banana
// cherry

5. Реальный практический пример

Давайте напишем утилиту для анализа логов веб-сервера (формат Common Log Format). Программа будет читать файл лога, извлекать IP-адреса, URL, коды ответов и собирать статистику: топ-10 IP-адресов по количеству запросов, количество запросов с кодом 404 и средний размер ответа.

use regex::Regex;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() -> Result<(), Box> {
    // Регулярное выражение для Common Log Format:
    // IP - ident - user [date] "METHOD URL PROTO" status size
    let log_pattern = Regex::new(
        r#"^(\S+) \S+ \S+ \[[^\]]+\] "([A-Z]+) (\S+) \S+" (\d{3}) (\d+|-)$"#
    ).unwrap();

    // Открываем файл лога
    let file = File::open("access.log")?;
    let reader = BufReader::new(file);

    // Структуры для статистики
    let mut ip_counts: HashMap = HashMap::new();
    let mut not_found_count: u64 = 0;
    let mut total_size: u64 = 0;
    let mut response_count: u64 = 0;

    // Обрабатываем каждую строку
    for line in reader.lines() {
        let line = line?;
        if let Some(caps) = log_pattern.captures(&line) {
            // Извлекаем данные
            let ip = caps.get(1).unwrap().as_str().to_string();
            let method = caps.get(2).unwrap().as_str();
            let url = caps.get(3).unwrap().as_str();
            let status_code = caps.get(4).unwrap().as_str();
            let size_str = caps.get(5).unwrap().as_str();

            // Считаем IP
            *ip_counts.entry(ip).or_insert(0) += 1;

            // Считаем 404
            if status_code == "404" {
                not_found_count += 1;
            }

            // Считаем общий размер (игнорируем '-')
            if size_str != "-" {
                let size: u64 = size_str.parse().unwrap_or(0);
                total_size += size;
                response_count += 1;
            }

            // Пример: вывод каждого запроса с кодом 500
            if status_code == "500" {
                println!("[ERROR] {} {} {} -> 500", ip, method, url);
            }
        }
    }

    // Вывод статистики
    println!("=== Статистика ===");
    println!("Всего уникальных IP: {}", ip_counts.len());
    println!("Всего запросов с кодом 404: {}", not_found_count);
    if response_count > 0 {
        println!("Средний размер ответа: {:.2} байт", total_size as f64 / response_count as f64);
    }

    // Топ-10 IP
    println!("\n=== Топ-10 IP ===");
    let mut ip_vec: Vec<(&String, &u64)> = ip_counts.iter().collect();
    ip_vec.sort_by(|a, b| b.1.cmp(a.1)); // сортировка по убыванию
    for (i, (ip, count)) in ip_vec.iter().take(10).enumerate() {
        println!("{}. {} -> {} запросов", i + 1, ip, count);
    }

    Ok(())
}

Этот пример демонстрирует: открытие файла, построчное чтение, использование captures для извлечения групп, сбор статистики в HashMap, сортировку и форматированный вывод. В реальном проекте стоит добавить обработку ошибок (например, если файл не найден) и, возможно, использовать clap для аргументов командной строки.

6. Советы по производительности и типичные ошибки

Советы по производительности

  • Компилируйте

Рекомендаций