Регулярные выражения в C++: подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представьте, что вы — библиотекарь в огромной библиотеке, где хранятся миллионы книг. К вам приходит читатель и говорит: «Мне нужны все книги, в названии которых есть слово "приключения", но только те, что написаны после 2000 года, и чтобы на обложке был красный цвет». Если бы вы просматривали каждую книгу вручную, это заняло бы недели. Но если бы у вас была волшебная лупа, которая мгновенно находит книги по таким сложным описаниям — это и есть регулярные выражения.
В мире программирования регулярные выражения (или regex от английского regular expressions) — это мощнейший инструмент для поиска и обработки текста. Они позволяют задать не просто конкретное слово, а шаблон (pattern), которому может соответствовать множество разных строк.
Простая аналогия: Если обычный поиск — это как сказать «найди мне слово "кот"», то регулярное выражение — это «найди мне любое слово из трёх букв, которое начинается на "к" и заканчивается на "т"». Второй вариант найдёт не только "кот", но и "кит", "кут" (если такое слово есть) и любые другие комбинации.
В C++ регулярные выражения появились в стандарте C++11 и с тех пор стали незаменимым инструментом для:
- Валидации данных — проверка, правильно ли введён email, номер телефона или почтовый индекс
- Поиска и замены — найти все даты в тексте и изменить их формат
- Разбора логов — извлечь нужную информацию из тысяч строк системных записей
- Лексического анализа — разбить программный код на составные части
Теория: как это работает изнутри
Чтобы понять, как работают регулярные выражения, нужно представить себе конечный автомат. Не пугайтесь этого термина — на самом деле всё просто.
Представьте, что у вас есть карта с городами и дорогами между ними. Вы начинаете путь в городе «Старт» и двигаетесь по дорогам, читая буквы текста. Каждая буква — это указатель, по какой дороге идти дальше. Если после прочтения всей строки вы оказались в городе «Финиш» — значит, строка подходит под шаблон. Если вы заблудились или не дошли до финиша — строка не соответствует.
Регулярное выражение компилируется в такой автомат внутри библиотеки C++. Когда вы передаёте строку для проверки, движок regex проходит по ней символ за символом, пытаясь проложить путь от начала до конца в соответствии с вашим шаблоном.
Основные строительные блоки регулярных выражений:
| Символ | Значение | Пример | Пояснение |
|---|---|---|---|
. |
Любой одиночный символ | к.т |
Найдёт "кот", "кит", "кут" |
* |
0 или более повторений | ко*т |
Найдёт "кт", "кот", "коот", "кооот" |
+ |
1 или более повторений | ко+т |
Найдёт "кот", "коот", но не "кт" |
? |
0 или 1 повторение | ко?т |
Найдёт "кт" и "кот" |
[] |
Набор символов | [км]от |
Найдёт "кот" и "мот" |
[^] |
Отрицание набора | [^к]от |
Найдёт "рот", "бот", но не "кот" |
^ |
Начало строки | ^кот |
Найдёт "кот" только в начале строки |
$ |
Конец строки | кот$ |
Найдёт "кот" только в конце строки |
| |
Или (альтернатива) | кот|пёс |
Найдёт либо "кот", либо "пёс" |
() |
Группировка | (ко)+т |
Найдёт "кот", "кокот", "кококот" |
d |
Цифра (0-9) | ddd |
Найдёт любые три цифры подряд |
w |
Буква, цифра или подчёркивание | w+ |
Найдёт целое слово |
s |
Пробельный символ | s+ |
Найдёт один или несколько пробелов |
Важно: В C++ строки с регулярными выражениями нужно писать с двойным экранированием. Например,dв регулярном выражении превращается в"\d"в коде C++. Это связано с тем, что обратная косая черта имеет особое значение в строках C++.
Синтаксис в C++: все формы и варианты
В C++ работа с регулярными выражениями сосредоточена в заголовочном файле <regex>. Основные классы и функции:
| Класс/Функция | Назначение | Пример использования |
|---|---|---|
std::regex |
Представляет само регулярное выражение | std::regex pattern("\d+"); |
std::regex_match() |
Проверяет, соответствует ли вся строка шаблону | regex_match("123", pattern) |
std::regex_search() |
Ищет первое совпадение в строке | regex_search("abc123def", pattern) |
std::regex_replace() |
Заменяет совпадения на новый текст | regex_replace("abc123", pattern, "X") |
std::smatch |
Хранит результаты поиска (для строк) | std::smatch matches; |
std::sregex_iterator |
Итератор для перебора всех совпадений | std::sregex_iterator it(...) |
Флаги компиляции регулярных выражений:
При создании объекта std::regex можно указать дополнительные флаги, которые меняют поведение поиска:
| Флаг | Описание |
|---|---|
std::regex::icase |
Игнорировать регистр букв |
std::regex::ECMAScript |
Использовать синтаксис ECMAScript (по умолчанию) |
std::regex::basic |
Базовый синтаксис POSIX |
std::regex::extended |
Расширенный синтаксис POSIX |
std::regex::awk |
Синтаксис утилиты awk |
std::regex::grep |
Синтаксис утилиты grep |
std::regex::egrep |
Расширенный синтаксис grep |
Совет: Для большинства задач используйте синтаксис ECMAScript (он используется по умолчанию). Он наиболее близок к тому, что вы видели в JavaScript или Python.
Примеры кода — от простого к сложному
Пример 1: Проверка, является ли строка числом
#include <iostream>
#include <regex>
#include <string>
int main() {
// Создаём регулярное выражение для поиска целого числа
// \d+ означает "одна или более цифр"
std::regex numberPattern("\d+");
std::string test1 = "12345";
std::string test2 = "abc123";
std::string test3 = "12.34";
// regex_match проверяет, соответствует ли ВСЯ строка шаблону
std::cout << "Тест 1 (12345): "
<< (std::regex_match(test1, numberPattern) ? "Да" : "Нет")
<< std::endl;
std::cout << "Тест 2 (abc123): "
<< (std::regex_match(test2, numberPattern) ? "Да" : "Нет")
<< std::endl;
std::cout << "Тест 3 (12.34): "
<< (std::regex_match(test3, numberPattern) ? "Да" : "Нет")
<< std::endl;
return 0;
}
Объяснение: Мы создали шаблон \d+, который ищет последовательность из одной или нескольких цифр. Функция regex_match проверяет, полностью ли строка соответствует этому шаблону. Строка "12345" подходит, а "abc123" — нет, потому что в ней есть буквы.
Пример 2: Поиск email в тексте
#include <iostream>
#include <regex>
#include <string>
int main() {
// Шаблон для поиска email-адреса
// \w+ — одно или более буквенно-цифровых символов
// @ — символ собаки
// [a-zA-Z]+\. — домен первого уровня (например, gmail.)
// [a-zA-Z]{2,} — домен второго уровня (например, com, ru)
std::regex emailPattern("\w+@[a-zA-Z]+\.\w{2,}");
std::string text = "Мои контакты: ivan@mail.com и petr@yandex.ru";
// Создаём объект для хранения результатов поиска
std::smatch matches;
// Ищем первое совпадение
if (std::regex_search(text, matches, emailPattern)) {
std::cout << "Найден email: " << matches[0] << std::endl;
std::cout << "Позиция в тексте: " << matches.position() << std::endl;
}
// Найдём все email-адреса с помощью итератора
std::cout << "
Все найденные email-адреса:" << std::endl;
auto words_begin = std::sregex_iterator(text.begin(), text.end(), emailPattern);
auto words_end = std::sregex_iterator();
for (std::sregex_iterator i = words_begin; i != words_end; ++i) {
std::smatch match = *i;
std::cout << match.str() << std::endl;
}
return 0;
}
Объяснение: Этот пример показывает два способа поиска. regex_search находит первое совпадение, а sregex_iterator позволяет перебрать все совпадения в тексте. Обратите внимание на экранирование точки — \. означает именно символ точки, а не любой символ.
Пример 3: Замена всех дат в тексте
#include <iostream>
#include <regex>
#include <string>
int main() {
// Исходный текст с датами в формате ДД.ММ.ГГГГ
std::string text = "Сегодня 15.03.2024. Встреча назначена на 20.04.2024. "
"Дедлайн: 01.05.2024.";
std::cout << "Исходный текст: " << text << std::endl;
// Шаблон для поиска дат: две цифры, точка, две цифры, точка, четыре цифры
std::regex datePattern("(\d{2})\.(\d{2})\.(\d{4})");
// Заменяем на формат ГГГГ-ММ-ДД
// $1, $2, $3 — ссылки на группы захвата
std::string newText = std::regex_replace(text, datePattern, "$3-$2-$1");
std::cout << "После замены: " << newText << std::endl;
return 0;
}
Объяснение: Здесь мы используем группы захвата (). Каждая группа запоминает свою часть совпадения. В строке замены $1, $2, $3 ссылаются на эти группы. Мы меняем порядок: год становится первым, месяц — вторым, день — третьим.
Пример 4: Проверка сложности пароля
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string password;
std::cout << "Введите пароль: ";
std::getline(std::cin, password);
// Проверяем различные требования к паролю
// Хотя бы одна заглавная буква
std::regex hasUpper("[A-Z]");
// Хотя бы одна строчная буква
std::regex hasLower("[a-z]");
// Хотя бы одна цифра
std::regex hasDigit("\d");
// Хотя бы один спецсимвол
std::regex hasSpecial("[!@#$%^&*()]");
// Длина не менее 8 символов
std::regex hasMinLength(".{8,}");
int score = 0;
if (std::regex_search(password, hasUpper)) score++;
if (std::regex_search(password, hasLower)) score++;
if (std::regex_search(password, hasDigit)) score++;
if (std::regex_search(password, hasSpecial)) score++;
if (std::regex_search(password, hasMinLength)) score++;
std::cout << "Сложность пароля: " << score << "/5" << std::endl;
if (score < 3) {
std::cout << "Пароль слишком слабый!" << std::endl;
} else if (score < 5) {
std::cout << "Пароль средний, можно улучшить." << std::endl;
} else {
std::cout << "Отличный пароль!" << std::endl;
}
return 0;
}
Объяснение: Вместо одного сложного регулярного выражения мы используем несколько простых. Каждое проверяет одно конкретное требование. Это делает код более читаемым и понятным.
Пример 5: Извлечение чисел из текста
#include <iostream>
#include <regex>
#include <string>
#include <vector>
int main() {
std::string text = "В магазине 5 яблок, 12 груш и 3 апельсина. "
"Цена: 99.99 рублей за кг.";
// Шаблон для поиска чисел (целых и с плавающей точкой)
// \d+ — одна или более цифр
// (?:\.\d+)? — необязательная дробная часть
std::regex numberPattern("\d+(?:\.\d+)?");
std::vector<std::string> numbers;
// Собираем все числа в вектор
auto begin = std::sregex_iterator(text.begin(), text.end(), numberPattern);
auto end = std::sregex_iterator();
for (auto it = begin; it != end; ++it) {
numbers.push_back(it->str());
}
std::cout << "Найденные числа:" << std::endl;
for (const auto& num : numbers) {
std::cout << num << std::endl;
}
// Преобразуем строки в числа и суммируем
double sum = 0.0;
for (const auto& num : numbers) {
sum += std::stod(num);
}
std::cout << "Сумма всех чисел: " << sum << std::endl;
return 0;
}
Объяснение: Шаблон \d+(?:\.\d+)? использует не захватывающую группу (?:...) для дробной части. Знак вопроса делает эту группу необязательной. Таким образом, мы находим и целые числа (5, 12, 3), и числа с плавающей точкой (99.99).
Пример 6: Разбор URL на составные части
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string url = "https://www.example.com:8080/path/to/page?name=value&key=val#section";
// Сложное регулярное выражение для разбора URL
// Протокол: http:// или https://
// Домен: буквы, цифры, точки, дефисы
// Порт: необязательный, :цифры
// Путь: необязательный, /что-то
// Параметры: необязательные, ?ключ=значение
// Якорь: необязательный, #что-то
std::regex urlPattern(
"(https?://)?" // Протокол (группа 1)
"([\w.-]+)" // Домен (группа 2)
"(?::(\d+))?" // Порт (группа 3)
"(/[\w/]*)?" // Путь (группа 4)
"(?:\?([\w=&]+))?" // Параметры (группа 5)
"(?:#(\w+))?" // Якорь (группа 6)
);
std::smatch matches;
if (std::regex_match(url, matches, urlPattern)) {
std::cout << "Протокол: " << matches[1] << std::endl;
std::cout << "Домен: " << matches[2] << std::endl;
std::cout << "Порт: " << matches[3] << std::endl;
std::cout << "Путь: " << matches[4] << std::endl;
std::cout << "Параметры: " << matches[5] << std::endl;
std::cout << "Якорь: " << matches[6] << std::endl;
}
return 0;
}
Объяснение: Этот пример показывает, как разбить сложную строку на логические части с помощью групп захвата. Каждая часть URL помещается в свою группу, и мы можем обратиться к ней по индексу. Необязательные части помечены знаком вопроса.
Пример 7: Валидация телефонного номера
#include <iostream>
#include <regex>
#include <string>
int main() {
// Различные форматы телефонных номеров
std::vector<std::string> phones = {
"+7 (123) 456-78-90",
"8-123-456-78-90",
"1234567890",
"+1 234 567 8900",
"abc-def-ghij" // Неправильный номер
};
// Шаблон для российских номеров
// Варианты: +7, 8, или просто 10 цифр
// Разделители: пробелы, дефисы, скобки — необязательны
std::regex phonePattern(
"^(?:\+7|8)?" // Код страны: +7 или 8 (необязательно)
"[-\s]?" // Необязательный разделитель
"\(?(\d{3})\)?" // Код города: 3 цифры, могут быть в скобках
"[-\s]?" // Разделитель
"(\d{3})" // Первые 3 цифры номера
"[-\s]?" // Разделитель
"(\d{2})" // Следующие 2 цифры
"[-\s]?" // Разделитель
"(\d{2})$" // Последние 2 цифры
);
for (const auto& phone : phones) {
std::smatch matches;
bool isValid = std::regex_match(phone, matches, phonePattern);
std::cout << phone << " — "
<< (isValid ? "корректный" : "некорректный");
if (isValid) {
std::cout << " (код: " << matches[1]
<< ", номер: " << matches[2]
<< matches[3] << matches[4] << ")";
}
std::cout << std::endl;
}
return 0;
}
Объяснение: Этот пример демонстрирует, как обрабатывать разные форматы одних и тех же данных. Шаблон учитывает различные варианты записи номера: с +7, с 8, с пробелами, дефисами и скобками. Группы захвата позволяют извлечь код города и сам номер отдельно.
Сравнение с Python: чем похоже и чем отличается
Если вы уже знакомы с регулярными выражениями в Python, вы заметите много общего, но есть и важные различия:
| Аспект | C++ | Python |
|---|---|---|
| Подключение | #include <regex> |
import re |
| Создание шаблона | std::regex pattern("\d+"); |
pattern = re.compile(r"d+") |
| Экранирование | Двойное: \d |
Сырые строки: r"d" |
| Поиск совпадения | std::regex_match() |
re.fullmatch() |
| Поиск в строке | std::regex_search() |
re.search() |
| Все совпадения | std::sregex_iterator |
re.findall() |
| Замена | std::regex_replace() |
re.sub() |
| Игнорирование регистра | std::regex::icase |
re.IGNORECASE |
| Производительность | Выше (компилируемый язык) | Ниже (интерпретируемый) |
| Удобство использования | Многословнее, сложнее | Проще, лаконичнее |
Главное отличие: В C++ нет «сырых» строк (raw strings), поэтому все обратные слеши нужно удваивать. Это делает регулярные выражения в C++ более громоздкими. Однако C++ выигрывает в производительности, что критично для обработки больших объёмов данных.
Типичные ошибки и подводные камни
1. Забыли про двойное экранирование
Самая частая ошибка новичков — написать "d" вместо "\d". В C++ обратная косая черта в строке — это escape-символ. "d" будет интерпретироваться как символ 'd' с каким-то специальным значением (которого нет), а не как «цифра».
2. Путаница между regex_match и regex_search
regex_match проверяет, соответствует ли вся строка шаблону от начала до конца. regex_search ищет подстроку, соответствующую шаблону. Если вам нужно найти слово в тексте — используйте regex_search.
3. Жадность квантификаторов
По умолчанию * и + являются «жадными» — они захватывают максимально возможную строку. Например, для текста "<div>текст</div>" шаблон <.*> найдёт всю строку целиком, а не отдельные теги. Чтобы сделать квантификатор «ленивым», добавьте после него знак вопроса: <.*?>.
4. Проблемы с кодировкой
Стандартная библиотека <regex> в C++ плохо работает с Unicode и кириллицей. Если вам нужно обрабатывать русские буквы, используйте std::wstring и std::wregex.
5. Исключения при компиляции
Если ваше регулярное выражение содержит синтаксическую ошибку, при создании объекта std::regex будет выброшено исключение std::regex_error. Всегда оборачивайте создание regex в try-catch блок.
try {
std::regex pattern("[неправильный шаблон");
} catch (const std::regex_error& e) {
std::cerr << "Ошибка в регулярном выражении: " << e.what() << std::endl;
}
6. Производительность
Создание объекта std::regex — дорогая операция, так как шаблон компилируется в конечный автомат. Создавайте regex один раз и переиспользуйте его, а не создавайте заново в цикле.
Лучшие практики и советы профессионалов
1. Начинайте с простого
Не пытайтесь написать идеальное регулярное выражение с первого раза. Начните с простого шаблона и постепенно усложняйте его. Используйте онлайн-тестеры (например, regex101.com) для отладки.
2. Комментируйте сложные выражения
Регулярные выражения могут быть очень сложными для понимания. Разбивайте их на части и комментируйте каждую часть, как в примере с URL.
3. Используйте именованные классы символов
Вместо [0-9] используйте \d, вместо [a-zA-Z0-9_] — \w. Это делает выражения короче и понятнее.
4. Предпочитайте конкретные символы общим
Если вы ищете конкретный символ (например, точку), используйте \. вместо .. Точка в регулярных выражениях означает «любой символ», что может привести к неожиданным совпадениям.
5. Проверяйте граничные случаи
Всегда тестируйте свои регулярные выражения на пустых строках, строках с пробелами, строках со спецсимволами. То, что работает для «нормальных» данных, может сломаться на граничных случаях.
6. Не злоупотребляйте регулярными выражениями
Не всё нужно решать с помощью regex. Для простого поиска подстроки используйте