✅ Решения заданий

⚠️ Рекомендация: попробуйте решить задания самостоятельно, прежде чем смотреть ответы. Решения — это последняя инстанция, когда собственные попытки исчерпаны.

⚡ Кратко

  • Задание 1: парсим даты, группируем оценки по кварталам через defaultdict.
  • Задание 2: ищем номера regex, очищаем цифрами, нормализуем к +49.
  • Задание 3: email ищем через \b и классы символов, проверяем домен.
  • Задание 4: карта — 16 цифр с необязательными пробелами/дефисами.

Решение 1. Анализ оценок по кварталам

Исходное решение из материалов курса:

import json
from datetime import datetime
from collections import defaultdict

# Определяем в какой квартал попадает месяц
def get_quarter(date_str):
    month = datetime.strptime(date_str, "%d-%m-%Y").month
    if 1 <= month <= 3:
        return "Q2"
    elif 4 <= month <= 5:
        return "Q3"
    elif 9 <= month <= 12:
        return "Q1"

# Чтение данных
with open("grades.json", "r", encoding="utf-8") as file:
    data = json.load(file)

# Сбор оценок по кварталам
quarterly = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))

for entry in data:
    name = entry["name"]
    subject = entry["subject"]
    grade = entry["grade"]
    date = entry["date"]

    quarter = get_quarter(date)
    if quarter:
        quarterly[name][subject][quarter].append(grade)

# Вычисляем средние
report = {}
for name, subjects in quarterly.items():
    report[name] = {}
    for subject, quarters in subjects.items():
        report[name][subject] = {}
        for quarter, grades in quarters.items():
            avg = round(sum(grades) / len(grades), 2)
            report[name][subject][quarter] = avg

# Запись результата
with open("grades_quarterly_report.json", "w", encoding="utf-8") as file:
    json.dump(report, file, indent=4)

Улучшенный вариант с учётом условия задачи:

import json
from collections import defaultdict
from datetime import datetime
from pathlib import Path


def get_quarter(date_str: str) -> str | None:
    """Возвращает квартал по дате в формате DD-MM-YYYY."""
    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  # летние месяцы (июнь–август) пропускаем


def build_quarterly_report(grades: list[dict]) -> dict:
    """Группирует оценки по ученикам, предметам и кварталам."""
    quarterly: defaultdict[str, defaultdict[str, defaultdict[str, list[int]]]] = (
        defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
    )

    for entry in grades:
        quarter = get_quarter(entry["date"])
        if quarter is None:
            continue
        quarterly[entry["name"]][entry["subject"]][quarter].append(entry["grade"])

    report: dict[str, dict[str, dict[str, float]]] = {}
    for name, subjects in quarterly.items():
        report[name] = {}
        for subject, quarters in subjects.items():
            report[name][subject] = {
                quarter: round(sum(scores) / len(scores), 2)
                for quarter, scores in quarters.items()
            }
    return report


if __name__ == "__main__":
    data = json.loads(Path("grades.json").read_text(encoding="utf-8"))
    report = build_quarterly_report(data)

    output = Path("quarterly_report.json")
    output.write_text(
        json.dumps(report, indent=4, ensure_ascii=False),
        encoding="utf-8",
    )
    print(f"Отчёт сохранён в {output}")

Логика: в исходном решении метки кварталов в функции get_quarter не соответствуют условию задачи (январь–март должны быть Q1, а не Q2). Улучшенный вариант использует правильную привязку месяцев к кварталам, аннотации типов, pathlib и f-строки.

Решение 2. Поиск немецких мобильных номеров

import re
from pathlib import Path


def extract_mobile_numbers(text: str) -> list[str]:
    pattern = r"(?:\+49|0049)[\s\-]*1[5-7](?:[\s\-]*\d){7,}"
    raw_matches = re.findall(pattern, text)

    cleaned_numbers = []
    for raw in raw_matches:
        digits = re.sub(r"\D", "", raw)
        if digits.startswith("0049"):
            digits = digits[4:]
        elif digits.startswith("49"):
            digits = digits[2:]
        if 10 <= len(digits) <= 12:
            cleaned_numbers.append(f"+49{digits}")

    return cleaned_numbers


if __name__ == "__main__":
    text = Path("german_numbers.txt").read_text(encoding="utf-8")
    valid_numbers = extract_mobile_numbers(text)

    print("Валидные номера:")
    for number in valid_numbers:
        print(number)

Логика: regex находит кандидатов, начинающихся с +49 или 0049 и префикса 15/16/17. Затем из совпадения удаляются все нецифры, код страны отбрасывается и проверяется длина оставшейся части. Префикс оператора уже гарантирован самим шаблоном.

Решение 3. Извлечение email-адресов

Исходное решение из материалов курса:

import re

text = """
Valid:
- support@mail.com
- info@company.org
- personal_email123@edu.university.net
- user.name@sub.domain.com
- contact_us123@my-site.org
- support@company-name.de
- user@data.edu.pl

Invalid:
- user@domain,com
- name@domaincom
- name@domain.c
- user@domain.toolongtldddddd
- hello.world@some.place.travel
- no@space .com
- bad@@mail.com
- missing@dotcom
"""

pattern = r"\b[a-zA-Z0-9._]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,3}\b"
emails = re.findall(pattern, text)

for email in emails:
    print(email)

Улучшенный вариант:

import re
from pathlib import Path


def extract_emails(text: str) -> list[str]:
    # Локальная часть: не начинается и не заканчивается точкой
    local = r"[a-zA-Z0-9_](?:[a-zA-Z0-9._]*[a-zA-Z0-9_])?"
    domain = r"[a-zA-Z0-9.-]+"
    tld = r"[a-zA-Z]{2,3}"
    pattern = rf"\b{local}@{domain}\.{tld}\b"
    return re.findall(pattern, text)


if __name__ == "__main__":
    text = Path("emails.txt").read_text(encoding="utf-8")
    for email in extract_emails(text):
        print(email)

Логика: улучшенный шаблон гарантирует, что локальная часть не начинается и не заканчивается точкой. В исходном решении [a-zA-Z0-9._]+ допускает адреса вида .user@example.com или user.@example.com, что противоречит условию.

Решение 4. Извлечение номеров банковских карт

import re
from pathlib import Path


def extract_card_numbers(text: str) -> list[str]:
    pattern = r"\b(?:\d{4}[- ]?){3}\d{4}\b"
    return re.findall(pattern, text)


if __name__ == "__main__":
    text = Path("cards.txt").read_text(encoding="utf-8")
    cards = extract_card_numbers(text)

    print("Валидные номера:")
    for card in cards:
        print(card)

Логика: шаблон ищет четыре группы по 4 цифры, между которыми может стоять пробел или дефис (или ничего). Двойные разделители и нецифровые символы автоматически отсеиваются.

Проверить по документации: для настоящей валидации банковских карт в production используйте алгоритм Луна и специализированные библиотеки, а не только регулярные выражения. См. модуль re.