Към съдържанието
wannacode

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

Проблем

Десет реда за една дата

Формулярът за резервация иска дата във вида дд.мм.гггг. Проверяваш го символ по символ:

def is_date(s):
if len(s) != 10:
return False
for i in range(len(s)):
c = s[i]
if i == 2 or i == 5:
if c != ".":
return False
elif c < "0" or c > "9":
return False
return True
print(is_date("05.03.2026"))
print(is_date("5.3.2026"))
print(is_date("05/03/2026"))
Изход
True
False
False

Десет реда, а функцията проверява само формата: две цифри, точка, две цифри, точка, четири цифри. Ако искаш да приемаш и / вместо точка, или ден с една цифра, ще добавиш още толкова. Номер на телефон, имейл или регистрационен номер ще ти струват по още една такава функция.

Идеята

Шаблон вместо код

Описваш как изглежда текстът, а Python проверява:

import re
def is_date(s):
return re.fullmatch(r"\d{2}\.\d{2}\.\d{4}", s) is not None
print(is_date("05.03.2026"))
print(is_date("5.3.2026"))
print(is_date("05/03/2026"))
Изход
True
False
False

Шаблонът се чете отляво надясно: \d е цифра, {2} значи „два пъти“, \. е точка. Такъв шаблон се казва регулярен израз (regular expression, regex).

Регулярните изрази са в модула re, затова файлът започва с import re. re.fullmatch(шаблон, текст) проверява дали целият текст отговаря на шаблона, от първия до последния символ. Ако да, връща съвпадението, ако не, връща None. Затова is not None дава True или False.

Защо пред кавичките има r? Обратната наклонена черта е специална и за обикновения низ в Python. С r низът е суров (raw) и \ в него е просто \. Ще го видим подробно след малко. Засега пиши всеки шаблон с r.

Виж го

Как шаблонът търси

re.sub не проверява целия текст, а търси парчетата, които отговарят на шаблона, и ги заменя. Ето как минава по реда:

import re
line = "120 € на 05.03.2026, падеж 15.04.2026"
masked = re.sub(r"\d{2}\.\d{2}\.\d{4}", "[дата]", line)
print(masked)
Стъпка по стъпка, с думи
  1. Редът е поредица от знаци, всеки със своя позиция.
  2. От позиция 0: „1“ и „2“ са цифри, но после идва „0“ вместо точка. Опитът пада.
  3. Нов опит от всеки следващ знак: „20 “, „0 “, „ €“… все неуспехи.
  4. От позиция 9 всичките десет части пасват: първото съвпадение.
  5. Търсенето продължава след съвпадението, от запетаята, и намира второто.
  6. re.sub заменя двете съвпадения с „[дата]“.

Шаблонът се пробва от всяка позиция поред. Щом някоя част не съвпадне, опитът пада и започва нов от следващия символ. Затова 120 не мина: две цифри, а после 0 вместо точка. Когато намери съвпадение, търсенето продължава след него.

Идеята

Класове от символи

В квадратни скоби изброяваш кои символи са позволени на едно място. Регистрационен номер на кола е една или две букви, четири цифри и две букви:

import re
plates = ["СА1234ВХ", "В7788КК", "СА12345ВХ", "са1234вх"]
for plate in plates:
print(plate, re.fullmatch(r"[А-Я]{1,2}\d{4}[А-Я]{2}", plate) is not None)
Изход
СА1234ВХ True
В7788КК True
СА12345ВХ False
са1234вх False
Шаблон Съвпада с един символ
[АВЕ] А, В или Е
[0-9], [А-Я] от интервала: цифра, главна буква на кирилица
[^0-9] всичко освен цифра (^ в началото на скобите)
\d цифра, същото като [0-9]
\s интервал, табулация, нов ред
\w буква от всеки език, цифра или _
. какъвто и да е символ (без нов ред)

Главните букви обръщат смисъла: \D е „не цифра“, \S „не интервал“, \W „не \w“.

В Python \w познава и кирилицата. С флага re.ASCII (трети аргумент на fullmatch) става само латиница, както е в Java и JavaScript:

import re
print(re.fullmatch(r"\w+", "Sofia") is not None)
print(re.fullmatch(r"\w+", "София") is not None)
print(re.fullmatch(r"\w+", "София", re.ASCII) is not None)
print(re.fullmatch(r"[А-Яа-я]+", "София") is not None)
Изход
True
True
False
True
По-точно, за любопитните
  • [А-Я] е интервал по кодовете на символите. В него влизат и руските Ы и Э, но не и Ё, която стои отделно в таблицата. За българския текст [А-Яа-я] стига. Ако те интересува и „ѝ“, добави го: [А-Яа-яѝЍ].
  • Истинските български номера ползват само 12 букви, които изглеждат еднакво на кирилица и латиница: [АВЕКМНОРСТУХ].

Пробвай

Точката

Някой е забравил обратната черта пред точките:

Какво ще отпечата програмата?

