Решение 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 цифры, между которыми может стоять пробел или дефис (или ничего). Двойные разделители и нецифровые символы автоматически отсеиваются.