🔷 C#

Регулярные выражения в C# (.NET): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы — библиотекарь в огромной библиотеке, где хранятся миллионы книг. К вам приходит читатель и говорит: «Найдите мне все книги, в названии которых есть слово "приключения", но только те, что написаны после 2000 года, и чтобы на обложке был дракон». Если бы вы искали каждую книгу вручную, это заняло бы недели. Но если бы у вас была волшебная лупа, которая мгновенно просматривает текст и находит всё, что соответствует вашему описанию — это и есть регулярное выражение.

В мире программирования регулярные выражения (или regex от английского «regular expressions») — это мощнейший инструмент для поиска, замены и проверки текста. Это не просто поиск по слову, как в Word или Блокноте. Это язык описания шаблонов текста. Вы говорите программе: «Найди мне все, что похоже на email», или «Проверь, что пароль содержит хотя бы одну цифру и букву», или «Замени все даты из американского формата (месяц/день/год) на европейский (день.месяц.год)».

В C# (.NET) регулярные выражения — это не просто встроенная функция, а целая мощная библиотека System.Text.RegularExpressions, которая позволяет делать с текстом буквально чудеса. Без regex вам пришлось бы писать десятки строк кода с циклами и проверками. С regex вы пишете одну строку — и задача решена.

Важно: Регулярные выражения — это как иностранный язык. Сначала он кажется страшным и непонятным (куча символов вроде d, ^, $, *), но после практики вы начнете «читать» их как обычный текст. Не пугайтесь — мы разберем всё по полочкам.

Теория: как это работает изнутри

Чтобы понять, как работают регулярные выражения, нужно заглянуть «под капот» движка regex в .NET. Когда вы передаете строку с шаблоном (например, "d{3}-d{2}-d{4}" для поиска номера СНИЛС или телефона), происходит следующее:

  1. Компиляция шаблона: Движок разбирает вашу строку-шаблон и превращает её в конечный автомат (finite automaton). Это абстрактная машина, которая умеет находиться в разных состояниях и переключаться между ними в зависимости от того, какой символ она видит во входном тексте.
  2. Проход по тексту: Движок начинает читать ваш текст символ за символом. Для каждого символа он проверяет, может ли текущее состояние автомата перейти в следующее. Если да — он двигается дальше. Если нет — он возвращается назад (это называется backtracking — возврат) и пробует другой путь.
  3. Поиск совпадений: Когда автомат доходит до конечного состояния (успешно «съел» весь шаблон), движок фиксирует совпадение: запоминает позицию в тексте, где оно началось и где закончилось.
  4. Оптимизация: .NET использует очень быстрый движок. Он умеет «запоминать» неудачные пути и не ходить по ним снова. Это называется кэшированием.

Ключевое понятие — жадность (greedy) и ленивость (lazy). По умолчанию квантификаторы (символы, которые указывают количество повторений, например * или +) работают жадно — они стараются захватить как можно больше текста. Если вы напишете .* (точка — любой символ, звездочка — любое количество), то движок попытается «съесть» весь текст до конца, а потом начнет отдавать символы назад, чтобы найти совпадение. Если добавить знак вопроса — .*? — квантификатор станет ленивым и будет захватывать минимум возможного.

Запомните: Движок regex в .NET — это NFA (Nondeterministic Finite Automaton — недетерминированный конечный автомат). Он очень мощный, поддерживает lookahead (просмотр вперед) и lookbehind (просмотр назад), но может быть медленным на плохо написанных шаблонах (эффект «котастрофического возврата» — об этом позже).

Синтаксис в C# (.NET): все формы и варианты

В .NET регулярные выражения живут в пространстве имен System.Text.RegularExpressions. Главный класс — Regex. Давайте разберем все основные элементы синтаксиса, которые поддерживаются именно в .NET (версия .NET 7+ поддерживает самые современные возможности).

Базовые символы (литералы)

Любой символ, который не является специальным, ищет сам себя. Например, шаблон "cat" найдет слово «cat» в тексте.

Специальные символы (метасимволы)

Символ Значение Пример
. Любой символ, кроме перевода строки ( ) c.t найдет "cat", "cot", "c3t"
d Любая цифра (0-9) dd найдет "42", "00"
D Любой НЕ цифра DD найдет "ab", "!@"
w Любая «буква»: буквы, цифры, подчеркивание w+ найдет "hello", "test_123"
W Любой НЕ «буква» W найдет пробел, точку, запятую
s Любой пробельный символ (пробел, табуляция, новая строка) s найдет пробел
S Любой НЕ пробельный символ S+ найдет слово
^ Начало строки ^Hello найдет "Hello" только в начале
$ Конец строки world$ найдет "world" только в конце
 Граница слова (начало или конец) cat найдет "cat", но не "catalog"
