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

Търсене и замяна

Проблем

Извлечението от банката

Банката ти праща текст с плащанията за месеца. Искаш всяка сума поотделно, за да ги събереш:

import re
statement = "Наем 450,00 € на 01.10, ток 87,35 € на 05.10, вода 23,10 € на 12.10"
print(re.fullmatch(r".*\d+,\d{2} €.*", statement) is not None)
Изход
True
fullmatch казва само „да, има сума някъде“. .* от двете страни е там, защото fullmatch иска шаблонът да покрие целия текст. re.sub би сменил сумите, но не ти ги дава. Трябва ти начин да намериш всяко съвпадение и да го вземеш.

Идеята

re.compile и finditer

За търсене модулът re има обект шаблон и обект съвпадение:

import re
statement = "Наем 450,00 € на 01.10, ток 87,35 € на 05.10, вода 23,10 € на 12.10"
amount = re.compile(r"\d+,\d{2} €")
for m in amount.finditer(statement):
print(m.group())
Изход
450,00 €
87,35 €
23,10 €
  • re.compile(...) превръща шаблона в обект Pattern. Прави го веднъж и го ползвай колкото пъти искаш.
  • amount.finditer(statement) дава съвпаденията едно по едно, отляво надясно. Затова е във for: всяко завъртане получава следващото, докато свършат.
  • m е обект Match: едно намерено съвпадение. m.group() е неговият текст.
По-точно, за любопитните
  • re.findall дава направо списък с текстовете на съвпаденията, без обектите Match.
  • re.search дава само първото съвпадение, където и да е, или None. re.match проверява само дали текстът започва със съвпадение: за „Маса 12“ дава None, макар че 12 е в текста.
  • re.sub, re.split и re.fullmatch сами компилират шаблона и пазят последните шаблони наготово. re.compile е удобен, когато един шаблон се ползва на много места.
import re
statement = "Наем 450,00 € на 01.10, ток 87,35 € на 05.10, вода 23,10 € на 12.10"
print(re.findall(r"\d+,\d{2} €", statement))
print(re.match(r"\d+", "Маса 12"))
print(re.match(r"\d+", "12 маси").group())
Изход
['450,00 €', '87,35 €', '23,10 €']
None
12

Виж го

Къде е съвпадението

Освен текста, съвпадението знае и къде е намерено:

import re
log = "ERROR диск; INFO старт; ERROR мрежа"
count = 0
for m in re.finditer("ERROR", log):
count += 1
print(f"{m.group()} от {m.start()} до {m.end()}")
print(f"Общо: {count}")
Изход
ERROR от 0 до 5
ERROR от 24 до 29
Общо: 2
m.start() е индексът на първия символ на съвпадението, а m.end() е индексът след последния, както при резен. Затова log[m.start():m.end()] е същото като m.group().

Пробвай

Цифра или число?

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

import re
for m in re.finditer(r"\d+", "Маса 12, 4 души, 2 бутилки"):
print(f"[{m.group()}]", end="")
print()
Отговор

Идеята

Групи: части от съвпадението

Скобите в шаблона правят група. Съвпадението помни какво е съвпаднало с всяка група поотделно:

import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
m = date.search("Падеж: 15.11.2026")
if m:
print(m.group())
print(m.group(1) + " / " + m.group(2) + " / " + m.group(3))
print(m.group(3) + "-" + m.group(2) + "-" + m.group(1))
Изход
15.11.2026
15 / 11 / 2026
2026-11-15
  • Групите се номерират по отварящите скоби, отляво надясно, от 1.
  • m.group(1) е група 1. group(0) или group() е цялото съвпадение.
  • search е за случаите, когато ти трябва само първото съвпадение. Ако няма такова, m е None и if m: го прескача.

Така от една дата вземаш поотделно деня, месеца и годината и ги нареждаш както ти трябва.

import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
m = date.search("Падеж: 15.11.2026")
if m:
print(m.group())
print(m.group(1) + " / " + m.group(2) + " / " + m.group(3))
print(m.group(3) + "-" + m.group(2) + "-" + m.group(1))
Стъпка по стъпка, с думи
  1. Шаблонът за дата има три двойки скоби: три групи.
  2. search търси шаблона в текста. Ако го намери, m е съвпадението (Match), иначе None.
  3. search намира датата: от индекс 7 до 17.
  4. Всяка група помни своята част: група 1 е 15, група 2 е 11, група 3 е 2026.
  5. Частите стоят в m, всяка под своя номер. group(0) е цялото съвпадение.
  6. group() е същото като group(0): 15.11.2026.
  7. Трите групи поотделно.
  8. Наредени в обратен ред, частите дават 2026-11-15.
