Примеры идут от простого к сложному: сначала отдельные приёмы (очистка строки, сборка шаблона, чтение JSON, дата, генератор), затем их связка — так же, как устроены реальные решения в заданиях практикума. Весь вывод в комментариях получен запуском кода, а не подобран по памяти.
Пример 1. re.compile + findall — база всех текстовых заданий
Во всех четырёх заданиях практикума текст сначала «фильтруется» регуляркой. Шаблон стоит скомпилировать один раз через re.compile, если он используется в цикле по строкам файла (см. урок 60 — Регулярные выражения).
import re
text = "Order #123, code: ABC-42, id: 007"
number_re = re.compile(r"\d+")
print(number_re.findall(text))
# ['123', '42', '007']
Что происходит: re.compile создаёт объект шаблона; findall возвращает список всех непересекающихся совпадений. Компиляция один раз вне цикла — так же быстрее, чем вызывать re.findall заново на каждой строке.
Пример 2. Очистка строки: re.sub(r"\D", "", ...)
Первый шаг нормализации номера в задании 2 — не пытаться угадать формат регуляркой, а сначала убрать всё лишнее.
import re
raw = "+49 157 1234-5678"
clean = re.sub(r"\D", "", raw)
print(clean)
# 4915712345678
Что происходит: \D — класс «не цифра» (пробел, дефис, табуляция, плюс). re.sub заменяет каждое совпадение на пустую строку, то есть вырезает его. После такой очистки с номером удобно работать как со строкой цифр — резать по индексам, проверять длину.
Пример 3. Собираем шаблон немецкого номера по частям
Задание 2 описывает номер как последовательность требований: код страны → префикс оператора → 7–9 цифр с необязательными разделителями. Каждое требование — это часть шаблона.
import re
phone_re = re.compile(r"(?:\+49|0049)[\s\-]*1[5-7](?:[\s\-]*\d){7,9}")
sample = "Call +49 157 12345678 or 0049-160-9876543 or +49 89 123456 (landline)"
print(phone_re.findall(sample))
# ['+49 157 12345678', '0049-160-9876543']
Что происходит: (?:\+49|0049) — негруппирующая альтернатива для кода страны (не создаёт отдельный элемент в результате). 1[5-7] — первые две цифры мобильного оператора (15/16/17). (?:[\s\-]*\d){7,9} повторяет «необязательный разделитель + цифра» от 7 до 9 раз. Городской номер +49 89 123456 в результат не попал — после 89 нет оператора 15/16/17.
Пример 4. Валидация после regex — фильтр первого приближения
Шаблон из примера 3 находит кандидатов, но задание 2 требует ещё проверить итоговую длину и привести номер к единому виду +49.... Это и есть приём «regex ищет, код — проверяет и нормализует».
def clean_and_validate(raw_match: str) -> str | None:
digits = re.sub(r"\D", "", raw_match)
if digits.startswith("0049"):
digits = digits[4:]
elif digits.startswith("49"):
digits = digits[2:]
if digits[:2] not in ("15", "16", "17"):
return None
if not (10 <= len(digits) <= 12):
return None
return f"+49{digits}"
candidates = phone_re.findall(sample)
valid = [n for n in (clean_and_validate(c) for c in candidates) if n]
print(valid)
# ['+4915712345678', '+491609876543']
Что происходит: сначала убираем код страны (0049 или 49), затем перепроверяем, что оставшийся префикс — оператор, и что длина укладывается в 10–12 цифр. Regex — фильтр первого приближения (см. теорию), окончательное решение принимает обычный код.
Пример 5. Границы слова в email: почему нельзя жадный класс символов
Задание 3 запрещает точку в начале и в конце локальной части email. \b сам по себе этого не гарантирует.
local = r"[a-zA-Z0-9_](?:[a-zA-Z0-9._]*[a-zA-Z0-9_])?"
email_re = re.compile(rf"\b{local}@[a-zA-Z0-9.-]+\.[a-zA-Z]{{2,3}}\b")
sample_emails = "ok: user.name@sub.domain.com, bad: .user@example.com, bad2: user.@example.com"
print(email_re.findall(sample_emails))
# ['user.name@sub.domain.com', 'user@example.com']
Что происходит: первый адрес корректный и найден целиком. Во втором случае (.user@example.com) граница \b стоит не перед точкой, а перед u — точка не входит в «слово», поэтому шаблон честно находит внутри строки корректный кусок user@example.com, игнорируя ведущую точку. А вот user.@example.com не совпал вовсе — локальная часть по построению не может заканчиваться точкой (см. разбор в «Ошибках»). Вывод тот же, что в примере 4: regex ловит форму, семантику иногда приходится доопределять шаблоном или кодом.
Пример 6. Номер карты: повторяющаяся группа (?:\d{4}[- ]?){3}\d{4}
Задание 4 — самое «геометрическое»: 16 цифр четвёрками, между которыми не более одного разделителя.
card_re = re.compile(r"\b(?:\d{4}[- ]?){3}\d{4}\b")
sample_cards = "1234 5678 9012 3456 and 1234--5678--9012--3456 and 1234567812345678"
print(card_re.findall(sample_cards))
# ['1234 5678 9012 3456', '1234567812345678']
Что происходит: группа (?:\d{4}[- ]?){3} повторяется ровно 3 раза, и после каждой четвёрки цифр допускает не более одного пробела или дефиса — квантификатор ? у [- ] разрешает максимум один разделитель. Последний блок \d{4} идёт без хвостового разделителя. Строка с двойным дефисом 1234--5678--... не подошла: после первого дефиса шаблон ждёт сразу цифру, а не второй дефис — это и есть проверка «без двойных разделителей» из условия задания.
Пример 7. JSON туда и обратно через pathlib
Задание 1 читает grades.json и пишет quarterly_report.json. Современный способ — без open()/close() вручную (см. урок 58 — Файлы JSON и datetime и урок 50 — Файловая система).
import json
from pathlib import Path
students = [
{"name": "Alice", "subject": "Math", "grade": 90, "date": "12-03-2025"},
{"name": "Alice", "subject": "Math", "grade": 70, "date": "20-04-2025"},
]
json_path = Path("students.json")
json_path.write_text(json.dumps(students, indent=2, ensure_ascii=False), encoding="utf-8")
loaded = json.loads(json_path.read_text(encoding="utf-8"))
print(loaded[0])
# {'name': 'Alice', 'subject': 'Math', 'grade': 90, 'date': '12-03-2025'}
Что происходит: write_text/read_text с явным encoding="utf-8" заменяют пару open(...)/close() — файл закрывается автоматически. Без явной кодировки на Windows можно получить UnicodeDecodeError (см. «Ошибки»). json.dumps/loads работают со строкой в памяти — удобно проверить формат перед записью на диск.
Пример 8. Дата → квартал: datetime.strptime и своя функция
Задание 1 требует ровно то разбиение на кварталы, что описано в условии, — не «логичное», а именно указанное.
from datetime import datetime
def get_quarter(date_str: str) -> str | None:
month = datetime.strptime(date_str, "%d-%m-%Y").month
if 1 <= month <= 3:
return "Q1"
if 4 <= month <= 5:
return "Q2"
if 9 <= month <= 12:
return "Q3"
return None # летние месяцы пропускаем
for date_str in ("12-03-2025", "20-04-2025", "06-09-2025"):
print(date_str, "->", get_quarter(date_str))
# 12-03-2025 -> Q1
# 20-04-2025 -> Q2
# 06-09-2025 -> Q3
Что происходит: strptime разбирает строку по явному формату "%d-%m-%Y" и возвращает объект datetime, у которого сразу есть .month. В исходном решении курса эти метки были перепутаны местами (январь–март помечены как Q2) — разбор в разделе «Ошибки».
Пример 9. Группировка без if: defaultdict(lambda: defaultdict(list))
Оценки нужно сгруппировать по ученику и кварталу одновременно — двухуровневый словарь без проверок «есть ли уже такой ключ» (см. урок 40 — Модуль collections).
from collections import defaultdict
entries = [
{"name": "Alice", "grade": 90, "date": "12-03-2025"},
{"name": "Alice", "grade": 70, "date": "20-04-2025"},
{"name": "Alice", "grade": 80, "date": "06-09-2025"},
]
by_quarter: defaultdict[str, list[int]] = defaultdict(list)
for row in entries:
quarter = get_quarter(row["date"])
if quarter:
by_quarter[quarter].append(row["grade"])
report = {q: round(sum(v) / len(v), 2) for q, v in by_quarter.items()}
print(report)
# {'Q1': 90.0, 'Q2': 70.0, 'Q3': 80.0}
Что происходит: defaultdict(list) создаёт пустой список сам при первом обращении к новому ключу — append не упадёт с KeyError. В настоящем задании 1 нужен ещё один уровень вложенности (по предмету), для этого используют фабрику defaultdict(lambda: defaultdict(list)) — она уже встречалась в справочнике. Средние значения считает генераторное выражение — без ручного накопления суммы и счётчика.
Пример 10. Генератор + regex: лениво читаем файл и сразу фильтруем
Финальная связка: генератор из урока 56 отдаёт строки файла по одной, а regex из примера 5 сразу отбирает нужное — не обязательно грузить весь файл в память, если он большой.
from pathlib import Path
def emails_from_file(path: Path):
with path.open(encoding="utf-8") as file:
for line in file:
for match in email_re.finditer(line):
yield match.group()
contacts = Path("contacts.txt")
contacts.write_text(
"Alice - alice@example.com\nnot an email line\nBob - bob.work@company.org\n",
encoding="utf-8",
)
print(list(emails_from_file(contacts)))
# ['alice@example.com', 'bob.work@company.org']
Что происходит: emails_from_file — функция с yield, она не читает файл целиком в память, а отдаёт совпадения по одной строке за раз. finditer возвращает итератор объектов Match; match.group() достаёт саму найденную строку. Эта связка «генератор построчного чтения + regex внутри» закрывает задания 2 и 3 в общем виде.
Что делать дальше
Переходите к заданиям — там те же приёмы нужно собрать в четыре полных решения. Если что-то не запустилось так, как в примерах, — сначала загляните в типичные ошибки, готовые решения — в разделе «Решения».
Как запустить в VS Code
- Создайте папку
python-fundamentals-practice/lesson-61/. - Создайте файлы данных (
grades.json,german_numbers.txt) и скрипты для каждого задания. - Откройте терминал в VS Code (Ctrl + `).
- Активируйте виртуальное окружение:
venv\Scripts\activate(Windows) илиsource venv/bin/activate(Mac/Linux). - Запустите:
python task_01_grades.py