import re
pattern = r"\d{2}.\d{2}.\d{4}"
print(re.fullmatch(pattern, "05.03.2026") is not None)
print(re.fullmatch(pattern, "05-03-2026") is not None)
print(re.fullmatch(pattern, "05.03.26") is not None)
Отговор

Идеята

Колко пъти

След символ или клас казваш колко пъти може да се повтори. Това е квантификатор:

Квантификатор Значи
? нула или едно (може да го има, може да го няма)
* нула или повече
+ едно или повече
{4} точно 4
{1,2} от 1 до 2
{2,} 2 или повече

Мобилен номер започва с 0 или +359, после 87, 88 или 89 и още 7 цифри:

import re
mobile = r"(\+359|0)8[789]\d{7}"
print(re.fullmatch(mobile, "0888123456") is not None)
print(re.fullmatch(mobile, "+359877654321") is not None)
print(re.fullmatch(mobile, "0888 123 456") is not None)
print(re.fullmatch(mobile, "088812345") is not None)
Изход
True
True
False
False

| значи „или“. Скобите държат \+359|0 заедно: без тях „или“ би разделило целия шаблон на две. Скобите правят още неща, които ще видим в следващия урок. + в началото е със \, защото иначе е квантификатор.

Квантификаторът важи и за цяла група в скоби. Цена с дробна част или без нея:

import re
price = r"\d+([.,]\d{1,2})? €"
print(re.fullmatch(price, "12 €") is not None)
print(re.fullmatch(price, "12.5 €") is not None)
print(re.fullmatch(price, "12,50 €") is not None)
print(re.fullmatch(price, ".50 €") is not None)
Изход
True
True
True
False

([.,]\d{1,2})? значи: „точка или запетая и една-две цифри“, а целият този блок може да го няма.

Идеята

Суров низ: r"…"

Шаблонът минава през два слоя. Първо Python чете низа в кавичките, после re чете шаблона.

В обикновен низ \ започва специален символ: \n е нов ред, \" е кавичка. \d не е такъв. Python 3.12 предупреждава, но оставя \d както е, и програмата работи:

import re
date = "05.03.2026"
print(re.fullmatch("\d{2}\.\d{2}\.\d{4}", date) is not None)
Конзолата
/shop/date.py:4: SyntaxWarning: invalid escape sequence '\d'
print(re.fullmatch("\d{2}\.\d{2}\.\d{4}", date) is not None)
True

Работи случайно. \b например е специален символ в низа („връщане назад“, backspace), и шаблон без r с него тихо търси друго. Ще го видиш след малко. Затова шаблоните се пишат като сурови низове: с r пред кавичките \ е просто \:

import re
date = "05.03.2026"
print(re.fullmatch(r"\d{2}\.\d{2}\.\d{4}", date) is not None)
print(re.fullmatch(r"C:\\Temp", "C:\\Temp") is not None)
Изход
True
True
Искаш в шаблона Пишеш в Python Без r
\d (цифра) r"\d" "\\d"
\. (истинска точка) r"\." "\\."
\\ (истинска обратна черта) r"\\" "\\\\"

Символите, които трябва да са със \, когато ги искаш буквално, са . * + ? ( ) [ ] { } | ^ $ \.

Пробвай

Намери грешката

Програмата трябва да отпечата „12 € и 50 цента“, а спира така:
Грешка
Traceback (most recent call last):
File "/shop/price.py", line 5, in <module>
euros = int(parts[0])
^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: ''

Избери реда с грешката.

price = "12.50"
print(price.split("."))
Изход
['12', '50']

Идеята

Замяна и разделяне по шаблон

re.sub(шаблон, замяна, текст) и re.split(шаблон, текст) работят с шаблон. Така чистиш и разбиваш текст, който хората пишат както им падне:

import re
address = "Доставка: ул. Шипка 12, вход Б"
print(re.sub(r"\s+", " ", address))
items = re.split(r"[,;\s]+", "хляб, мляко;кафе чай")
print(f"{len(items)}: " + " | ".join(items))
print(re.sub(r"(?i)summer\d+", "***", "Код SUMMER10, код summer25"))
Изход
Доставка: ул. Шипка 12, вход Б
4: хляб | мляко | кафе | чай
Код ***, код ***
  • \s+ е „един или повече интервала“: всяка група интервали става един.
  • [,;\s]+ е „запетаи, точки и запетаи или интервали, колкото и да са“: разделителят може да е всеки от тях.
  • (?i) в началото е флаг: шаблонът не гледа главни и малки букви.

Методът на текста replace също заменя всички появи, но търси обикновен текст, не шаблон. Ако не ти трябва шаблон, ползвай него: не трябва да мислиш за точки и обратни черти.

\b е граница на дума: мястото между буква и нещо, което не е буква. С него заменяш само цели думи. В Python \b познава кирилицата, но шаблонът трябва да е суров низ:

