У темі 05 ми розібрали методи рядка:
.split(), .find(), .startswith(), .replace().
Кожен із них відповідає на просте питання про конкретний підрядок: чи є він, де він,
заміни його. У темі 12 ми навчились обходити рядки циклом,
у 14 — загортати перевірки у функції, у
18 — чесно падати, коли дані не такі, як обіцяли.
Тепер зʼявляється задача, у якій усього цього замало.
Наскрізний приклад теми — три рядки з журналу подій маленького інтернет-магазину. Такий файл ти прочитаєш засобами теми 20 і отримаєш звичайний рядок:
2026-03-15 09:42 WARN #1044 Петро petro@ua.net +380509876543 1290.00 грн
2026-03-16 11:03 ERROR #1045 Ірина iryna@mail.com +380631112233 75.00 грн
Питання, на які треба відповісти: скільки тут номерів замовлень, які саме адреси пошти
трапляються, чи всі телефони мають правильний формат, як заховати останні цифри телефонів
перед тим, як показати журнал стажерові. І головне — усе це має працювати і на трьох рядках,
і на трьохстах тисячах, де замість WARN буде warn, замість
#1043 — #7, а після ціни хтось поставить два пробіли.
01 / МежаКоли регулярка доречна, а коли зайва
Почнімо з протилежного боку — з випадків, де регулярний вираз не потрібен. Це важливіше за синтаксис, бо синтаксис вивчиться за годину, а звичка тягнути регулярку в кожен рядок псує код роками.
Правило одне: регулярний вираз виправданий тоді, коли шаблон справді змінний.
Якщо ти можеш назвати підрядок словами — «починається з 2026-», «містить
@», «розділене крапкою з комою» — то це робота для методів рядка. Вони швидші,
читаються без коментарів і не мають власного синтаксису, у якому можна помилитись.
| завдання | простий спосіб | чи потрібна регулярка |
|---|---|---|
| чи є «грн» | "грн" in рядок | ні, ніколи |
| чи починається з дати 2026 | рядок.startswith("2026") | ні |
| розбити по пробілах | рядок.split() | ні |
| прибрати крайні пробіли | рядок.strip() | ні |
| замінити «грн» на «UAH» | рядок.replace(…) | ні |
| усі числа з рядка | — | так |
| дата у трьох різних форматах | — | так |
| розбити по «кома або крапка з комою» | — | так |
| перевірити формат телефону | — | так |
Різниця між двома половинами таблиці помітна одразу: у верхній ми знаємо що шукаємо, у нижній — лише яким воно має бути. «Усі числа» — це не підрядок, це опис форми: одна або більше цифр підряд. Ось для таких описів і придумали регулярні вирази.
<div> усередині <div> усередині
<div>. Щоб знайти парний закривальний тег, треба рахувати глибину
вкладеності, тобто памʼятати необмежену кількість станів. Регулярний вираз цього не вміє
принципово, а не через недоробку. Те саме стосується JSON, дужок у виразі й будь-чого
вкладеного. Для HTML бери справжній розбірник, для JSON — модуль json
(тема 21). Регулярка тут не «поганий інструмент», вона просто інший інструмент.02 / r"…"Сирі рядки: чому без них усе подвоюється
Перш ніж писати перший шаблон, треба залагодити конфлікт двох мов. Зворотний скісний
(backslash) — службовий символ і в Python, і в мові регулярних виразів. Python бачить
"\n" і робить із двох символів один — перехід рядка. Регулярний вираз бачить
\d і розуміє це як «будь-яка цифра». Проблема в тому, що рядок спершу читає Python
і лише потім віддає результат модулю re.
У літералі "\d" Python не знає екранування \d, тому лишає
обидва символи як є — і ще й попереджає про це (SyntaxWarning у 3.12).
А от у "\b" він знає: це символ забою (backspace), код 8. Один символ
замість двох.
Модулю дістається вже оброблений рядок. Замість шаблону «межа слова» (\b
у мові регулярних виразів) він отримує невидимий символ забою — і чесно шукає його
в тексті. Не знаходить ніколи.
Або подвоювати кожен скісний — "\\b\\d+", — або вимкнути обробку
екранувань префіксом r: r"\b\d+". Другий варіант коротший
і не має жодного мінуса. Тому шаблон завжди пишуть сирим рядком,
навіть коли скісних у ньому немає — щоб не думати про це щоразу.
len(r"\b") → 2 # скісний і літера b — те, що треба re
Єдине обмеження сирого рядка: він не може закінчуватись непарною кількістю скісних, бо навіть у сирому режимі скісний перед лапкою не дає їм закрити рядок. На практиці це не заважає ніколи.
03 / АбеткаАбетка шаблону
Шаблон — це рядок, у якому більшість символів означають самі себе, а десяток службових означають «щось із форми». Почнімо з чотирьох найважливіших цеглин.
Літерал. Літера г у шаблоні збігається з літерою г
у тексті. Шаблон r"грн" — це те саме, що "грн" in рядок, тільки
дорожче. Саме тому такі шаблони й не пишуть.
Крапка . — «будь-який один символ, крім переходу рядка».
Шаблон r"#..." знайде решітку й три будь-які символи після неї. Крапка — найширший
і найнебезпечніший елемент мови: вона проковтне і цифру, і пробіл, і кому.
Клас символів […] — «будь-який один символ із перелічених».
[абв] — це «а», «б» або «в», і рівно один. Усередині квадратних дужок можна
задавати діапазон через дефіс: [а-я], [0-9],
[A-Za-z]. А якщо одразу після відкривної дужки поставити ^, клас
перевертається: [^0-9] — «будь-який символ, крім цифри».
[.] — це саме крапка, а не «будь-який символ», і
екранувати її не треба. Дефіс теж стає звичайним, якщо стоїть першим або останнім:
[-+] — це «мінус або плюс». А от українські діапазони пиши обережно:
[а-я] не містить літер «і», «ї», «є», «ґ» — у таблиці Unicode
вони живуть окремо від основного кириличного блоку. Правильний діапазон для українських
малих: [а-щьюяіїєґ].І нарешті готові класи — скорочення для трьох найчастіших наборів. Саме на них новачки роблять найтихішу помилку теми, тому їх варто помацати руками.
| скор. | що означає | заперечення |
|---|---|---|
| \d | цифра | \D — не цифра |
| \w | «словесний» символ: літера, цифра або підкреслення | \W — решта |
| \s | пробільний: пробіл, табуляція, перехід рядка | \S — не пробільний |
Пастка в тому, що в Python ці класи за замовчуванням розуміють увесь Unicode.
\w — це не [A-Za-z0-9_], як у багатьох мовах і як підказує здоровий
глузд англомовної документації. Українська «я» під \w потрапляє. Грецька «λ»
теж. І арабська цифра «٣» під \d теж потрапляє, бо в Unicode вона позначена
як десяткова цифра. Це рятує, коли ти пишеш r"\w+" і хочеш ловити слова
українською. І це підводить, коли ти пишеш r"^\w+$" для перевірки логіна й
раптом дозволяєш логін «Олена٣».
Інтерактив 1 · Що насправді входить у \d, \w і \s
Десять символів проти шести класів. Перемикач re.ASCII
показує, як усе змінюється, коли Unicode вимкнено.
свій символ — останній ряд
re.ASCII — ні. Прапорець re.ASCII звужує \d,
\w, \s і \b до чистого ASCII одним рухом; його
ставлять, коли шаблон описує технічний формат (ідентифікатор, ключ конфігурації), а не
людський текст. Зверни увагу й на ½: це «словесний» символ, але не цифра —
\w ширший за \d не тільки на літери.04 / СкількиСкільки разів: квантифікатори
Досі кожна цеглина відповідала рівно за один символ. Квантифікатор (quantifier) стоїть після цеглини й каже, скільки разів вона має повторитись.
| запис | скільки разів | приклад |
|---|---|---|
| * | нуль або більше | r"ко*т" — «кт», «кот», «кооот» |
| + | один або більше | r"\d+" — «7», «1043» |
| ? | нуль або один | r"кольор?" — «кольо» і «кольор» |
| {n} | рівно n | r"\d{4}" — рік |
| {n,m} | від n до m | r"\d{2,4}" |
| {n,} | n або більше | r"\d{3,}" |
Ось тут регулярні вирази й починають окупатись. Дата з нашого журналу описується як
r"\d{4}-\d{2}-\d{2}" — чотири цифри, дефіс, дві цифри, дефіс, дві цифри.
Номер замовлення — r"#\d+", і байдуже, чи там одна цифра, чи сім. Ціна —
r"\d+\.\d{2}", де крапку довелось екранувати, бо гола крапка означала б
«будь-що».
Найкорисніша комбінація в темі — заперечений клас із плюсом:
[^ ]+ означає «якнайдовший шматок без пробілів». Через неї описують «поле до
роздільника», і саме вона врятує нас у розділі про жадібність.
Тепер збери шаблон сам. Поле зліва — текст журналу, його можна правити; поле справа — шаблон. Усі збіги підсвічуються прямо в тексті: перший рожевим, решта — бірюзовим.
Інтерактив 2 · Конструктор шаблону
Живий пошук по нашому журналу. Шаблон компілюється на кожен натиск клавіші; зламаний шаблон покаже помилку так само, як її покаже Python.
шаблон · пиши як у r"…"
(\d+ не компілюється,
і Python зупиняє програму ще до пошуку — re.error. Це добра новина: помилка
в шаблоні виявляється відразу, а не через тисячу рядків мовчазно неправильних результатів.
Далі спробуй ^\d{4} з галочкою re.MULTILINE і без неї: без неї
збіг один, з нею — три.05 / ЖадібністьЖадібність — головна пастка теми
Усі квантифікатори з таблиці вище жадібні (greedy): вони беруть якнайбільше, а потім віддають назад по одному символу, поки решта шаблону не складеться. Слово «якнайбільше» тут ключове, і воно означає «до кінця рядка», а не «до наступного розумного місця» — бо розумних місць регулярка не знає.
Класична демонстрація — теги. Візьмімо наш номер замовлення, обгорнутий розміткою:
Шаблон r"<.+>" читається як «кутова дужка, потім щось, потім кутова
дужка». Людина розуміє це як «один тег». Рушій розуміє інакше: .+ ковтає весь
рядок до кінця, тоді відступає назад до останньої дужки — і повертає весь рядок
цілком як один збіг.
Полагодити можна двома способами, і обидва варто знати:
- Лінивий квантифікатор. Знак питання після
*,+або?перевертає стратегію:*?,+?,??беруть якнайменше й додають по одному символу, лише поки решта не складеться.r"<.+?>"знайде чотири теги; - Заперечений клас.
r"<[^>]+>"— «дужка, потім щось, у чому немає закривної дужки, потім дужка». Тут нема чого віддавати назад: клас фізично не може перескочити>.
Другий спосіб і читабельніший, і помітно швидший, бо не потребує відкату. Правило досвідчених: якщо ти написав ліниву крапку, спитай себе, чи не можна замінити її запереченим класом. Найчастіше можна.
Інтерактив 3 · Жадібний проти лінивого
Один текст, один шаблон, два режими. Верхній ряд — жадібний, нижній — той самий шаблон зі знаком питання.
<b>.*</b> обидва режими дають один
збіг однакової довжини, бо тексту з двома парами <b> тут немає. Лінивість
не безкоштовна й не завжди правильна: вона просто інша стратегія відкату.06 / ЯкоріЯкорі: місце, а не символ
Три позначки в мові шаблонів не збігаються з жодним символом — вони описують позицію між символами. Тому їх називають якорями, і тому вони мають нульову ширину.
^— початок рядка. За замовчуванням це початок усього тексту; з прапорцемre.MULTILINE— початок кожного рядка;$— кінець. З тією ж поправкою наre.MULTILINE. Одна тонкість:$збігається і перед фінальним переходом рядка, томуr"\d$"знайде цифру в"1043\n";\b— межа слова: місце, де з одного боку словесний символ (\w), а з другого — ні. Не плутай із пробілом: у рядку"#1043"межа стоїть між#і1, хоча пробілу там нема.
Якорі — головний спосіб не знайти зайвого. Шаблон r"\d{4}" у нашому журналі
знайде і рік 2026, і номер 1043. А r"^\d{4}" із
re.MULTILINE — лише рік, бо номер стоїть не на початку рядка. Так само
r"\bгрн\b" не спрацює всередині слова «грнище», тоді як
r"грн" спрацює.
\b і українську. Оскільки \w у Python
охоплює весь Unicode, \b теж рахує українські літери словесними — і межа слова
стоїть там, де її очікує людина. З прапорцем re.ASCII все ламається:
у слові «Олена» кожна літера стає несловесною, межі зникають, і r"\b\w+\b"
раптом не знаходить нічого. Це рівно та причина, чому прапорець ставлять свідомо, а не «про
всяк випадок».07 / ФункціїФункції модуля re
Мова шаблонів — половина справи. Друга половина — вісім функцій модуля, і найчастіша
помилка новачка живе саме тут: re.match робить не те, що здається
з назви.
| функція | що робить | що повертає |
|---|---|---|
| search | шукає перший збіг будь-де | обʼєкт Match або None |
| match | перевіряє збіг з початку тексту | Match або None |
| fullmatch | вимагає, щоб збіг покрив увесь текст | Match або None |
| findall | усі збіги одразу | список рядків |
| finditer | усі збіги ліниво, по одному | ітератор обʼєктів Match |
| sub | замінює всі збіги | новий рядок |
| subn | те саме плюс лічильник | кортеж (рядок, скільки) |
| split | ріже текст по збігах | список рядків |
re.match прикріплений до початку тексту, але не до кінця:
re.match(r"\d+", "1043 грн") успішно знайде "1043", а
re.match(r"\d+", "#1043") поверне None, бо на нульовій позиції
стоїть решітка. Саме тому для перевірки «чи весь рядок має такий формат» беруть
fullmatch, а для «чи є десь усередині» — search. Функція
match потрібна рідше, ніж її використовують; здебільшого людина хотіла
search і випадково написала знайому назву.
Інтерактив 4 · search проти match проти fullmatch
Один шаблон, один текст, три функції. Сірі позначки ліворуч і рамка показують, що саме функція зобовʼязана покрити.
текст: #1043 на 249.50 грн
\d+: search знаходить
1043 на позиції 1, match повертає None, хоча цифри
в тексті очевидно є. Тому if re.match(...) у перевірці «чи містить рядок
число» — тиха помилка, яка не падає, а просто завжди відповідає «ні».sub і split: заміна й розбиття за формою
re.sub(шаблон, заміна, текст) замінює всі збіги. Заміна може бути рядком —
або функцією, якій передають обʼєкт збігу, а вона повертає рядок. Другий
варіант — найпотужніша річ у темі: він дозволяє замінювати не константою, а обчисленням.
+380671234567 → +38067*****67
re.split(шаблон, текст) — це .split(), у якого роздільник заданий
формою. re.split(r"[,;]\s*", "кава, чай;сік") дасть три елементи, і не треба
двічі викликати .replace(), щоб звести роздільники до одного.
08 / ГрупиГрупи: витягти частину збігу
Досі ми відповідали «так/ні» і «де». Тепер навчимось діставати шматки. Круглі дужки
(…) у шаблоні роблять дві речі одночасно: групують частину виразу для
квантифікатора й запамʼятовують, що саме туди потрапило.
Обʼєкт збігу віддає ці шматки трьома методами:
.group(0)— увесь збіг цілком. Те саме, що.group();.group(1),.group(2)… — вміст першої, другої дужки. Нумерація йде за порядком відкривних дужок зліва направо, тому у вкладених групах зовнішня завжди має менший номер;.groups()— кортеж усіх груп без нульової.
Номери працюють, поки груп дві. На пʼятій ти вже рахуєш дужки пальцем, а після редагування
шаблону всі номери зсуваються. Тому є іменовані групи:
(?P<імʼя>…). Тоді .group("рік") читається без коментаря, а
.groupdict() одразу віддає словник — готовий запис для подальшої обробки.
м.groupdict() → {'рік': '2026', 'місяць': '03', 'день': '15'}
Інтерактив 5 · Що бачить кожна група
Смуги під текстом — межі груп. Нульова група — увесь збіг, решта — те, що спіймали дужки.
(?:…) групує,
але нічого не запамʼятовує. Це не мікрооптимізація, а гігієна: r"(?:грн|UAH)"
не зсуває номери решти груп і не засмічує .groups(). Ще один наслідок групування
відчувається у findall: якщо в шаблоні є групи, він повертає не збіги, а
вміст груп — і re.findall(r"(\d+)\.(\d+)", текст) віддасть список
кортежів, а не список чисел. Це щоразу дивує.09 / Прапорціre.compile і прапорці
re.compile(шаблон) перетворює рядок на обʼєкт-шаблон із тими самими методами:
ШАБЛОН.search(текст), ШАБЛОН.findall(текст). Компіляція коштує часу,
тому модуль тримає внутрішній кеш останніх шаблонів — і на практиці різниця у швидкості між
re.search(…) у циклі й заздалегідь скомпільованим обʼєктом невелика.
Справжня причина компілювати інша, і вона про людей:
- Шаблон отримує імʼя.
ТЕЛЕФОН = re.compile(r"\+380\d{9}")на початку модуля — це документація. У циклі читаєтьсяТЕЛЕФОН.findall(рядок), а не стіна зі скісних; - Помилка в шаблоні виявляється при імпорті, а не тоді, коли до цієї гілки коду вперше дійде виконання;
- Кеш обмежений (кілька сотень шаблонів) і чиститься цілком при переповненні. Якщо шаблон збирається з даних у циклі, кеш не допоможе.
Прапорці передають третім аргументом функції або другим у compile. Три з них
покривають майже все:
| прапорець | що змінює | коли треба |
|---|---|---|
| re.IGNORECASE | велика й мала літера стають однакові | WARN, Warn, warn у журналі |
| re.MULTILINE | ^ і $ ловлять кожен рядок | обробка багаторядкового тексту |
| re.DOTALL | крапка починає ловити й перехід рядка | збіг має перетнути рядки |
| re.ASCII | \d \w \s \b звужуються до ASCII | технічні формати |
| re.VERBOSE | пробіли й #-коментарі в шаблоні ігноруються | довгий шаблон із поясненнями |
Кілька прапорців зʼєднують вертикальною рискою:
re.compile(r"^\w+", re.IGNORECASE | re.MULTILINE). А re.VERBOSE
варто спробувати на будь-якому шаблоні, довшому за рядок: він дозволяє розбити вираз на
кілька рядків із коментарями українською, і це різниця між «шифром» і кодом, який переживе
наступний рік.
10 / РушійЯк рушій шукає насправді
Тепер шар для тих, хто вже пише код. Регулярний вираз у Python виконує рушій із відкатом (backtracking engine). Він працює прямолінійно:
- стає на позицію 0 тексту й намагається прикласти шаблон зліва направо;
- жадібний квантифікатор бере якнайбільше, лінивий — якнайменше;
- якщо наступна цеглина не збіглась, рушій відкочується: змушує попередній квантифікатор віддати один символ і пробує знову;
- якщо всі варіанти вичерпано, зсувається на одну позицію вправо й починає спочатку.
Останній пункт пояснює, чому re.search дорожчий за re.match:
невдалий пошук у рядку довжиною n означає n спроб. Прокрути інтерактив по кроках — і
подивись, як саме рушій ковтає, відступає й починає заново.
Інтерактив 6 · Крок за кроком
Спрощений, але чесний рушій: літерали, крапка, класи й квантифікатори
* + ?. Кожен крок — одне рішення.
\d+грн: збігу немає, але рушій це зʼясовує
не одразу. Він чесно пробує кожну стартову позицію, і на кожній цифрі ще й відкочується.
Порожній результат коштує роботи — це важливо памʼятати, коли шаблон прикладають до
мільйона рядків.Катастрофічний відкат
А тепер найдорожча грабля, яку регулярний вираз може вкласти у продакшн. Вона зветься катастрофічним відкатом (catastrophic backtracking) і виникає, коли квантифікатор стоїть усередині іншого квантифікатора, а те, що вони ловлять, перетинається.
Збігу тут немає — літери b в кінці нема. Але щоб це довести, рушій має
перебрати всі способи розділити ланцюжок із n літер «a» між зовнішнім і внутрішнім
квантифікаторами. Способів рівно 2n−1. На 24 літерах це вісім
мільйонів спроб, на 30 — пів мільярда, на 40 — понад пʼятсот мільярдів. Програма не падає
й не повідомляє про проблему: вона просто перестає відповідати.
Інтерактив 7 · Ціна вкладеного квантифікатора
Скільки варіантів перебере рушій, доводячи, що збігу немає. Вісь ліворуч — логарифмічна: кожна поділка вдесятеро більша за попередню.
(a+)+, (\w+\s*)*,
(.*)*). Заміняй .* на заперечений клас. І ніколи не збирай
шаблон із тексту, який прислав користувач — це готова відмова в обслуговуванні
на одному рядку.Регулярка не памʼятає стану
Повернімось до того, з чого почали. Рушій із відкатом усе одно розпізнає лише регулярні мови: у нього немає лічильника, немає стека, немає памʼяті про те, скільки дужок він уже відкрив. Ось наочна межа:
- можна: «рівно чотири цифри» —
r"\d{4}". Число фіксоване, рушій просто повторює крок чотири рази; - можна: «будь-яка кількість цифр» —
r"\d+". Рахувати не треба, треба лише знати «ще одна чи вже ні»; - не можна: «стільки ж закривних дужок, скільки відкривних». Для цього треба памʼятати число, яке нічим не обмежене. Ніякий шаблон цього не опише — і саме тому вкладений HTML принципово поза межами теми.
Практичний наслідок: щойно тобі знадобилось «памʼятати, що було раніше в тексті» — зупинись. Далі йде звичайний цикл із умовами або готовий розбірник формату. Регулярка добре ловить окремі шматки; складати з них структуру — робота Python.
Чому імена груп краще за номери
І остання порада, яка нічого не коштує. Шаблон із номерами груп ламається тихо: варто
дописати ще одні дужки на початку — і .group(2) починає повертати щось інше,
не викидаючи жодної помилки. Шаблон з іменами ламається голосно: помилкове імʼя дає
IndexError: no such group одразу. У парі з re.VERBOSE це перетворює
регулярний вираз із заклинання на код, який можна читати вголос.
(?P<дата>\d{4}-\d{2}-\d{2})\s+ # 2026-03-15
(?P<час>\d{2}:\d{2})\s+ # 09:41
(?P<рівень>[A-Z]+)\s+ # INFO / WARN / ERROR
\#(?P<номер>\d+) # #1043
""", re.VERBOSE)
11 / ПідсумокЩо з цього забрати
- Спершу спитай, чи потрібна регулярка.
in,.startswith(),.split()покривають більшість задач і читаються без зусиль. Регулярка — коли шаблон змінний; - Шаблон завжди сирим рядком —
r"…". Інакше Python зʼїсть скісні до того, як їх побачить модульre; \wу Python — це Unicode. Українські літери туди входять;re.ASCIIзвужує до латиниці й ламає\bдля кирилиці;- Жадібність за замовчуванням.
.+бере до кінця рядка;.+?бере якнайменше; заперечений клас[^>]+— і читабельніший, і швидший за обидва; match— не «чи є», а «чи з початку». Для «чи є десь» бериsearch, для «чи весь рядок такий» —fullmatch;- Іменовані групи й
.groupdict()перетворюють збіг на готовий запис і не ламаються від дописаних дужок; - Вкладений квантифікатор — міна.
(a+)+на непідхожому тексті перебирає 2n варіантів і вішає програму без жодної помилки.
Наскрізна думка теми та сама, що й у попередніх: інструмент варто розуміти зсередини.
Регулярний вираз здається магією, поки не побачиш, що це прямолінійний перебір із відкатом.
Щойно побачив — стає зрозуміло і чому .+ ковтає забагато, і чому невдалий пошук
коштує дорожче за вдалий, і чому вкладені квантифікатори не можна писати не думаючи.
practice.ipynb ти зараз розбереш наш журнал на записи, замаскуєш телефони через
sub, перевіриш формати через fullmatch — і окремо переконаєшся, що
в одному з чотирьох завдань регулярка була зайва.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.