OOP Course
Сьогодні

Підрозділ 11.5

Регулярні вирази

Пояснює регулярні вирази в .NET: клас Regex, Matches, Match, RegexOptions, базовий синтаксис шаблонів, перевірку через IsMatch і заміну через Replace.

11.5. Регулярні вирази

Методи Contains, IndexOf, Split — ефективні для пошуку фіксованих підрядків. Але коли потрібно знайти рядок за шаблоном — наприклад, будь-який код МКХ-10 формату [Літера][2 цифри][.цифра]?, номер телефону або email — ці методи вимагають складного ручного коду. Для таких задач існують регулярні вирази (regular expressions, regex).

Основна функціональність у .NET зосереджена у просторі імен System.Text.RegularExpressions. Центральний клас — Regex.

Регулярні вирази у C# — синтаксис та API класу Regex

Як працює двигун регулярних виразів

.NET використовує NFA-двигун (Non-deterministic Finite Automaton — недетермінований скінченний автомат) для пошуку збігів. Принцип роботи:

  1. Двигун починає порівнювати шаблон з вхідним рядком з поточної позиції.
  2. При збігу він просувається далі; при невдачі повертається назад (backtracking) і пробує альтернативні шляхи.
  3. Якщо всі альтернативи вичерпано без збігу — позиція зсувається на 1 символ і процес повторюється.

Повернення (backtracking) — ключова властивість NFA: двигун може «відкатуватися» до попередньої точки вибору і спробувати інший шлях. Саме це дозволяє описувати складні шаблони з альтернативами | і квантифікаторами. Але воно ж може стати пасткою: погано написаний шаблон з багатьма вкладеними квантифікаторами може генерувати експоненційну кількість спроб — явище, відоме як «катастрофічне повернення» (catastrophic backtracking). Для продуктивного коду шаблони мають бути специфічними і уникати надмірного перекриття між альтернативами.

Жадібні та ліниві квантифікатори

За замовчуванням квантифікатори (*, +, ?, {n,m}) є жадібними (greedy): вони намагаються захопити якомога більше символів, а потім повертаються назад, поступаючись символами лише якщо це необхідно для продовження збігу:

// Жадібний: захоплює все між першим < і останнім >
Match m1 = Regex.Match("<b>текст</b>", "<.+>");
Console.WriteLine(m1.Value); // "<b>текст</b>" — увесь рядок

// Лінивий (lazy): захоплює мінімально можливу частину
Match m2 = Regex.Match("<b>текст</b>", "<.+?>");
Console.WriteLine(m2.Value); // "<b>" — тільки перший тег

Ледачий (lazy) квантифікатор записується як *?, +?, ??, {n,m}?. Він означає «якомога менше, але стільки, скільки треба для збігу». Для HTML і XML-подібного тексту ліниві квантифікатори зазвичай дають правильний результат, тоді як жадібні захоплюють занадто багато.

У медичних застосунках цей нюанс важливий при парсингу XML-виписок або HL7 FHIR-повідомлень, де між тегами можуть бути вкладені структури.

Синтаксис регулярних виразів

Регулярний вираз — це рядок-шаблон, де більшість символів означають себе, а спеціальні символи мають особливе значення:

Елемент Значення
^ / $ Початок / кінець рядка
. Будь-який один символ (крім \n)
* Попередній елемент 0 або більше разів
+ Попередній елемент 1 або більше разів
? Попередній елемент 0 або 1 раз
{n} Рівно n разів
{n,m} Від n до m разів
\d / \D Цифра / не цифра
\w / \W Словесний символ (літера, цифра, _) / решта
\s / \S Пробільний / не пробільний символ
\b Межа слова (word boundary)
[abc] Один із символів a, b або c
[A-Z] Символ у діапазоні
(...) Група збігу
(?<name>...) Іменована група
(?=...) Позитивний lookahead
(?!...) Негативний lookahead
(?<=...) Позитивний lookbehind
| Альтернатива: abc|def
\. Екранування: літеральна крапка