По-точно, за любопитните
  • (?:...) е група, която само държи частите заедно, без да ги помни. Например (?:\+359|0)8[789]\d{7} не прави група 1.
  • date.groups дава броя на групите в шаблона.
  • Ако група не е участвала в съвпадението (например (,\d{2})? я е нямало), групата е None.
import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
print(date.groups)
m = re.search(r"(\d+)(,\d{2})? €", "Бакшиш 5 €")
print(m.group(1), m.group(2))
Изход
3
5 None

Идеята

Групи с имена и замяна

Когато групите са много, номерата се бъркат. Можеш да ги кръстиш: (?P<име>...), а после m.group("име"). В Python има P след въпросителния знак: записът (?<име>...) от Java и JavaScript в Python 3.12 е грешка. Ред от сметка в кафене:
import re
item = re.compile(r"(?P<qty>\d+) x (?P<name>[А-Яа-я ]+) по (?P<price>\d+\.\d{2})")
for m in item.finditer("2 x Кафе с мляко по 3.20; 1 x Кроасан по 2.90"):
qty = int(m.group("qty"))
price = float(m.group("price"))
print(f"{m.group('name')}: {qty * price} €")
Изход
Кафе с мляко: 6.4 €
Кроасан: 2.9 €
Групите работят и в re.sub: в текста за замяна \1 е група 1, а \g<име> е групата с това име. Текстът за замяна също е суров низ, заради \. Датите от български към ISO вид и към американски:
import re
text = "Срок: 15.11.2026, напомняне: 08.11.2026"
print(re.sub(r"(\d{2})\.(\d{2})\.(\d{4})", r"\3-\2-\1", text))
print(re.sub(r"(?P<d>\d{2})\.(?P<m>\d{2})\.(?P<y>\d{4})", r"\g<m>/\g<d>/\g<y>", text))
Изход
Срок: 2026-11-15, напомняне: 2026-11-08
Срок: 11/15/2026, напомняне: 11/08/2026
Тук не ти трябват re.compile и finditer: re.sub разбира групите сам.
По-точно, за любопитните
  • \ в текста за замяна е специален. Ако текстът идва от потребителя, например път до папка, re.sub може да спре с грешка. Удвои обратните черти в него с replace("\\", "\\\\"):
import re
user = "C:\\Temp"
try:
re.sub("ПАПКА", user, "Копие в ПАПКА")
except re.error as e:
print(e)
print(re.sub("ПАПКА", user.replace("\\", "\\\\"), "Копие в ПАПКА"))
Изход
bad escape \T at position 2
Копие в C:\Temp
  • Групата може да се използва и в самия шаблон: \1 значи „същото като група 1“. Така се хващат повторени думи. Виж картата за флаговете.

Пробвай

Колко взима +?

Искаш всички HTML етикети от реда:

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

import re
html = "<b>Цена:</b> <i>12 €</i>"
for m in re.finditer(r"<.+>", html):
print(m.group())
Отговор

Идеята

Жадно и нежадно

? след квантификатор го прави нежаден: той взима колкото може по-малко и спира при първата възможност.

import re
html = "<b>Цена:</b> <i>12 €</i>"
for m in re.finditer(r"<.+?>", html):
print(m.group())
print(re.sub(r"<[^>]+>", "", html))
Изход
<b>
</b>
<i>
</i>
Цена: 12 €
  • <.+?> е „<, най-малко един символ и първото > след него“.
  • <[^>]+> прави същото по друг начин: „<, символи, които не са >, и >“. Той не може да прескочи >, затова е и по-бърз. С него махаш етикетите и остава само текстът.
Жаден Нежаден
* *?
+ +?
? ??
{2,5} {2,5}?

Пробвай

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

Програмата трябва да отпечата трите номера на поръчки, а отпечатва само #1042. На кой ред е грешката?

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

Идеята

