Підрозділ 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.

Як працює двигун регулярних виразів
.NET використовує NFA-двигун (Non-deterministic Finite Automaton — недетермінований скінченний автомат) для пошуку збігів. Принцип роботи:
- Двигун починає порівнювати шаблон з вхідним рядком з поточної позиції.
- При збігу він просувається далі; при невдачі повертається назад (backtracking) і пробує альтернативні шляхи.
- Якщо всі альтернативи вичерпано без збігу — позиція зсувається на 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 та телефону пацієнта: