Регулярні вирази — це міні-мова для опису текстових шаблонів. Модуль `re` компілює ці шаблони та застосовує їх до рядків.
Чотири найпоширеніші функції різняться тим, *де* вони шукають:
```python
import re
text = "User: alice, age 30"
re.search(r"\d+", text) # перший збіг ДЕ ЗАВГОДНО в рядку -> Match "30"
re.match(r"\d+", text) # збіг лише НА ПОЧАТКУ -> None (починається з "U")
re.fullmatch(r"\d+", "42") # весь рядок має збігтися -> Match "42"
re.findall(r"\d+", text) # всі незбіжні збіги -> ["30"]
re.sub(r"\d+", "??", text) # замінити всі збіги -> "User: alice, age ??"
```
**Навіщо raw-рядки?**
Зворотний слеш у рядку Python має особливе значення: `\n` = новий рядок, `\t` = табуляція. Але regex теж активно використовує зворотні слеші (`\d`, `\w`, `\s`). Без raw-рядків довелось би подвоювати кожен слеш:
```python
re.search("\\d+", text) # працює, але нечитабельно
re.search(r"\d+", text) # те саме - завжди використовуй raw-рядки
```
Правило: кожен патерн regex має бути raw-рядком `r"..."`.
**Читаємо патерн посимвольно**
Візьмемо `r"(\w+)@(\w+\.\w+)"` (простий патерн email):
```
( відкрити групу 1
\w+ один або більше символів слова [a-zA-Z0-9_]
) закрити групу 1
@ літеральний "@"
( відкрити групу 2
\w+ один або більше символів слова
\. літеральна крапка (екранована - звичайна . означає "будь-який символ")
\w+ один або більше символів слова
) закрити групу 2
```
```python
m = re.search(r"(\w+)@(\w+\.\w+)", "Надіслати на [email protected] будь ласка")
m.group(0) # "[email protected]" - повний збіг
m.group(1) # "bob" - група 1
m.group(2) # "example.com" - група 2
```
**Методи об'єкта Match**
```python
m = re.search(r"\d{4}", "Рік: 2024 або 2025")
if m:
m.group() # "2024" - знайдений текст
m.start() # 6 - початковий індекс
m.end() # 10 - кінцевий індекс
m.span() # (6, 10) - обидва разом
```
Завжди перевіряй `if m:` перед викликом методів - `search` повертає `None`, якщо збігу немає.
**re.sub з функцією**
Заміна може бути функцією, що отримує кожен об'єкт збігу:
```python
def double(m):
return str(int(m.group()) * 2)
re.sub(r"\d+", double, "a=3, b=7") # "a=6, b=14"
```
Це потужніше за статичну заміну рядка.
Групи, не-захоплюючі групи, прапори, re.compile, finditer
**Захоплюючі групи** дозволяють витягувати частини збігу. Оберни дужками те, що хочеш захопити.
```python
import re
log = "2024-03-15 ERROR: disk full"
m = re.search(r"(\d{4}-\d{2}-\d{2}) (\w+): (.+)", log)
m.group(1) # "2024-03-15"
m.group(2) # "ERROR"
m.group(3) # "disk full"
```
**Іменовані групи** роблять код самодокументованим: `(?P<name>...)` визначає групу, `m.group('name')` її отримує.
```python
m = re.search(r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<level>\w+): (?P<msg>.+)", log)
m.group("date") # "2024-03-15"
m.group("level") # "ERROR"
m.group("msg") # "disk full"
```
**Не-захоплюючі групи** `(?:...)` групують без створення нумерованого захоплення - зручні для чергування або квантифікаторів, коли не треба витягувати:
```python
# Знайти 'colour' або 'color', не захоплюючи необов'язкову 'u'
re.findall(r"colou?r", "colour and color") # ['colour', 'color']
# Не-захоплююча група для чергування
re.findall(r"(?:cat|dog)s?", "cats and dogs") # ['cats', 'dogs']
```
**Прапори** змінюють поведінку рушія:
```python
re.search(r"hello", "Hello World", re.IGNORECASE) # без урахування регістру
re.findall(r"^\w+", text, re.MULTILINE) # ^ збігається з початком кожного рядка
re.search(r"begin.+end", text, re.DOTALL) # . збігається і з новими рядками
re.search(r"\d+", text, re.IGNORECASE | re.MULTILINE) # комбінація через |
```
Вбудований прапор у патерн: `(?i)hello` означає без урахування регістру (те саме, що `re.IGNORECASE`).
**re.compile** - попередня компіляція, якщо патерн використовується багато разів:
```python
phone_pat = re.compile(r"\+?\d[\d\s\-]{7,14}\d")
for line in big_list:
m = phone_pat.search(line) # швидше, ніж re.search(pattern, line) щоразу
```
**re.finditer vs re.findall**
`findall` повертає список рядків. `finditer` повертає ітератор об'єктів Match - більш ощадливий до пам'яті для великих текстів, і дає інформацію про позицію:
```python
for m in re.finditer(r"\d+", "a1 b22 c333"):
print(m.group(), m.start()) # "1" 1 / "22" 4 / "333" 7
```
**re.split** ділить за патерном (не лише за фіксованим роздільником):
```python
re.split(r"[,;\s]+", "one, two; three") # ['one', 'two', 'three']
# Захоплююча група в патерні -> роздільник включається в результат
re.split(r"(,)", "a,b,c") # ['a', ',', 'b', ',', 'c']
```
Жадібні vs ліниві квантифікатори, re.VERBOSE і довідник синтаксису
**Жадібні vs ліниві квантифікатори**
За замовчуванням квантифікатори (`*`, `+`, `{n,m}`) є *жадібними* - вони захоплюють якомога більше. Додай `?`, щоб зробити їх *лінивими* - захоплювати якомога менше.
```python
import re
html = "<b>bold</b> and <i>italic</i>"
re.findall(r"<.+>", html) # жадібний -> ['<b>bold</b> and <i>italic</i>']
re.findall(r"<.+?>", html) # лінивий -> ['<b>', '</b>', '<i>', '</i>']
```
Поширена помилка: використання `.+` всередині кутових дужок і захоплення надто багато. Завжди питай: чи має це зупинитися на першій можливій позиції?
**re.VERBOSE - читабельні складні патерни**
Режим verbose ігнорує пробіли та коментарі `#` всередині патерну:
```python
date_pat = re.compile(r'''
(?P<year> \d{4} ) # 4-цифровий рік
-
(?P<month> \d{2} ) # 2-цифровий місяць
-
(?P<day> \d{2} ) # 2-цифровий день
''', re.VERBOSE)
m = date_pat.search("Дедлайн: 2024-12-31")
m.group("year") # "2024"
m.group("month") # "12"
m.group("day") # "31"
```
Використовуй VERBOSE для будь-якого патерну довшого за ~40 символів.
**Довідник синтаксису патернів**
```
. будь-який символ крім нового рядка (re.DOTALL включає новий рядок)
^ початок рядка (або рядка в тексті з re.MULTILINE)
$ кінець рядка
\d цифра [0-9]
\D не-цифра
\w символ слова [a-zA-Z0-9_]
\W не-символ слова
\s пробільний символ (пробіл, табуляція, новий рядок)
\S не-пробільний символ
* 0 або більше (жадібний) *? лінивий
+ 1 або більше (жадібний) +? лінивий
? 0 або 1
{n} рівно n
{n,m} від n до m (жадібний) {n,m}? лінивий
[abc] клас символів: a, b або c
[^abc] негативний клас: все крім a, b, c
[a-z] діапазон
(...) захоплююча група
(?:...) не-захоплююча група
(?P<name>...) іменована захоплююча група
| чергування: a|b збігається з a або b
\ екранує наступний символ
```
**Типові пастки**
- `.` збігається з будь-яким символом - використовуй `\.` для літеральної крапки
- `^` та `$` поводяться інакше з `re.MULTILINE` - перевіряй явно
- `re.match` перевіряє лише початок; `re.fullmatch` перевіряє весь рядок - не плутай їх при валідації
- Відкат у складних патернах може бути повільним - тестуй на довгих рядках, якщо важлива швидкодія
Використайте `re.match`, щоб перевірити, чи є рядок дійсним ідентифікатором Python (літери, цифри, підкреслення; повинен починатися з літери або підкреслення).
import re
def is_identifier(s):
# ваш код тут
pass
print(is_identifier('my_var')) # True
print(is_identifier('_private')) # True
print(is_identifier('2bad')) # False
print(is_identifier('hello world')) # False
Рішення
import re
def is_identifier(s):
return bool(re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', s))
Використайте групи захоплення, щоб витягти рік, місяць і день з рядка дати у форматі 'DD/MM/YYYY'.
import re
def parse_date(s):
# Повернути (day, month, year) як цілі числа або None при невірному форматі
pass
print(parse_date('15/03/2024')) # (15, 3, 2024)
print(parse_date('01/12/1999')) # (1, 12, 1999)
print(parse_date('invalid')) # None
Рішення
import re
def parse_date(s):
m = re.match(r'^(\d{2})/(\d{2})/(\d{4})$', s)
if not m:
return None
return int(m.group(1)), int(m.group(2)), int(m.group(3))
Використайте `re.sub` з функцією для подвоєння всіх чисел, знайдених у рядку.
import re
def double_numbers(s):
# ваш код тут
pass
print(double_numbers('I have 3 cats and 12 dogs')) # 'I have 6 cats and 24 dogs'
print(double_numbers('price: 9.99')) # 'price: 19.98'
Скомпілюйте шаблон regex один раз і використайте його для фільтрації списку рядків, залишаючи лише ті, що відповідають шаблону номера телефону: `+X-XXX-XXX-XXXX` (де X — цифра).
import re
def filter_phones(strings):
# ваш код тут
pass
items = ['+1-800-555-1234', 'not a phone', '+44-020-555-9876', '123-456']
print(filter_phones(items)) # ['+1-800-555-1234', '+44-020-555-9876']
Рішення
import re
def filter_phones(strings):
pattern = re.compile(r'^\+\d+-\d{3}-\d{3}-\d{4}$')
return [s for s in strings if pattern.match(s)]
Використайте іменовані групи для розбору рядка логу у форматі `[LEVEL] YYYY-MM-DD: message`. Поверніть словник з ключами 'level', 'date' і 'message'.
import re
def parse_log(line):
# ваш код тут
pass
print(parse_log('[ERROR] 2024-03-15: Connection refused'))
# {'level': 'ERROR', 'date': '2024-03-15', 'message': 'Connection refused'}
print(parse_log('[INFO] 2024-01-01: Server started'))
# {'level': 'INFO', 'date': '2024-01-01', 'message': 'Server started'}
Рішення
import re
def parse_log(line):
m = re.match(r'^\[(?P<level>\w+)\] (?P<date>\d{4}-\d{2}-\d{2}): (?P<message>.+)$', line)
return m.groupdict() if m else None
Використайте `re.split` для розбивки рядка на будь-який знак пунктуації (`.`, `,`, `!`, `?`, `;`, `:`). Відфільтруйте порожні рядки з результату.
import re
def split_on_punctuation(s):
# ваш код тут
pass
print(split_on_punctuation('Hello, world! How are you?'))
# ['Hello', ' world', ' How are you']
print(split_on_punctuation('one.two;three:four'))
# ['one', 'two', 'three', 'four']
Рішення
import re
def split_on_punctuation(s):
return [part for part in re.split(r'[.,!?;:]', s) if part]
Використайте прапор `re.VERBOSE` (або `re.X`) для написання зрозумілого regex з коментарями, що перевіряє надійний пароль: мінімум 8 символів, мінімум одна велика, одна мала, одна цифра, один спеціальний символ (`!@#$%^&*`).
import re
def is_strong_password(s):
# ваш код тут
pass
print(is_strong_password('Abc@1234')) # True
print(is_strong_password('abcdefgh')) # False (немає великої/цифри/спец.)
print(is_strong_password('Abc@12')) # False (занадто короткий)
Рішення
import re
def is_strong_password(s):
pattern = re.compile(r'''
^ # початок рядка
(?=.*[A-Z]) # мінімум одна велика
(?=.*[a-z]) # мінімум одна мала
(?=.*\d) # мінімум одна цифра
(?=.*[!@#$%^&*]) # мінімум один спецсимвол
.{8,} # мінімум 8 символів загалом
$ # кінець рядка
''', re.VERBOSE)
return bool(pattern.match(s))
No split tab
Налаштування cookies
Ми використовуємо необхідні cookies для роботи сайту. З вашого дозволу ми також можемо зберігати налаштування сайту та використовувати аналітичні й рекламні cookies, щоб розуміти використання сайту й підтримувати розвиток проєкту.
* Ви завжди можете змінити свій вибір у налаштуваннях сайту.
Оберіть категорії cookies
Налаштування аналітики
Можна вимкнути аналітику використання платформи. Також можна надіслати в Google Analytics запит на видалення даних про використання цього сайту, пов'язаних із цим браузером.
Навчальний workspace
Навчайтеся, читаючи, запускаючи код і розв'язуючи задачі.
Практикуйте програмування з поясненнями тем, вправами, інструментами browser IDE, перевіркою regex і тренуванням друку коду в одному workspace.
Відкривайте інструменти у вкладках.Вправи, IDE-інструменти й тренажери залишаються доступними як вкладки сайту.
Перемикайтеся без втрати контексту.Переходьте між поясненнями, кодом та утилітами, зберігаючи своє місце.
Використовуйте sidebar як карту.Ліві панелі містять навігацію, налаштування, файли, libraries та керування інструментами.
PythonJavaScriptSQLite
Одна IDE, три практичні режими
Python у браузері.Запускайте невеликі скрипти, пробуйте бібліотеки й тренуйте API-запити без встановлення.
JavaScript для швидких експериментів.Перевіряйте код для браузера й порівнюйте ідеї поруч із навчальними матеріалами.
SQLite для практики з даними.Відкривайте оглядач бази даних, переглядайте таблиці, пишіть запити й вивчайте SQL локально.
ТемаIDE
Працюйте поруч у split tabs
Тримайте інструкції перед очима.Відкрийте вправу або довідкову сторінку поруч з IDE, замість постійних перемикань.
Порівнюйте інструменти під час навчання.Розміщуйте перевірки regex, пояснення та експерименти з кодом поруч, коли це потрібно для задачі.
Закрийте split, коли завершите.Workspace повернеться до однієї сфокусованої вкладки, а відкриті вкладки сайту залишаться доступними.
Тренажер друку коду
Або звичайного тексту
Тренажер розрахований на фізичну клавіатуру.Відкрийте цей розділ на ноутбуці або комп'ютері з широким екраном. На телефоні тренування друку не працюватиме коректно.
Швидкість: 0 зн/хв
0 слів/хв
Найкраща швидкість (60с): 0 зн/хв
0 слів/хв
Помилки: 0
Загальний час: 0.0 с
Щоб тренувати сліпий друк, не підглядайте на фізичну клавіатуру.