Флагове и редове

Флаговете се дават като втори аргумент на re.compile, събрани с |. От лог файл с няколко реда вземаш съобщенията за грешки, написани с главни или малки букви:

import re
log = "INFO: Старт\nERROR: Няма връзка с базата\nINFO: Повторен опит\nerror: Диск 95%"
p = re.compile(r"^error: (.+)$", re.IGNORECASE | re.MULTILINE)
for m in p.finditer(log):
print(m.group(1))
Изход
Няма връзка с базата
Диск 95%
  • re.IGNORECASE е същото като (?i): главните и малките букви са еднакви.
  • re.MULTILINE прави ^ и $ начало и край на всеки ред. Без него те са само началото и краят на целия текст и шаблонът няма да намери нищо.
  • re.DOTALL позволява на . да хваща и нов ред.
По-точно, за любопитните

Две неща, които ще срещнеш в чужди шаблони:

  • Обратна препратка (backreference) \1: „същият текст като група 1“. Така се хващат повторени думи.
  • Поглед напред (lookahead) (?=...): „оттук нататък някъде има ...“, без да взима символи. Няколко такива един след друг са удобни за пароли: „има цифра“ и „има главна буква“, и поне 8 символа.
import re
twice = re.compile(r"\b(\w+) \1\b")
for m in twice.finditer("Плащането е е прието, моля моля проверете."):
print("Повторено: " + m.group(1))
strong = r"(?=.*\d)(?=.*[A-Z]).{8,}"
print(re.fullmatch(strong, "parola12") is not None, re.fullmatch(strong, "Parola12") is not None)
Изход
Повторено: е
Повторено: моля
False True

(?!...) е обратното: „оттук нататък няма ...“. Има и поглед назад: (?<=...) и (?<!...).

Упражнение

Намери, извлечи, замени

Реши задачите в редактора си. Текстът е на един ред от клавиатурата. В урока „Файлове“ ще ги направиш и с файл.

1. Намери и замени. Замени думата „старо“ с „ново“ навсякъде, където е цяла дума, и кажи колко замени си направил. Части от други думи („старомодно“) не се пипат. re.subn прави същото като re.sub, но връща двойка: новия текст и броя замени.

Вход
Това е старо съдържание. Още старо съдържание, не старомодно.
Изход
Това е ново съдържание. Още ново съдържание, не старомодно.
Замени: 2

2. Думи с префикс. На първия ред е префиксът, на втория текстът. Отпечатай всяка дума, която започва с префикса.

Вход
app
apple application banana appreciate snapple
Изход
apple
application
appreciate

3. Адреси в текст. Извади всички уеб адреси, които започват с http:// или https://. Точката в края на изречението не е част от адреса.

Вход
Виж сайта https://www.example.com и този: http://www.test.com. Документите са на https://docs.example.com/guide/start.
Изход
https://www.example.com
http://www.test.com
https://docs.example.com/guide/start

4. Дати към ISO. Превърни всяка дата дд.мм.гггг в текста във вида гггг-мм-дд с един re.sub.

Вход
Договор от 03.10.2026, плащане до 15.10.2026.
Изход
Договор от 2026-10-03, плащане до 2026-10-15.

Още задачи

  • Сбор от извлечението. Вземи сумите от първата карта с група за еврото и група за центовете, събери ги и отпечатай „Общо: 560,45 €“.
  • Хаштагове. Извади всички думи, които започват с #, от публикация на кирилица и латиница. Помисли какво би станало с флага re.ASCII.
  • Без етикети. Махни HTML етикетите от <p>Срокът е <b>15.11</b>.</p> и отпечатай чистия текст.
  • Телефони в текст. Намери всички мобилни номера в текст и ги отпечатай във вида +359 88 812 3456 с групи и re.sub.

Тест

Провери се

1. Какво дава re.finditer(шаблон, текст)?
2. Шаблонът е (\d{2})\.(\d{2})\.(\d{4}), а текстът е 15.11.2026. Какво е m.group(2)?
3. Какво дава re.sub(r"(\d+)\.(\d+)\.(\d+)", r"\2/\1", "05.03.2026")?
4. Защо "<.+>" хваща целия текст "<b>Цена</b>", а не само "<b>"?
5. Какво прави re.MULTILINE?

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