Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 20 из 20 Начальный 25 мин 150 XP

Проект: анализ уникальных посетителей на Python

Итоговый проект: журнал из 12 визитов вида «имя:день» превращается в отчёт — уникальные посетители, гости по дням, постоянные через пересечение, топ повторов. Три куска, каждый самодостаточен.

Редакция Питоники

Финальный проект собирает раздел в одну программу. Данные — журнал посещений: двенадцать записей вида «имя:день», как их выдал бы сервер или счётчик посещений. Заказчик просит четыре ответа: сколько всего визитов и разных людей, кто пришёл в каждый день, кто постоянный гость двух дней и кто ходит чаще всех. Всё это уже было по кусочкам: уникальность множества, пересечение и разность, дедупликация — из них собирается отчёт.

Кусок проектаЧто отвечаетКакой приём
Кусок 1Уникальные посетители и гости каждого дняsplit, словарь множеств, len
Кусок 2Постоянные и одиночки между днями& , -, ^ на множествах
Кусок 3Частота визитов и топ повторовСчётчик get, sorted с key

Данные: журнал как список строк

В песочнице файлов нет, поэтому журнал — список строк, тот же объект, что вернул бы readlines. Первое правило любого разбора — показать данные как есть, с номерами записей:

шаг 0: показать журнал как есть
visits = [
    'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
    'Анна:вт', 'Глеб:вт', 'Дана:вт',
    'Борис:ср', 'Анна:ср',
    'Вера:пт', 'Анна:пт', 'Глеб:пт',
]

for n, record in enumerate(visits, 1):
    print(n, '-', record)
Вывод
1 - Анна:пн
2 - Борис:пн
3 - Вера:пн
4 - Егор:пн
5 - Анна:вт
6 - Глеб:вт
7 - Дана:вт
8 - Борис:ср
9 - Анна:ср
10 - Вера:пт
11 - Анна:пт
12 - Глеб:пт

Двенадцать записей, четыре дня, шесть разных имён — все данные проекта видны с первого взгляда. Формат каждой записи один: имя, двоеточие, день. Значит, split(':') из курса про строки разберёт любую запись на пару:

split делит запись на пару
name, day = 'Анна:пн'.split(':')
print('имя:', name, '- день:', day)
print('частей:', len('Анна:пн'.split(':')))
Вывод
имя: Анна - день: пн
частей: 2

Кусок 1: уникальные посетители и гости по дням

Разбор идёт одним циклом: каждая запись делится на имя и день, имя добавляется в множество своего дня. Словарь дней хранит set — повторные визиты одного человека за день поглощаются молча. Параллельно копится объединение всех гостей:

кусок 1: разбор журнала
visits = [
    'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
    'Анна:вт', 'Глеб:вт', 'Дана:вт',
    'Борис:ср', 'Анна:ср',
    'Вера:пт', 'Анна:пт', 'Глеб:пт',
]

days = {}
all_guests = set()

for record in visits:
    name, day = record.split(':')
    if day not in days:
        days[day] = set()
    days[day].add(name)
    all_guests.add(name)

print('записей в журнале:', len(visits))
print('уникальных посетителей:', len(all_guests))
for day in ['пн', 'вт', 'ср', 'пт']:
    print(day, '- гостей:', len(days[day]), '-', sorted(days[day]))
Вывод
записей в журнале: 12
уникальных посетителей: 6
пн - гостей: 4 - ['Анна', 'Борис', 'Вера', 'Егор']
вт - гостей: 3 - ['Анна', 'Глеб', 'Дана']
ср - гостей: 2 - ['Анна', 'Борис']
пт - гостей: 3 - ['Анна', 'Вера', 'Глеб']

Первая половина заказа выполнена: двенадцать визитов, шесть разных людей, и по каждому дню виден состав гостей. Проверьте сумму дней: четыре плюс три плюс два плюс три — тоже двенадцать, ни одна запись не потерялась. Печать гостей идёт через sorted — правило проекта: множества печатаются только отсортированными, иначе вывод меняется от запуска к запуску.

сверка: дни складываются в записи
visits = ['Анна:пн', 'Борис:пн', 'Анна:вт', 'Вера:пт']

days = {}
for record in visits:
    name, day = record.split(':')
    if day not in days:
        days[day] = set()
    days[day].add(name)

total = 0
for day in days:
    total += len(days[day])

print('по дням:', total)
print('записей:', len(visits))
print('сходится:', total == len(visits))
Вывод
по дням: 4
записей: 4
сходится: True

Сверка полноты стоит после разбора и работает на любых данных: каждая запись попала ровно в одно множество дня, поэтому сумма размеров множеств обязана равняться числу записей. Если когда-нибудь строка окажется в двух днях или потеряется — проверка скажет сходится: False, и это правильное место для assert из курса про исключения.