У C# шаблони зазвичай записують як verbatim-рядки @"...", щоб уникнути подвійного екранування зворотних слешів: @"\d+" замість "\\d+".

Межа слова (\b)

Метасимвол \b позначає межу слова — позицію між символом \w і символом \W (або початком/кінцем рядка). Він дозволяє шукати слово як ціле, а не підрядок:

// Без \b — знаходить "I10" у будь-якому місці
var re1 = new Regex("I10");
Console.WriteLine(re1.IsMatch("код I10.9"));   // true
Console.WriteLine(re1.IsMatch("XI10Y"));        // true — помилковий збіг!

// З \b — тільки якщо I10 є окремим словом
var re2 = new Regex(@"\bI10\b");
Console.WriteLine(re2.IsMatch("код I10.9"));   // false — після I10 стоїть крапка, але \b є між 0 і .
Console.WriteLine(re2.IsMatch("код I10 є"));   // true — I10 між пробілами

Межі слова є важливими при пошуку кодів у клінічних текстах, де код може бути частиною більшого ідентифікатора.

Lookahead та lookbehind

Lookahead (?=...) і lookbehind (?<=...) дозволяють описувати умови оточення збігу, не включаючи це оточення у результат. Це так звані «нульові твердження» (zero-width assertions) — вони перевіряють контекст, але не «з'їдають» символи:

// Знайти число, що стоїть перед " мм рт.ст."
var reBP = new Regex(@"\d+(?= мм рт\.ст\.)");
Match m = reBP.Match("Тиск 140 мм рт.ст. норма");
Console.WriteLine(m.Value); // "140" — без " мм рт.ст."

// Знайти число після "ЧСС: "
var reHR = new Regex(@"(?<=ЧСС: )\d+");
Match m2 = reHR.Match("АТ: 140, ЧСС: 78, SpO2: 97");
Console.WriteLine(m2.Value); // "78" — без "ЧСС: "

Negative lookahead (?!...) та negative lookbehind (?<!...) — інвертовані версії: збіг відбувається лише якщо не виконується умова оточення.

Клас Regex та основні методи

using System.Text.RegularExpressions;

var regex = new Regex(@"[A-Z]\d{2}(\.?\d)?");

// IsMatch — bool: чи є хоч один збіг
bool ok = regex.IsMatch("I10.9");   // true
bool no = regex.IsMatch("999");     // false

// Match — перший збіг
Match m = regex.Match("код: I10.9, примітка");
Console.WriteLine(m.Value);  // "I10.9"
Console.WriteLine(m.Index);  // 5 — позиція у рядку
Console.WriteLine(m.Success); // true — перевіряємо перед використанням

// Matches — всі збіги
MatchCollection all = regex.Matches("I10.9 і J45.0");
foreach (Match match in all)
    Console.WriteLine(match.Value); // "I10.9", "J45.0"

Властивість m.Success — обов'язкова перевірка при роботі з Match. Якщо збігу немає, m.Value та m.Index повернуть порожній рядок та 0 відповідно — без виключень. Тому код, який звертається до m.Value без перевірки m.Success, може давати хибні результати замість очевидної помилки.

RegexOptions

Конструктор Regex приймає параметр RegexOptions, який змінює поведінку шаблону:

Параметр Ефект
IgnoreCase Нечутливість до регістру
Multiline ^ та $ — початок/кінець кожного рядка
Singleline . відповідає і \n
Compiled Компіляція у IL для швидшого виконання при багаторазовому використанні
CultureInvariant Ігнорувати регіональні відмінності
var re = new Regex(@"гіпертензія", RegexOptions.IgnoreCase | RegexOptions.Compiled);

RegexOptions.Compiled заслуговує на особливу увагу: без цього параметра .NET інтерпретує шаблон під час виконання кожного IsMatch/Match виклику. З Compiled — перший виклик компілює шаблон у IL (машинний код CLR), і всі наступні виклики виконуються значно швидше. Платою є затримка і більше пам'яті при першому використанні. Правило: застосовуйте Compiled для шаблонів, що використовуються тисячі разів у циклі; уникайте для разових перевірок.

