Данные и базы данных

Регулярные выражения (Regex) в C#

Обновлено: · 7 мин чтения

Регулярные выражения в C# реализует класс Regex из пространства имён System.Text.RegularExpressions. Он проверяет строки на соответствие шаблону, извлекает фрагменты, заменяет и разбивает текст. Ниже разобраны методы IsMatch, Match, Matches, Replace и Split, группы, флаги RegexOptions, готовые шаблоны для email, телефона и чисел, а также производительность и генератор исходного кода.

Регулярные выражения (Regex) в C#
Содержание
  1. Основы: что такое регулярное выражение
  2. IsMatch: проверка соответствия
  3. Match и Matches: поиск совпадений
    1. Match: первое совпадение
    2. Matches: все совпадения
  4. Группы и именованные группы
  5. Replace: замена по шаблону
  6. Split: разбиение строки
  7. Флаги RegexOptions
  8. Готовые шаблоны: email, телефон, числа
  9. Производительность и защита от зависаний
  10. Source generator: GeneratedRegex
  11. Типичные ошибки
  12. Частые вопросы

Основы: что такое регулярное выражение

Регулярное выражение - это строка-шаблон, описывающая набор допустимых строк. Например, шаблон \d+ означает "одна или несколько цифр", а [A-Z]{2} - "ровно две заглавные латинские буквы". Движок .NET читает шаблон, сопоставляет его с текстом и возвращает результат: найдено или нет, какой именно фрагмент и где.

Основные элементы синтаксиса собраны в таблице. Их хватит для большинства повседневных задач.

ЭлементЗначениеПример
.Любой символ, кроме перевода строкиa.c найдёт abc, a1c
\d, \DЦифра, не цифра\d{3} - три цифры
\w, \WБуква, цифра или подчёркивание; всё остальное\w+ - слово
\s, \SПробельный символ, не пробельный\s+ - пробелы
^ и $Начало и конец строки^\d+$ - только цифры
[abc], [^abc]Один символ из набора, не из набора[а-яё] - русская строчная
*, +, ?0 и более, 1 и более, 0 или 1 разcolou?r
{n}, {n,m}Ровно n раз, от n до m раз\d{2,4}
( ) и |Группа и альтернатива(кот|пёс)
\bГраница слова\bкот\b

В C# шаблоны принято писать в буквальных строках с префиксом @ или в необработанных строках с тремя кавычками. Тогда обратную косую черту не нужно удваивать: @"\d+" вместо "\\d+".

IsMatch: проверка соответствия

Метод IsMatch возвращает true, если в строке есть хотя бы одно совпадение с шаблоном. Это самый простой и быстрый способ валидации. Обратите внимание: без якорей ^ и $ метод ищет шаблон где угодно внутри текста, и строка "abc123xyz" пройдёт проверку \d+. Если нужно сравнить всю строку целиком, добавляйте якоря.

using System.Text.RegularExpressions;

Console.WriteLine(Regex.IsMatch("abc123", @"\d+"));      // True - цифры где-то есть
Console.WriteLine(Regex.IsMatch("abc123", @"^\d+$"));    // False - вся строка не из цифр
Console.WriteLine(Regex.IsMatch("12345", @"^\d+$"));     // True

var regex = new Regex(@"^[A-Z]{2}\d{4}$");
Console.WriteLine(regex.IsMatch("AB1234"));              // True
Console.WriteLine(regex.IsMatch("ab1234"));              // False

Match и Matches: поиск совпадений

Когда нужно не просто проверить, а получить найденный текст, применяют Match для одного результата и Matches для всех.

Match: первое совпадение

Метод Match возвращает объект Match. Свойство Success говорит, найдено ли совпадение, Value содержит найденный текст, Index хранит позицию начала, Length длину. Метод NextMatch переходит к следующему совпадению.

string text = "Заказ №4521 от 12.03.2025, сумма 1890 руб.";
Match m = Regex.Match(text, @"\d{2}\.\d{2}\.\d{4}");