import re
text = "стар и старо"
print(re.sub("стар", "нов", text))
print(re.sub("\bстар\b", "нов", text))
print(re.sub(r"\bстар\b", "нов", text))
Изход
нов и ново
стар и старо
нов и старо

Без \b „старо“ става „ново“. На втория ред няма r: там \b е символът backspace, а не граница, затова не се заменя нищо и грешка няма. Едва със суровия низ на третия ред се заменя само думата „стар“.

По-точно, за любопитните
  • \B е обратното на \b: място вътре в дума. r"\Bстар" намира „стар“ само когато не е в началото на дума: в „застаря“, но не в „старо“.
  • ^ е началото на текста, а $ краят: re.sub(r"^0+", "", text) маха водещите нули. При fullmatch не ти трябват, защото той и без това гледа целия текст.
  • Флаговете може да се дадат и като константи: re.IGNORECASE е същото като (?i), re.ASCII като (?a). Подават се като последен аргумент: re.sub(шаблон, замяна, текст, flags=re.IGNORECASE).
  • re.split("|", "a|b") е капан като точката: | е „или“ и шаблонът е празен, затова дели между всеки два символа. Методът на текста "a|b".split("|") търси обикновен текст и няма този проблем.
import re
print(re.findall(r"\Bстар", "застаря старо"))
print(re.fullmatch(r"(?a)\w+", "София"), re.fullmatch(r"(?i)стар", "СТАР") is not None)
print(re.split("|", "a|b"))
print("a|b".split("|"))
Изход
['стар']
None True
['', 'a', '|', 'b', '']
['a', 'b']

Пробвай

Пощенски код

Българският пощенски код е точно четири цифри. Какво липсва, за да отпечата програмата True за "1000" и False за "100" или "10000"?

import re

code = "1000"
print(re.fullmatch(r"", code) is not None)

Упражнение

Проверки с шаблон

Реши задачите в редактора си. Чети редове, докато има: for line in sys.stdin:, с import sys най-горе. Всеки ред завършва с "\n", затова го махни със strip() или rstrip("\n"). В урока „Файлове“ ще четеш същите редове от файл.

1. Дата. Провери всяка дата дали е във вида дд/мм/гггг. Денят е от 01 до 31, а месецът от 01 до 12. Подсказка: денят е 0[1-9] или [12]\d, или 3[01].

Вход
01/01/2020
32/01/2020
15/05/2021
Изход
01/01/2020 - Валидна
32/01/2020 - Невалидна
15/05/2021 - Валидна

2. Мобилен номер. Хората пишат номера с интервали и тирета. Махни ги с re.sub и провери това, което остава, с шаблона за мобилен номер от урока.

Вход
0888 123 456
+359 87 765 4321
02 812 3456
0899-12-345
Изход
0888 123 456 - валиден
+359 87 765 4321 - валиден
02 812 3456 - невалиден
0899-12-345 - невалиден

3. Имейл. Провери дали всеки ред прилича на имейл: една или повече латински букви, цифри, ., + или -, после @, после име на домейн с поне една точка (firma.bg, mail.firma.bg). Тук \w би пуснал и кирилица: изброй латиницата с [A-Za-z0-9_].

Вход
elena.koleva@firma.bg
elena@firma
@firma.bg
elena koleva@firma.bg
Изход
elena.koleva@firma.bg - да
elena@firma - не
@firma.bg - не
elena koleva@firma.bg - не

4. Списък за пазаруване. Продуктите са разделени със запетаи, точки и запетаи, вертикални черти или интервали, понякога по няколко наведнъж. Отпечатай ги номерирани.

Вход
хляб, мляко;кафе чай|сирене
Изход
1. хляб
2. мляко
3. кафе
4. чай
5. сирене
По-точно, за любопитните
  • Шаблонът за дата приема и 31/02/2026. Дали денят съществува в месеца, regex не може да каже разумно. За това има модулът datetime, а шаблонът е само първото сито:
import datetime
try:
datetime.date(2026, 2, 31)
except ValueError as e:
print(e)
Изход
day is out of range for month
  • Пълното правило за имейл адрес (RFC 5322) позволява кавички, коментари и интервали в определени места. Шаблон, който го покрива изцяло, е стотици символи и никой не го поддържа. Истинската проверка е да изпратиш писмо с връзка за потвърждение.

Още задачи

  • Парола. Провери дали паролата е поне 8 символа и има поне една цифра. Направи го с два re.fullmatch: r".{8,}" и r".*\d.*".
  • Час. Провери час във вида чч:мм от 00:00 до 23:59.
  • Само цифри. От "Тел.: +359 (88) 812-34-56" остави само цифрите и + с един re.sub и [^...].

Тест

Провери се

1. Какво проверява re.fullmatch(r"\d{4}", "2026")?
2. С кое съвпада . (точка) в шаблон?
3. Кое значи „едно или повече“?
4. Съвпада ли "София" с \w+ в Python?
5. Колко части дава re.split(r"[,;]\s*", "хляб, мляко;кафе")?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·