Кусок 2: постоянные гости и одиночки

Второй вопрос заказчика: кто приходил в оба дня из пары, а кто только в один. Дни уже лежат множествами — остаются операции из уроков 7 и 8. Пересечение находит постоянных, симметричная разность — тех, кто был ровно в одном из двух дней:

кусок 2: пересечения между днями
visits = [
    'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
    'Анна:вт', 'Глеб:вт', 'Дана:вт',
    'Борис:ср', 'Анна:ср',
    'Вера:пт', 'Анна:пт', 'Глеб:пт',
]

days = {}
for record in visits:
    name, day = record.split(':')
    if day not in days:
        days[day] = set()
    days[day].add(name)

mon, tue = days['пн'], days['вт']
print('пришли в пн и вт:', sorted(mon & tue))
print('были в пн или вт, но не в оба:', sorted(mon ^ tue))
print('постоянные пн и ср:', sorted(days['пн'] & days['ср']))
Вывод
пришли в пн и вт: ['Анна']
были в пн или вт, но не в оба: ['Борис', 'Вера', 'Глеб', 'Дана', 'Егор']
постоянные пн и ср: ['Анна', 'Борис']

Три вопроса — три операции. Вера и Егор пришли в понедельник, но не во вторник, поэтому попали в симметричную разность; Анна была в оба дня и выпала из неё. Никаких циклов по парам людей: множества сравниваются целиком, и код читается как формулировка вопроса. Если заказчик завтра спросит про вт и ср — поменяются две буквы.

Кусок 3: частота визитов и итоговый отчёт

Третий вопрос — кто ходит чаще. Множества здесь бессильны: они знают факт «был», но не число раз. Частоту считает словарь-счётчик — тот же приём, что в задачах на множества:

частоты по именам
visits = [
    'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
    'Анна:вт', 'Глеб:вт', 'Дана:вт',
    'Борис:ср', 'Анна:ср',
    'Вера:пт', 'Анна:пт', 'Глеб:пт',
]

counts = {}
for record in visits:
    name, day = record.split(':')
    counts[name] = counts.get(name, 0) + 1

for name in sorted(counts):
    print(name, '-', counts[name])
Вывод
Анна - 4
Борис - 2
Вера - 2
Глеб - 2
Дана - 1
Егор - 1

Шесть гостей и их счётчики: Анна ходит чаще всех, трое имеют по два визита, Дана и Егор пришли по разу. Топ собирает sorted с составным ключом — сначала по убыванию числа визитов, при равенстве — по алфавиту имени — и печатает первые три строки. Собираем всё в одну функцию analyze и печатаем итоговый отчёт:

кусок 3: итоговый отчёт проекта
visits = [
    'Анна:пн', 'Борис:пн', 'Вера:пн', 'Егор:пн',
    'Анна:вт', 'Глеб:вт', 'Дана:вт',
    'Борис:ср', 'Анна:ср',
    'Вера:пт', 'Анна:пт', 'Глеб:пт',
]

def analyze(visits):
    days = {}
    counts = {}
    for record in visits:
        name, day = record.split(':')
        if day not in days:
            days[day] = set()
        days[day].add(name)
        counts[name] = counts.get(name, 0) + 1
    return days, counts

days, counts = analyze(visits)

print('=== отчёт по журналу ===')
print('записей:', len(visits))
print('уникальных посетителей:', len(counts))
print()
for day in ['пн', 'вт', 'ср', 'пт']:
    print(day, '- гостей:', len(days[day]), '-', sorted(days[day]))
print()
print('постоянные пн и вт:', sorted(days['пн'] & days['вт']))
print()
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
print('топ повторов (визитов):')
for i, (name, n) in enumerate(top[:3], 1):
    print(' ', i, '.', name, '-', n)
Вывод
=== отчёт по журналу ===
записей: 12
уникальных посетителей: 6

пн - гостей: 4 - ['Анна', 'Борис', 'Вера', 'Егор']
вт - гостей: 3 - ['Анна', 'Глеб', 'Дана']
ср - гостей: 2 - ['Анна', 'Борис']
пт - гостей: 3 - ['Анна', 'Вера', 'Глеб']

постоянные пн и вт: ['Анна']

топ повторов (визитов):
  1 . Анна - 4
  2 . Борис - 2
  3 . Вера - 2

Отчёт отвечает на все четыре вопроса заказа разом: записи и люди, состав дней, постоянные гости, топ ходоков. Ключ сортировки (-kv[1], kv[0]) стоит разобрать: минус переворачивает число визитов — самые частые первыми, а имя вторым элементом решает ничьи в алфавитном порядке: у Бориса, Веры и Глеба поровну, и они встали по букве. Множество отвечает, кто приходил вообще; счётчик — кто приходил чаще: вместе они покрывают весь отчёт по журналу.