Іменовані групи

Групи (...) дозволяють витягати підчастини збігу. Іменовані групи (?<name>...) зручніші за числові: не потрібно пам'ятати порядок:

var re = new Regex(
    @"(?<last>\w+);(?<first>\w+);(?<age>\d+);(?<icd>[A-Z]\d{2}\.?\d?)");

Match m = re.Match("Петренко;Іван;67;I10.9");
if (m.Success)
{
    Console.WriteLine(m.Groups["last"].Value);  // Петренко
    Console.WriteLine(m.Groups["age"].Value);   // 67
    Console.WriteLine(m.Groups["icd"].Value);   // I10.9
}

Якщо поля в рядку змінять порядок — треба змінити лише шаблон, а не весь код читання груп.

Числові групи m.Groups[1] доступні поряд з іменованими. Нульова група m.Groups[0] завжди дорівнює m.Value — тобто весь збіг цілком. Групи можуть бути вкладеними і одна й та сама група може з'являтися в MatchCollection кілька разів (наприклад, для (\w+;)+). У такому випадку m.Groups["name"].Captures містить усі входження цієї групи.

Replace та Split

Replace замінює всі збіги шаблону на заданий рядок:

// Видалити всі не-цифри з телефону
string phone  = "+38 (067) 123-45-67";
string digits = Regex.Replace(phone, @"\D", ""); // "380671234567"

// Нормалізувати множинні пробіли
string notes  = "пацієнт   прийнятий   10.06.2026";
string clean  = Regex.Replace(notes, @"\s{2,}", " "); // "пацієнт прийнятий 10.06.2026"

Replace також підтримує підстановки (back-references) у рядку заміни. Наприклад, $1 вставляє вміст першої групи, ${name} — вміст іменованої групи. Це дозволяє трансформувати формат без повторного парсинга:

// Перетворити "Петренко Іван" → "Іван Петренко"
string name    = "Петренко Іван";
string swapped = Regex.Replace(name, @"(?<last>\w+) (?<first>\w+)", "${first} ${last}");
Console.WriteLine(swapped); // "Іван Петренко"

Regex.Split розбиває рядок по збігах шаблону — потужніший аналог string.Split:

// Розбити по будь-якій кількості пробілів або крапок з комою
string[] parts = Regex.Split("Петренко ; Іван ;; 67", @"\s*;\s*");
// ["Петренко", "Іван", "", "67"]

Статичні методи Regex

Клас Regex дозволяє викликати методи статично, без створення об'єкта. Зручно для разових перевірок:

bool valid = Regex.IsMatch("I10.9", @"^[A-Z]\d{2}(\.?\d)?$");
string     = Regex.Replace("+380671234567", @"\D", "");

Але для повторного використання в циклі або гарячому шляху краще створити об'єкт Regex один раз і зберегти його у полі класу — об'єкт Regex є потокобезпечним (thread-safe) для читання. Статичні методи внутрішньо кешують останні 15 шаблонів, але при багатьох різних шаблонах кеш переповнюється, і компіляція відбувається щоразу заново.

Оптимальний паттерн для сервісних класів:

class PatientValidator
{
    // Компілюється один раз при першому виклику ValidateIcd
    private static readonly Regex IcdRegex =
        new Regex(@"^[A-Z]\d{2}(\.?\d)?$", RegexOptions.Compiled);

    public bool ValidateIcd(string code) => IcdRegex.IsMatch(code);
}

readonly static поле гарантує, що об'єкт Regex створюється лише раз і безпечно використовується з будь-яких потоків.

Валідація клінічних даних — runnable приклад

Перевірка формату коду МКХ-10 та телефону пацієнта:

Парсинг медичного запису через іменовані групи — runnable приклад

Розроблено Tomka Yurii · © 2026 ·