B НЕ граница слова BcatB найдет "cat" внутри "catalog"

Квантификаторы (сколько раз повторять)

Символ Значение Пример
* 0 или более раз (жадный) ab*c найдет "ac", "abc", "abbc"
+ 1 или более раз (жадный) ab+c найдет "abc", "abbc", но не "ac"
? 0 или 1 раз (жадный) colou?r найдет "color" и "colour"
{n} Ровно n раз d{3} найдет ровно 3 цифры
{n,} n или более раз d{2,} найдет 2 и более цифр
{n,m} От n до m раз d{2,4} найдет от 2 до 4 цифр
*? 0 или более (ленивый) a.*?b найдет "ab" в "aXbYb"
+? 1 или более (ленивый) a.+?b найдет "aXb" в "aXbYb"

Группы и захваты

Круглые скобки () делают две вещи: группируют часть шаблона и захватывают найденный текст в отдельную группу, к которой можно обратиться потом.

  • (abc) — группа с номером 1 (нумерация с 1, группа 0 — всё совпадение целиком)
  • (?<name>abc) — именованная группа с именем "name"
  • (?:abc) — незахватывающая группа (только группировка, без сохранения)

Альтернация (выбор)

Вертикальная черта | означает «или». cat|dog найдет либо "cat", либо "dog".

Экранирование

Если нужно найти сам специальный символ (например, точку или звездочку), поставьте перед ним обратную косую черту: ., *, +.

Lookahead и Lookbehind (просмотр вперед и назад)

Это «условия», которые проверяют, что перед или после совпадения есть (или нет) определенный текст, но сам этот текст не включается в результат.

  • (?=...) — положительный просмотр вперед (проверяет, что дальше есть ...)
  • (?!...) — отрицательный просмотр вперед (проверяет, что дальше НЕТ ...)
  • (?<=...) — положительный просмотр назад (проверяет, что перед этим есть ...)
  • (?<!...) — отрицательный просмотр назад (проверяет, что перед этим НЕТ ...)

Флаги (опции) в C#

При создании объекта Regex можно передать флаги из перечисления RegexOptions:

Флаг Что делает
IgnoreCase Игнорирует регистр букв
Multiline Меняет поведение ^ и $ — они работают для каждой строки, а не для всего текста
Singleline Точка (.) начинает совпадать с любым символом, включая
IgnorePatternWhitespace Игнорирует пробелы в шаблоне (можно писать комментарии с #)
Compiled Компилирует regex в IL-код для максимальной скорости (но дольше создается)
NonBacktracking (.NET 7+) Запрещает возврат (backtracking) — быстрее, но меньше возможностей

Примеры кода — от простого к сложному

Все примеры ниже — полностью рабочие. Вы можете скопировать их в консольное приложение C# (.NET 6+).

Пример 1: Простейший поиск — есть ли в тексте слово?

using System;
using System.Text.RegularExpressions;

string text = "Привет, мир! Как дела?";
string pattern = @"мир";  // @ — вербатимная строка, чтобы не экранировать слэши

// Создаем объект Regex
Regex regex = new Regex(pattern);

// Проверяем, есть ли совпадение
bool isMatch = regex.IsMatch(text);

Console.WriteLine($"Есть ли слово 'мир' в тексте? {isMatch}");  // Вывод: True
// IsMatch — самый быстрый метод, если нужно только узнать, есть ли совпадение

Пример 2: Поиск всех совпадений — найдем все цифры

using System;
using System.Text.RegularExpressions;

string text = "У меня 2 кота, 3 собаки и 10 рыбок.";
string pattern = @"d+";  // d — цифра, + — одна или более

Regex regex = new Regex(pattern);

// MatchCollection — коллекция всех совпадений
MatchCollection matches = regex.Matches(text);

Console.WriteLine("Найденные числа:");
foreach (Match match in matches)
{
    Console.WriteLine($"  '{match.Value}' на позиции {match.Index}");
}
// Вывод:
// Найденные числа:
//   '2' на позиции 6
//   '3' на позиции 16
//   '10' на позиции 26

Пример 3: Замена текста — цензурируем плохие слова

using System;
using System.Text.RegularExpressions;

string text = "Этот фильм — отстой! Полный отстой, я считаю.";
string pattern = @"отстой";  // Найдем слово "отстой"

// Заменяем на звездочки той же длины
string result = Regex.Replace(text, pattern, m => new string('*', m.Length));

Console.WriteLine("Результат замены:");
Console.WriteLine(result);
// Вывод: Этот фильм — ******! Полный ******, я считаю.
// Обратите внимание: мы использовали лямбду (m => ...), где m — это Match.
// Match.Length — длина найденного слова.

Пример 4: Проверка email (простая версия)

using System;
using System.Text.RegularExpressions;

string[] emails = { "user@example.com", "invalid-email", "test@test", "user@site.ru" };
// Простой шаблон: что-то @ что-то . что-то (2-4 буквы)
string pattern = @"^[w.-]+@[w.-]+.w{2,4}$";

foreach (string email in emails)
{
    bool isValid = Regex.IsMatch(email, pattern, RegexOptions.IgnoreCase);
    Console.WriteLine($"{email}: {(isValid ? "Корректный" : "Некорректный")}");
}
// Вывод:
// user@example.com: Корректный
// invalid-email: Некорректный
// test@test: Некорректный (нет точки и домена)
// user@site.ru: Корректный

Пример 5: Извлечение данных — парсинг HTML-тегов

using System;
using System.Text.RegularExpressions;

string html = "<div>Привет</div><p>Мир</p><span>!</span>";
// Найдем содержимое всех тегов: <...>содержимое</...>
// Используем именованную группу для захвата содержимого
string pattern = @"<(w+)>(?<content>.*?)</1>";
// 1 — обратная ссылка на первую группу (имя тега)
// (?<content>...) — именованная группа "content"
// .*? — ленивый квантификатор (захватываем минимум)

Regex regex = new Regex(pattern);
MatchCollection matches = regex.Matches(html);

Console.WriteLine("Содержимое тегов:");
foreach (Match match in matches)
{
    string tagName = match.Groups[1].Value;  // Группа 1 — имя тега
    string content = match.Groups["content"].Value;  // Именованная группа
    Console.WriteLine($"  <{tagName}>: '{content}'");
}
// Вывод:
// Содержимое тегов:
//   <div>: 'Привет'
//   <p>: 'Мир'
//   <span>: '!'

Пример 6: Сложный парсинг — даты из текста

using System;
using System.Text.RegularExpressions;

string text = "Встреча 15.03.2024, конференция 01/12/2023, день рождения 2024-05-20.";
// Найдем даты в трех форматах: ДД.ММ.ГГГГ, ММ/ДД/ГГГГ, ГГГГ-ММ-ДД
string pattern = @"(d{2}).(d{2}).(d{4})|(d{2})/(d{2})/(d{4})|(d{4})-(d{2})-(d{2})";

Regex regex = new Regex(pattern);
MatchCollection matches = regex.Matches(text);

Console.WriteLine("Найденные даты:");
foreach (Match match in matches)
{
    Console.WriteLine($"  '{match.Value}' (позиция {match.Index})");
    // Можно разобрать группы, чтобы узнать формат
    if (match.Groups[1].Success) // формат ДД.ММ.ГГГГ
        Console.WriteLine($"    День: {match.Groups[1].Value}, Месяц: {match.Groups[2].Value}, Год: {match.Groups[3].Value}");
    else if (match.Groups[4].Success) // формат ММ/ДД/ГГГГ
        Console.WriteLine($"    Месяц: {match.Groups[4].Value}, День: {match.Groups[5].Value}, Год: {match.Groups[6].Value}");
    else if (match.Groups[7].Success) // формат ГГГГ-ММ-ДД
        Console.WriteLine($"    Год: {match.Groups[7].Value}, Месяц: {match.Groups[8].Value}, День: {match.Groups[9].Value}");
}
// Вывод будет содержать все три даты с разбором по группам

Пример 7: Использование Lookahead — парольная проверка

using System;
using System.Text.RegularExpressions;

string[] passwords = { "weak", "Strong1", "Short1A", "GoodPass123!" };
// Требования: минимум 8 символов, хотя бы одна заглавная, одна строчная, одна цифра, один спецсимвол
string pattern = @"^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[!@#$%^&*()_+]).{8,}$";
// (?=.*[a-z]) — где-то есть строчная буква
// (?=.*[A-Z]) — где-то есть заглавная
// (?=.*d) — где-то есть цифра
// (?=.*[!@#$%...]) — где-то есть спецсимвол
// .{8,} — минимум 8 любых символов

foreach (string pwd in passwords)
{
    bool isValid = Regex.IsMatch(pwd, pattern);
    Console.WriteLine($"'{pwd}': {(isValid ? "Надежный" : "Слабый")}");
}
// Вывод:
// 'weak': Слабый
// 'Strong1': Слабый (нет спецсимвола)
// 'Short1A': Слабый (меньше 8 символов)
// 'GoodPass123!': Надежный

Пример 8: Разделение строки с сохранением разделителей

using System;
using System.Text.RegularExpressions;

string text = "яблоко;груша:вишня;апельсин:банан";
// Разделим по точке с запятой или двоеточию, но сохраним разделители
string pattern = @"[;:]";  // Набор символов: ; или :

// Split — разбивает строку
string[] parts = Regex.Split(text, pattern);

Console.WriteLine("Части строки:");
foreach (string part in parts)
{
    Console.WriteLine($"  '{part}'");
}
// Вывод:
// Части строки:
//   'яблоко'
//   'груша'
//   'вишня'
//   'апельсин'
//   'банан'

// Если нужно сохранить разделители, используем положительный lookahead/lookbehind
string patternWithDelim = @"(?<=[;:])|(?=[;:])";
string[] partsWithDelim = Regex.Split(text, patternWithDelim);
Console.WriteLine("С разделителями:");
foreach (string part in partsWithDelim)
{
    Console.WriteLine($"  '{part}'");
}
// Вывод:
//   'яблоко'
//   ';'
//   'груша'
//   ':'
//   'вишня'
//   ';'
//   'апельсин'
//   ':'
//   'банан'

Сравнение с Python: чем похоже и чем отличается

Если вы уже знакомы с Python или планируете изучать его, полезно знать различия. В Python тоже есть модуль re, но есть важные нюансы.

Характеристика C# (.NET) Python
Пространство имен / модуль System.Text.RegularExpressions import re
Основной класс Regex (объектный подход) + статические методы Функции: re.search(), re.findall(), re.sub()
Синтаксис шаблонов Почти идентичен Perl-совместимым (PCRE). Поддерживает A, , G Тоже PCRE, но есть свои особенности: A и  поддерживаются, G — нет
Lookbehind Поддерживает переменную длину (например, (?<=d+)) Только фиксированная длина (нельзя (?<=d+), только (?<=d{3}))
Именованные группы (?<name>...) (?P<name>...) (другое обозначение)
Обратные ссылки 1, k<name> 1, (?P=name)
Флаг IgnoreCase RegexOptions.IgnoreCase re.IGNORECASE
Компиляция RegexOptions.Compiled — компилирует в IL-код (очень быстро) re.compile() — просто парсит шаблон, но не компилирует в машинный код
Производительность Очень быстрый, особенно с Compiled. Есть NonBacktracking режим Медленнее на сложных шаблонах, нет аналога NonBacktracking
Поддержка Unicode Отличная: p{Lu} для заглавных букв любой культуры Хорошая, но синтаксис другой: p{Lu} тоже работает
Совет: Если вы переходите с Python на C#, запомните главное: в C# принято создавать объект Regex один раз и переиспользовать его. В Python вы чаще используете функции напрямую. Также в C# намного удобнее работать с именованными группами через match.Groups["name"].

Типичные ошибки и подводные камни

Даже опытные программисты иногда спотыкаются на regex. Вот самые частые проблемы в C#:

1. Забыли экранировать обратную косую черту

В C# строка "d" — это символ с кодом d? Нет! d в обычной строке — это escape-последовательность, которая означает символ с кодом 100 (буква d). Чтобы передать regex именно d, нужно писать "\d" или использовать вербатимную строку: @"d". Всегда используйте @ перед строкой с regex!

2. Квантификаторы жадные — захватывают слишком много

Шаблон <.*> в строке <div>текст</div> найдет всю строку целиком, а не отдельные теги. Потому что .* жадно съедает всё до последнего >. Решение: используйте ленивый квантификатор <.*?>.

3. Катастрофический возврат (Catastrophic Backtracking)

Это когда движок застревает в бесконечном переборе вариантов. Например, шаблон (a+)+b на строке из 20 букв "a" без "b" в конце заставит движок перебрать миллионы комбинаций. Признак: программа зависает или выполняется очень долго. Решение: используйте RegexOptions.NonBacktracking (доступно с .NET 7) или перепишите шаблон более конкретно.

4. Неправильное использование ^ и $

Без флага RegexOptions.Multiline символ ^ означает начало всего текста, а $ — конец всего текста. Если вы хотите проверять каждую строку отдельно, обязательно добавляйте Multiline.

5. Забыли про группы

Если вы используете () только для группировки, но не хотите захватывать текст, используйте (?:). Иначе вы создаете лишние группы, что замедляет работу и усложняет код.

6. Проблемы с Unicode и региональными символами

w в .NET по умолчанию включает буквы любой культуры (русские, арабские и т.д.). Если вам нужны только латинские буквы, используйте [a-zA-Z0-9_].

7. Слишком сложный шаблон для простой задачи

Не пытайтесь написать один regex, который парсит HTML или email «по стандарту». Для email есть библиотеки вроде System.Net.Mail.MailAddress, а для HTML — парсеры типа HtmlAgilityPack. Regex для таких задач становится нечитаемым и ненадежным.

Лучшие практики и советы профессионалов

  1. Компилируйте часто используемые regex. Если вы используете один и тот же шаблон много раз (например, в цикле), создавайте объект Regex с флагом RegexOptions.Compiled. Это ускорит работу в 5-10 раз.
  2. Используйте именованные группы. Вместо match.Groups[1] пишите match.Groups["username"]