«Хороший сайт делается для людей, а поисковики приходят следом»

— из принципов редакции SeoLiga.ru
Главная » Статьи » .NET и C# » Регулярные выражения в C#

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

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

В .NET за работу с регулярками отвечает пространство имён System.Text.RegularExpressions. Разберём основные возможности и подводные камни.

Первое знакомство с Regex

Центральный класс — Regex. Самый простой сценарий — проверить, соответствует ли строка шаблону.

using System.Text.RegularExpressions;

bool ok = Regex.IsMatch("abc123", @"^[a-z]+\d+$");
Console.WriteLine(ok);   // True

Обратите внимание на префикс @ перед строкой. Это буквальная (verbatim) строка: в ней обратная косая черта не требует удвоения. Без неё шаблон \d+ пришлось бы писать как "\\d+", и читать такие выражения очень тяжело.

Основы синтаксиса

Прежде чем писать сложные шаблоны, нужно выучить небольшой набор базовых элементов.

  • . — любой символ, кроме перевода строки.
  • \d — цифра, \w — буква, цифра или подчёркивание, \s — пробельный символ. Заглавные версии (\D, \W, \S) означают обратное.
  • ^ и $ — начало и конец строки.
  • [abc] — один из перечисленных символов, [^abc] — любой, кроме них, [a-z] — диапазон.
  • * — ноль или больше повторений, + — одно и больше, ? — ноль или одно.
  • {n}, {n,m} — точное число или диапазон повторений.
  • | — альтернатива («или»).
  • ( ) — группа.

Квантификаторы по умолчанию жадные: они захватывают как можно больше текста. Добавление вопросительного знака (.*?) делает их ленивыми, и они берут минимально возможное. Эта разница — источник самых частых ошибок при разборе HTML и логов.

Поиск совпадений

Метод Match возвращает первое совпадение, а Matches — все.

string text = "Звоните: 8-900-123-45-67 или 8-911-765-43-21";
var rx = new Regex(@"8-\d{3}-\d{3}-\d{2}-\d{2}");

foreach (Match m in rx.Matches(text))
{
    Console.WriteLine($"{m.Value} (позиция {m.Index})");
}

У объекта Match есть свойства Value (найденный текст), Index (позиция), Length и Success. Всегда проверяйте Success, прежде чем читать результат одиночного Match.

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

Скобки позволяют не только группировать, но и извлекать части совпадения. Номера групп идут по порядку открывающих скобок, а нулевая группа — это всё совпадение целиком.

var rx = new Regex(@"(\d{2})\.(\d{2})\.(\d{2})");
var m = rx.Match("Релиз 15.03.24 выпущен");
if (m.Success)
{
    Console.WriteLine(m.Groups[1].Value);   // 15
    Console.WriteLine(m.Groups[2].Value);   // 03
}

Нумерованные группы быстро становятся нечитаемыми. Именованные решают проблему:

var rx = new Regex(@"(?<day>\d{2})\.(?<month>\d{2})\.(?<yy>\d{2})");
var m = rx.Match("15.03.24");
Console.WriteLine(m.Groups["month"].Value);   // 03

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

Замена текста

Метод Regex.Replace умеет подставлять группы в результат через $1 или ${name}.

string s = "Иванов Иван";
string r = Regex.Replace(s, @"(\w+) (\w+)", "$2 $1");
Console.WriteLine(r);   // Иван Иванов

Для сложной логики подойдёт делегат MatchEvaluator, который получает совпадение и возвращает строку-замену:

string r = Regex.Replace("цена 10 и 20", @"\d+",
    m => (int.Parse(m.Value) * 2).ToString());
// "цена 20 и 40"

Разбиение строк

Если разделитель нестандартный или их несколько, string.Split не справится, а Regex.Split подойдёт идеально:

string[] parts = Regex.Split("a, b;c   d", @"[,;\s]+");
// ["a", "b", "c", "d"]

Опции RegexOptions

Поведение шаблона настраивается флагами. Самые полезные:

ОпцияЧто делает
IgnoreCaseИгнорирует регистр букв
Multiline^ и $ работают на границах каждой строки
SinglelineТочка совпадает и с переводом строки
IgnorePatternWhitespaceРазрешает пробелы и комментарии внутри шаблона
CompiledКомпилирует шаблон для ускорения при частом использовании
CultureInvariantОтключает зависимость от региональных настроек

Опция IgnorePatternWhitespace спасает читаемость сложных выражений:

var rx = new Regex(@"
    ^(?<user>[\w.+-]+)   # имя пользователя
    @
    (?<host>[\w-]+\.[\w.-]+)$  # домен
", RegexOptions.IgnorePatternWhitespace);

Производительность и безопасность

Регулярка может стать причиной серьёзных проблем с быстродействием. Главная угроза — катастрофический откат (backtracking). Вложенные квантификаторы вроде (a+)+ на неудачных входных данных заставляют движок перебирать экспоненциальное число вариантов, и процессор зависает.

Что делать:

  1. Избегайте вложенных повторений с пересекающимися множествами символов.
  2. Задавайте таймаут при создании: new Regex(pattern, options, TimeSpan.FromSeconds(1)). По истечении будет выброшено исключение RegexMatchTimeoutException.
  3. Не создавайте объект Regex в цикле. Вынесите его в статическое поле и переиспользуйте.
  4. Для шаблонов, которые вызываются очень часто, применяйте RegexOptions.Compiled или генераторы исходного кода, доступные в новых версиях платформы.
  5. Если на входе приходит пользовательский текст, всегда ограничивайте его длину перед проверкой.

Атомарные группы (?>...) и притяжательные конструкции запрещают откат и помогают ускорить поиск там, где он точно не нужен.

Типичные ловушки

  • Забытые якоря. Без ^ и $ проверка email найдёт адрес внутри любой строки мусора и вернёт true.
  • Неэкранированная точка. Шаблон a.b совпадёт и с «axb». Чтобы искать именно точку, пишите \..
  • Разбор HTML регулярками. Для вложенных структур используйте специальные парсеры, а не регулярные выражения.
  • Идеальная проверка email. Полноценный шаблон по стандарту огромен. На практике достаточно простой проверки и отправки письма с подтверждением.
  • Кириллица и \w. В .NET класс \w понимает юникод, так что русские буквы проходят. Но в других средах это может отличаться.

Практический пример: разбор лога

Допустим, есть строки вида [ERROR] 14:25:03 Не удалось подключиться к базе. Нужно вытащить уровень, время и сообщение.

var rx = new Regex(
    @"^\[(?<level>\w+)\]\s+(?<time>\d{2}:\d{2}:\d{2})\s+(?<msg>.+)$",
    RegexOptions.Compiled);

foreach (var line in File.ReadLines("app.log"))
{
    var m = rx.Match(line);
    if (!m.Success) continue;
    Console.WriteLine($"{m.Groups["level"].Value} | {m.Groups["time"].Value}");
}

Инструменты для отладки

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

Заключение

Регулярные выражения в C# — это компромисс между мощью и читаемостью. Используйте именованные группы, комментируйте шаблоны через IgnorePatternWhitespace, задавайте таймауты и не создавайте Regex в горячих циклах. Там, где достаточно string.Contains или Split, регулярки не нужны вообще. Но когда задача действительно требует гибкого поиска, этот инструмент сделает работу в разы короче и надёжнее.

Р
Редакция
Автор SeoLiga.ru. Пишет о практике: что работает на реальных проектах, а что лучше обойти стороной. Хотите предложить свою статью? Загляните в раздел «Авторам».