От списка в коде к настоящему журналу

Единственное отличие проекта от настоящей программы — две строки, где журнал превращается в список строк:

как строки приходят из файла
visits = []
with open('visits.txt', encoding='utf-8') as f:
    for line in f:
        line = line.strip()
        if line:
            visits.append(line)
В песочнице файлов нет, поэтому журнал лежит прямо в коде. На настоящих данных readlines вернёт строки с переводами строк на конце — strip в разборе срежет их, проект не изменится.

strip и проверка на пустоту — тот же шаблон, что в проекте про исключения: файл даёт мусор по краям, разбор его отрезает до split. Дальше всё без изменений: analyze получает список строк и не знает, откуда они приехали.

Что проект закрепил

  • запись имя:день разбирается split-ом в пару, и распаковка даёт две переменные без индексов;
  • словарь дней хранит set посетителей: повторные визиты за день поглощаются автоматически;
  • уникальные посетители — объединение всех множеств или отдельный set в цикле разбора;
  • постоянные гости — пересечение & дней, одиночки — симметричная разность ^, без единого цикла по парам;
  • частоту хранит словарь-счётчик get(name, 0) + 1: множество знает факт, но не число раз;
  • топ собирает sorted с ключом (-число, имя): убывание частоты и алфавит при ничьих;
  • множества печатаются только через sorted и len — вывод не зависит от запуска.

Раздел «Множества и кортежи» окончен: от первого set до отчёта, который из двенадцати строк журнала выжимает состав дней, постоянных гостей и топ повторов. Дальше эти структуры встретятся сразу в нескольких курсах: исключения страхуют разбор грязных журналов try-ом, а словари — родной дом для счётчиков и пар ключ-значение.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

visits = ['Анна:пн', 'Анна:пн', 'Борис:вт']
names = set()
for r in visits:
    names.add(r.split(':')[0])
print(len(visits), len(names))
days = {}
for r in ['Анна:пн', 'Борис:пн', 'Анна:пн']:
    name, day = r.split(':')
    if day not in days:
        days[day] = set()
    days[day].add(name)
print(len(days['пн']))
print(sorted(days['пн']))
counts = {}
for name in ['А', 'Б', 'А', 'А', 'Б']:
    counts[name] = counts.get(name, 0) + 1
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
print(top[0])
Проверь себя
0 / 7

1. Как в проекте узнать число уникальных посетителей журнала?

2. Зачем в разборе проверка if day not in days?

3. Как найти посетителей, пришедших и в пн, и в вт?

4. Почему частоту визитов считает словарь-счётчик, а не множество дней?

5. Что делает ключ (-kv[1], kv[0]) в sorted(counts.items(), key=...)?

6. Строка журнала пришла без двоеточия. Что случится в разборе?

7. Почему analyze возвращает данные, а не печатает отчёт сам?

Карточки терминов
Запомнено: 0 / 8
Практика

Дан журнал visits из проекта. Посчитайте, сколько раз приходил Борис, и найдите людей, которые были и в вт, и в ср. Напечатайте строку «Борис: N» и отсортированный список пересечения.

practice.py
Вопросы и ответы по уроку

Как посчитать уникальных посетителей в журнале логов?

Разобрать каждую запись split-ом на имя и день и складывать имена в множество: len(множества) даст число разных посетителей, len(журнала) — число визитов. Повторы множество поглощает автоматически — циклы со сравнением имён не нужны.

Как найти людей, которые приходили в оба дня?

Собрать посетителей каждого дня в отдельные множества и взять пересечение: days['пн'] & days['вт']. Симметричная разность ^ наоборот найдёт тех, кто был ровно в один из двух дней, а объединение | — всех гостей пары дней.

Урок 7: пересечение множеств

Как построить топ самых частых посетителей?

Считать визиты словарём: counts[name] = counts.get(name, 0) + 1, затем отсортировать пары по составному ключу: sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])). Минус ставит частые записи первыми, имя при равенстве решает ничью по алфавиту; срез [:3] берёт топ-3.

Почему повторный визит одного человека за день не увеличивает число гостей?

Потому что день хранится множеством: второй add того же имени ничего не меняет. Это осознанная модель: множество отвечает на вопрос «кто был», а частоту визитов хранит отдельный словарь-счётчик. Гостей за день считает len множества, визитов — сумма счётчиков.

Что делать с битыми записями журнала без двоеточия?

Страховать разбор try-ом: name, day = record.split(':') внутри блока try, except ValueError отправляет битую строку в журнал ошибок с номером записи, а отчёт продолжает разбирать остальные. Каркас тот же, что в проекте курса про исключения.

Проект: разбор грязного файла с try

Понравился урок? Сошлитесь на него

«Множество отвечает, кто приходил вообще; счётчик — кто приходил чаще: вместе они покрывают весь отчёт по журналу.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.