if (m.Success)
    Console.WriteLine($"Дата {m.Value} найдена с позиции {m.Index}"); // 12.03.2025

Matches: все совпадения

Matches возвращает коллекцию всех непересекающихся совпадений. Её можно перебирать через foreach, а с помощью LINQ быстро превратить в список значений.

string text = "Цены: 120, 3500 и 99 рублей";
MatchCollection all = Regex.Matches(text, @"\d+");

foreach (Match m in all)
    Console.WriteLine(m.Value);   // 120, 3500, 99

int[] numbers = all.Select(m => int.Parse(m.Value)).ToArray();
Console.WriteLine(numbers.Sum()); // 3719

Если найденные строки нужно превращать в числа, посмотрите разбор преобразования string в int: там описаны TryParse и типичные ошибки формата.

Группы и именованные группы

Круглые скобки создают группу, из которой можно получить часть совпадения. Группа 0 содержит весь найденный фрагмент, группы 1, 2 и далее соответствуют скобкам по порядку. Для читаемости используйте именованные группы (?<имя>...): доступ к ним идёт по строковому ключу.

string log = "2025-03-12 14:05:33 ERROR База данных недоступна";
var rx = new Regex(@"^(?<date>\d{4}-\d{2}-\d{2}) (?<time>[\d:]+) (?<level>[A-Z]+) (?<msg>.+)$");

Match m = rx.Match(log);
if (m.Success)
{
    Console.WriteLine(m.Groups["date"].Value);  // 2025-03-12
    Console.WriteLine(m.Groups["level"].Value); // ERROR
    Console.WriteLine(m.Groups["msg"].Value);   // База данных недоступна
}

// обычные группы по номеру
var date = Regex.Match("12.03.2025", @"(\d+)\.(\d+)\.(\d+)");
Console.WriteLine($"{date.Groups[3].Value}-{date.Groups[2].Value}-{date.Groups[1].Value}"); // 2025-03-12

Если группа нужна только для объединения, но не для извлечения, делайте её незахватывающей: (?:...). Это немного ускоряет работу и не засоряет нумерацию. Для проверки соседнего текста без его включения в результат служат опережающие и ретроспективные проверки: (?=...), (?!...), (?<=...), (?<!...).

Replace: замена по шаблону

Replace заменяет все совпадения на указанную строку. В строке замены допустимы ссылки на группы: $1, $2 или ${имя}. Второй вариант перегрузки принимает MatchEvaluator - функцию, которая вычисляет замену для каждого совпадения, например, увеличивает число или меняет регистр.

// убрать лишние пробелы
string clean = Regex.Replace("много    пробелов   тут", @"\s+", " ");
Console.WriteLine(clean); // много пробелов тут

// поменять формат даты: 12.03.2025 -> 2025-03-12
string iso = Regex.Replace("Дата: 12.03.2025", @"(\d{2})\.(\d{2})\.(\d{4})", "$3-$2-$1");

// вычисляемая замена: цены +10%
string prices = Regex.Replace("Товар 100 руб, товар 250 руб",
    @"\d+", m => (int.Parse(m.Value) * 11 / 10).ToString());
Console.WriteLine(prices); // Товар 110 руб, товар 275 руб

// удалить HTML-теги (упрощённо)
string plain = Regex.Replace("<b>текст</b>", "<[^>]+>", "");

Split: разбиение строки

Regex.Split делит строку по любому разделителю, который описывается шаблоном. Это удобнее обычного string.Split, когда разделители разные или встречаются подряд: пробелы, запятые, точки с запятой в любом сочетании. Если в шаблоне есть захватывающая группа, найденные разделители попадут в результирующий массив.

string input = "яблоко, груша;  слива ,вишня";
string[] parts = Regex.Split(input, @"\s*[,;]\s*");
Console.WriteLine(string.Join("|", parts)); // яблоко|груша|слива|вишня

