Търсене и замяна
Проблем
Извлечението от банката
Банката ти праща текст с плащанията за месеца. Искаш всяка сума поотделно, за да ги събереш:
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)Truefullmatch казва само „да, има сума някъде“. .* от двете страни е там, защото 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 €']None12Виж го
Къде е съвпадението
Освен текста, съвпадението знае и къде е намерено:
import re
log = "ERROR диск; INFO старт; ERROR мрежа"count = 0for m in re.finditer("ERROR", log): count += 1 print(f"{m.group()} от {m.start()} до {m.end()}")print(f"Общо: {count}")ERROR от 0 до 5ERROR от 24 до 29Общо: 2m.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()\d+ е „една или повече цифри“, затова „12“ е едно съвпадение. Със \d без + всяка цифра би била отделно: [1][2][4][2]. Интервалът и запетаята прекъсват поредицата, затова 12 и 4 не се сливат.
Идеята
Групи: части от съвпадението
Скобите в шаблона правят група. Съвпадението помни какво е съвпаднало с всяка група поотделно:
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.202615 / 11 / 20262026-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))Стъпка по стъпка, с думи
- Шаблонът за дата има три двойки скоби: три групи.
- search търси шаблона в текста. Ако го намери, m е съвпадението (Match), иначе None.
- search намира датата: от индекс 7 до 17.
- Всяка група помни своята част: група 1 е 15, група 2 е 11, група 3 е 2026.
- Частите стоят в m, всяка под своя номер. group(0) е цялото съвпадение.
- group() е същото като group(0): 15.11.2026.
- Трите групи поотделно.
- Наредени в обратен ред, частите дават 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))35 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/2026re.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. На кой ред е грешката?
Избери реда с грешката.
search намира само първото съвпадение. За всички трябва for m in order.finditer(...): всяко завъртане продължава оттам, където е спряло предното.Идеята
Флагове и редове
Флаговете се дават като втори аргумент на 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, но връща двойка: новия текст и броя замени.
Това е старо съдържание. Още старо съдържание, не старомодно.Това е ново съдържание. Още ново съдържание, не старомодно.Замени: 22. Думи с префикс. На първия ред е префиксът, на втория текстът. Отпечатай всяка дума, която започва с префикса.
appapple application banana appreciate snappleappleapplicationappreciate3. Адреси в текст. Извади всички уеб адреси, които започват с http:// или https://. Точката в края на изречението не е част от адреса.
Виж сайта https://www.example.com и този: http://www.test.com. Документите са на https://docs.example.com/guide/start.https://www.example.comhttp://www.test.comhttps://docs.example.com/guide/start4. Дати към 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.
Тест