string[] words = Regex.Split("Привет, мир! Как дела?", @"\W+", RegexOptions.None);
// разделение на слова без знаков препинания (последний элемент может быть пустым)

Для простого разбиения по одному символу достаточно обычного Split. Другие приёмы работы со строками описаны в материале про преобразование строки в массив.

Флаги RegexOptions

Поведение шаблона настраивается флагами перечисления RegexOptions. Их можно комбинировать оператором |. Самые полезные приведены в таблице.

ФлагЧто меняет
IgnoreCaseНе различает регистр букв
Multiline^ и $ срабатывают на границах каждой строки, а не всего текста
SinglelineТочка совпадает и с переводом строки
IgnorePatternWhitespaceИгнорирует пробелы и разрешает комментарии # в шаблоне
CompiledКомпилирует шаблон в IL для ускорения при частом использовании
CultureInvariantСравнение без учёта текущей культуры
ExplicitCaptureЗахватываются только именованные группы
RightToLeftПоиск справа налево
NonBacktrackingДвижок без возвратов, гарантирует линейное время (.NET 7+)
var rx = new Regex(@"привет", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
Console.WriteLine(rx.IsMatch("ПРИВЕТ, мир")); // True

string text = "строка 1\nстрока 2\nстрока 3";
var lines = Regex.Matches(text, @"^строка \d$", RegexOptions.Multiline);
Console.WriteLine(lines.Count); // 3

var verbose = new Regex(@"
    ^(\d{3})   # код оператора
    -(\d{2})   # первая часть
    -(\d{2})$  # вторая часть
", RegexOptions.IgnorePatternWhitespace);

Готовые шаблоны: email, телефон, числа

Регулярные выражения часто применяют для проверки форм. Ниже собраны рабочие шаблоны, которые подходят для типичных случаев. Помните, что идеально проверить email регуляркой невозможно, полный стандарт слишком сложен. Для настоящей проверки отправляйте письмо с подтверждением.

static bool IsEmail(string s) =>
    Regex.IsMatch(s, @"^[\w.+-]+@[\w-]+(\.[\w-]+)*\.[A-Za-z]{2,}$");

static bool IsRuPhone(string s) =>
    Regex.IsMatch(s, @"^(\+7|8)[\s-]?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}$");

static bool IsInteger(string s) => Regex.IsMatch(s, @"^-?\d+$");
static bool IsDecimal(string s) => Regex.IsMatch(s, @"^-?\d+([.,]\d+)?$");

Console.WriteLine(IsEmail("user.name@mail.example.ru"));   // True
Console.WriteLine(IsEmail("user@@mail"));                   // False
Console.WriteLine(IsRuPhone("+7 (916) 123-45-67"));         // True
Console.WriteLine(IsRuPhone("8-916-123-45-67"));            // True
Console.WriteLine(IsDecimal("-12,5"));                      // True
Console.WriteLine(IsInteger("12.5"));                       // False

Если вы проверяете ввод в полях формы, например в TextBox, вызывайте проверку в обработчике Validating или TextChanged и подсвечивайте поле при ошибке. А чтобы извлечь все телефоны из большого текста, уберите якоря и используйте Matches.

Производительность и защита от зависаний

Статические методы вроде Regex.IsMatch(text, pattern) кэшируют разобранные шаблоны, но при частых вызовах всё равно стоит создать один объект Regex и использовать его повторно. Флаг Compiled заметно ускоряет работу при тысячах вызовов, но увеличивает время старта и расход памяти, поэтому для разовых проверок он не нужен.

Главная опасность - катастрофический откат (catastrophic backtracking). Шаблон вроде (a+)+$ на строке из многих "a" с одним лишним символом в конце перебирает экспоненциальное число вариантов и подвешивает программу. Если шаблон применяется к пользовательскому вводу, обязательно задавайте время ожидания и избегайте вложенных квантификаторов.

var rx = new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(200));
try
{
    Console.WriteLine(rx.IsMatch(new string('a', 40) + "!"));
}
catch (RegexMatchTimeoutException)
{
    Console.WriteLine("Шаблон выполнялся слишком долго");
}

// движок без возвратов - линейное время
var safe = new Regex(@"^(\w+\s?)*$", RegexOptions.NonBacktracking);

Source generator: GeneratedRegex

Начиная с .NET 7 регулярные выражения можно компилировать на этапе сборки. Для этого помечают разделяемый (partial) метод атрибутом GeneratedRegex, а компилятор генерирует оптимизированный исходный код. Такой вариант быстрее запускается, не использует рефлексию, работает с Native AOT и позволяет просматривать сгенерированный код в отладчике.

using System.Text.RegularExpressions;

public static partial class Validators
{
    [GeneratedRegex(@"^[\w.+-]+@[\w-]+(\.[\w-]+)*\.[A-Za-z]{2,}$", RegexOptions.IgnoreCase)]
    private static partial Regex EmailRegex();

    [GeneratedRegex(@"\d+")]
    public static partial Regex DigitsRegex();

    public static bool IsEmail(string s) => EmailRegex().IsMatch(s);
}

// использование
Console.WriteLine(Validators.IsEmail("test@example.com"));
Console.WriteLine(Validators.DigitsRegex().Matches("a1b22c333").Count); // 3

Класс, содержащий такой метод, должен быть объявлен как partial, а возвращаемый тип метода - Regex. Для новых проектов на .NET 7 и выше это рекомендуемый способ, если шаблон известен заранее. Динамические шаблоны, которые строятся во время работы программы, по-прежнему создают через new Regex.

Типичные ошибки

Первая ошибка - забытые якоря при валидации: IsMatch без ^ и $ пропускает строки с лишними символами. Вторая - неэкранированные спецсимволы. Точка, скобки, плюс, вопросительный знак и звёздочка имеют особый смысл, и чтобы найти их как обычный символ, нужен обратный слеш или метод Regex.Escape, особенно если шаблон собирается из пользовательского ввода.

Третья - жадные квантификаторы. Шаблон <.+> на строке "<b>a</b>" захватит всё от первой скобки до последней, а нужно ленивое <.+?> или <[^>]+>. Четвёртая - попытка разобрать HTML и вложенные структуры регулярками: для этого лучше подходят настоящие парсеры, например HtmlAgilityPack или System.Text.Json для JSON. Наконец, не усложняйте: если задачу решают Contains, StartsWith или Split, регулярное выражение избыточно и хуже читается.

Частые вопросы

Как сделать регулярное выражение нечувствительным к регистру?

Передайте флаг RegexOptions.IgnoreCase третьим аргументом статического метода или в конструктор Regex. Также можно добавить в начало шаблона встроенный модификатор (?i).

Чем Match отличается от Matches?

Match возвращает только первое совпадение, а Matches коллекцию всех совпадений. Если нужно просто узнать, есть ли совпадение, используйте IsMatch: он работает быстрее обоих.

Как проверить только русские буквы?

Используйте класс символов [А-Яа-яЁё]. Для строки целиком добавьте якоря и квантификатор: ^[А-Яа-яЁё]+$. Класс \p{IsCyrillic} тоже подходит и охватывает весь блок кириллицы.

Как экранировать пользовательскую строку для шаблона?

Вызовите Regex.Escape(текст). Он добавит обратные слеши перед всеми специальными символами, и введённый текст будет искаться буквально. Это защищает от ошибок и внедрения лишних конструкций.

Что быстрее: Regex или обычные строковые методы?

Для простых случаев Contains, StartsWith, IndexOf и Split значительно быстрее. Регулярки оправданы, когда шаблон сложный: несколько вариантов, повторения, группы и условия.

Как получить все группы из всех совпадений?

Переберите Matches в цикле, а внутри каждого совпадения обратитесь к m.Groups по номеру или имени. Для компактной записи подойдёт LINQ: Matches.Select(m => m.Groups["name"].Value).