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

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

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

Проект: анализатор текстового отчёта

Финальный проект курса: настоящий текстовый отчёт магазина, полный ловушек из уроков 19 — пробелы, регистр, пустые строки. Собираем анализатор, который чистит, разбирает и выводит итоги.

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

Десять недель вы учили срезы, f-строки, методы и ловушки. Пора применить всё сразу к задаче, которая встречается в каждом втором проекте: пришёл текстовый отчёт — сырой, с лишними пробелами и разнобоем регистра — а из него нужны цифры. Разберём отчёт магазина построчно и соберём сводку.

Что за отчёт и что нужно получить

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

сырой отчёт — данные проекта
ОТЧЁТ МАГАЗИНА: 21 октября

Кофе, 89
Чай, 120
  Кофе,  74
Хлеб, 45
ОШИБКА: цена не указана
Чай, 120
кофе, 95

ОШИБКА: склад закрыт
Вывод
(текст отчёта)
Текст встроен в код проекта ниже — здесь он для знакомства

Шаг 1. Разбиваем на строки и чистим

Отчёт разбирается splitlines — по переводам строк. Каждая строка проходит strip: срезает случайные пробелы. Пустые строки (после strip) пропускаем — это просто разделители секций.

чистка строк отчёта
report = '''ОТЧЁТ МАГАЗИНА: 21 октября

Кофе, 89
Чай, 120
  Кофе,  74
Хлеб, 45
ОШИБКА: цена не указана
Чай, 120
кофе, 95

ОШИБКА: склад закрыт'''

lines = [line.strip() for line in report.splitlines()]
lines = [line for line in lines if line]
print('Строк после чистки:', len(lines))
Вывод
Строк после чистки: 9

Шаг 2. Фильтруем ошибки и разбираем продажи

Строки с ошибками начинаются с ОШИБКА — отбираем их startswith (после приведения к верхнему регистру — ловушка регистра из урока 19). Остальные строки — продажи: разбираем split(', ') на название и сумму, приводим название к аккуратному виду через capitalize, сумму — в число.

разбор продаж и счётчик ошибок
report = '''ОТЧЁТ МАГАЗИНА: 21 октября
Кофе, 89
Чай, 120
  Кофе,  74
Хлеб, 45
ОШИБКА: цена не указана
Чай, 120
кофе, 95

ОШИБКА: склад закрыт'''

lines = [line.strip() for line in report.splitlines() if line.strip()]

sales = []
errors = 0
for line in lines:
    if line.upper().startswith('ОТЧЁТ'):
        continue
    if line.upper().startswith('ОШИБКА'):
        errors += 1
        continue
    name, price = line.split(', ')
    sales.append([name.strip().capitalize(), int(price.strip())])

print('Продаж:', len(sales), '| Ошибок:', errors)
Вывод
Продаж: 6 | Ошибок: 2

Шаг 3. Считаем итоги

Общая выручка — сумма цен. Лидер продаж — подсчёт по названиям словарём (названия уже нормализованы capitalize — «Кофе» и «кофе» стали одним товаром, ловушка регистра обезврежена). Итоги печатаем f-строкой с выравниванием из урока 16.

выручка и лидер продаж
sales = [['Кофе', 89], ['Чай', 120], ['Кофе', 74], ['Хлеб', 45], ['Чай', 120], ['Кофе', 95]]

total = sum(sale[1] for sale in sales)
print(f'Выручка: {total} руб.')

counts = {}
for name, price in sales:
    counts[name] = counts.get(name, 0) + 1

leader = max(counts, key=counts.get)
print(f'Лидер: {leader:<8} ({counts[leader]} продаж)')
Вывод
Выручка: 543 руб.
Лидер: Кофе     (3 продаж)
полный проект одним листингом
report = '''ОТЧЁТ МАГАЗИНА: 21 октября

Кофе, 89
Чай, 120
  Кофе,  74
Хлеб, 45
ОШИБКА: цена не указана
Чай, 120
кофе, 95

ОШИБКА: склад закрыт'''

lines = [line.strip() for line in report.splitlines() if line.strip()]


sales = []
errors = 0
for line in lines:
    if line.upper().startswith('ОТЧЁТ'):
        continue
    if line.upper().startswith('ОШИБКА'):
        errors += 1
    else:
        name, price = line.split(', ')
        sales.append([name.strip().capitalize(), int(price.strip())])

total = sum(sale[1] for sale in sales)
counts = {}
for name, price in sales:
    counts[name] = counts.get(name, 0) + 1
leader = max(counts, key=counts.get)

print('=== СВОДКА ===')
print(f'Строк продаж: {len(sales)}')
print(f'Ошибок: {errors}')
print(f'Выручка: {total} руб.')
print(f'Лидер: {leader} ({counts[leader]} продаж)')

for name, price in sorted(sales, key=lambda s: -s[1])[:3]:
    print(f'{name:<8} {price:>5} руб.')
Вывод
=== СВОДКА ===
Строк продаж: 6
Ошибок: 2
Выручка: 543 руб.
Лидер: Кофе (3 продаж)
Чай        120 руб.
Чай        120 руб.
Кофе        95 руб.

Проект использовал всё: splitlines и split (урок 9), strip и capitalize (5, 6), startswith и upper (8, 5), f-строки с выравниванием (3, 4, 16), sorted с ключом (18), словарь-счётчик и списковое включение. Тот же скелет — чистка, разбор, фильтр, счётчики, отчёт — работает для логов сервера, CSV-выгрузок и сообщений ботов.

Что мы строим и зачем

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

Проверьте себя: какие строки в отчёте ниже вы бы считали проблемными? Там есть пробелы в начале строки, два пробела после запятой, разный регистр товара («Кофе» и «кофе»), пустые строки и две строки с ошибками. Каждая из этих мелочей ломает наивный разбор — и именно поэтому проект полезнее списка методов.

Шаг 0: скелет решения

  • Чистка: strip на каждой строке, пустые строки отбрасываются.
  • Фильтр: служебные строки (заголовок, ошибки) отделяются по префиксу.
  • Разбор: split по разделителю, поля чистятся своими strip.
  • Подсчёты: словарь-счётчик по товарам, сумма по ценам.
  • Вывод: f-строки с выравниванием, чтобы сводка читалась.

Этот скелет — не изобретение проекта, а стандарт обработки текстовых данных. Тот же порядок работает для логов сервера, CSV-выгрузки, сообщений бота и ответа внешнего API. Запомните его как шаблон: чистка, фильтр, разбор, подсчёт, вывод.

Шаг 1: чистим строки

Отчёт разбирается splitlines — по переводам строк. Каждая строка проходит strip: срезает случайные пробелы по краям. Пустые строки после strip пропускаем — это разделители секций, а не записи.

чистка строк отчёта
report = '''ОТЧЁТ МАГАЗИНА: 21 октября

Кофе, 89
Чай, 120
  Кофе,  74
Хлеб, 45
ОШИБКА: цена не указана
Чай, 120
кофе, 95

ОШИБКА: склад закрыт'''

lines = [line.strip() for line in report.splitlines() if line.strip()]
print('Строк после чистки:', len(lines))
Вывод
Строк после чистки: 9

Девять строк из одиннадцати: ушли две пустые. И тут же видна тонкость фильтра — if line.strip() проверяет ОРИГИНАЛЬНУЮ строку, а в список попадает очищенная. Для пустых строк это одно и то же, но если бы вы фильтровали по другому признаку (скажем, «строка не начинается с ОШИБКА»), проверять надо было бы уже очищенную копию. Мелкая деталь, из-за которой потом неделю ищете, почему один отчёт разобрался, а другой нет.

Шаг 2: фильтруем и разбираем

Строки с ошибками начинаются с ОШИБКА — отбираем их startswith, но сначала приводим к верхнему регистру: в отчёте может встретиться «Ошибка» с большой буквы, и это ловушка регистра из урока про lower. Остальные строки — продажи: разбираем split(', '), название приводим к аккуратному виду через capitalize, сумму — в число.

разбор продаж и счётчик ошибок
report = '''ОТЧЁТ МАГАЗИНА: 21 октября
Кофе, 89
Чай, 120
  Кофе,  74
Хлеб, 45
ОШИБКА: цена не указана
Чай, 120
кофе, 95

ОШИБКА: склад закрыт'''

lines = [line.strip() for line in report.splitlines() if line.strip()]

sales = []
errors = 0
for line in lines:
    if line.upper().startswith('ОТЧЁТ'):
        continue
    if line.upper().startswith('ОШИБКА'):
        errors += 1
        continue
    name, price = line.split(', ')
    sales.append([name.strip().capitalize(), int(price.strip())])

print('Продаж:', len(sales), '| Ошибок:', errors)
Вывод
Продаж: 6 | Ошибок: 2

Шесть продаж и две ошибки — сходится: девять строк, из них одна заголовок, две ошибки, шесть продаж. Заголовок пропускается через continue, и это важно: если бы мы его не отфильтровали, split(', ') на строке «ОТЧЁТ МАГАЗИНА: 21 октября» дал бы две части, и int('МАГАЗИНА: 21 октября') упал бы с ValueError. Каждая строка отчёта — либо запись, либо мусор, и разбор обязан уметь отличить одно от другого ДО попытки превратить в число.

Здесь важно не запомнить конкретный приём, а понять принцип: разделитель должен быть ВЕЗДЕ ОДИНАКОВЫМ, иначе количество частей перестаёт быть предсказуемым. Если в данных встречаются и «, » и «,», разбор одним split невозможен в принципе — придётся нормализовать разделители заранее или брать модуль csv, который умеет это сам. Мой совет: если в данных есть сомнения, разбирайте по позициям или через csv.reader, а не через ручной split.

Шаг 3: считаем итоги

Общая выручка — сумма цен. Лидер продаж — подсчёт по названиям словарём: названия уже нормализованы через capitalize, поэтому «Кофе» и «кофе» стали одним товаром и ловушка регистра обезврежена. Итоги печатаем f-строкой с выравниванием из урока 16, чтобы сводка читалась.

выручка и лидер продаж
sales = [['Кофе', 89], ['Чай', 120], ['Кофе', 74], ['Хлеб', 45], ['Чай', 120], ['Кофе', 95]]
total = sum(sale[1] for sale in sales)
print(f'Выручка: {total} руб.')

counts = {}
for name, price in sales:
    counts[name] = counts.get(name, 0) + 1

leader = max(counts, key=counts.get)
print(f'Лидер: {leader:<8} ({counts[leader]} продаж)')
Вывод
Выручка: 543 руб.
Лидер: Кофе     (3 продаж)

Что здесь стоит отметить. sum(sale[1] for sale in sales) — генераторное выражение: суммируются вторые элементы каждой продажи. counts.get(name, 0) — чтение словаряря с умолчанием: товара ещё нет — получаем 0 и прибавляем 1. max(counts, key=counts.get) — ключ с наибольшим счётчиком: max перебирает ключи, а key говорит, по чему их сравнивать. Возвращается КЛЮЧ, то есть строка «Кофе», а не число 3. Три строки, и вся сводка.

Про capitalize стоит сказать отдельно, потому что это не просто «красота». Он приводит первую букву к верхнему регистру, а остальные — к нижнему. Для «кофе» получится «Кофе», и это совпадёт с «Кофе» из другой строки. Если бы товар назывался «КОФЕ», capitalize дал бы «Кофе» — тоже совпадёт. А вот upper() дал бы «КОФЕ» и «КОФЕ» — тоже совпало бы, но слово выглядело бы криво в отчёте. Смысл normalize — не в красоте, а в том, чтобы ключи словаря совпали.

Строка отчётаЧто с ней делаемИнструмент
Кофе, 74почистить края, разобрать поляstrip + split(', ')
ОШИБКА: ...посчитать как ошибку, не разбиратьupper() + startswith
ОТЧЁТ МАГАЗИНА: ...пропустить как заголовокupper() + startswith + continue
кофе, 95нормализовать имя и привести цену к числуcapitalize + int()
`` (пустая)отбросить как разделитель секцийstrip + фильтр

Скелет из пяти шагов переносится на что угодно. Тот же CSV-лог загрузок: строки с 'ERROR' считаем ошибками, остальные разбираем по запятой, имена нормализуем, суммируем по полю price, выводим топ. Разница только в разделителе и в том, какие поля есть в записи.

И последнее, что стоит сказать про этот проект. Он маленький — тридцать строк кода. Но в нём есть всё, что делает разбор текста настоящей работой: грязные данные, нормализация, фильтрация, подсчёт и форматированный вывод. Тот же скелет, применённый к логу из ста тысяч строк, даст отчёт по частоте ошибок; применённый к выгрузке из CRM — статистику по товарам. Дальше — только объём данных.

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

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

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

line = '  Кофе,  74'
name, price = line.split(', ')
print(name.strip().capitalize(), int(price.strip()))
counts = {'Кофе': 3, 'Чай': 2, 'Хлеб': 1}
print(max(counts, key=counts.get))
print('-'.join(['a', 'b']))
Проверь себя
0 / 8

1. Зачем в разборе нужна строка price.strip() перед int()?

2. Почему названия приводятся через capitalize()?

3. Что делает counts.get(name, 0)?

4. Что делает max(counts, key=counts.get)?

5. Какой скелет у проекта?

6. Какой скелет у проекта?

7. Что произойдёт, если в отчёте окажется строка без цены — 'Кофе,'?

8. Как найти самый продаваемый товар в списке продаж?

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

Дан текст orders = 'чай, 80\nКофе, 150\n чай, 70'. Посчитайте и напечатайте: количество заказов, выручку и строку-лидер в формате «Лидер: N продаж» (товары нормализуйте capitalize, лидера найдите словарём-счётчиком).

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

Что делать, если формат отчёта сложнее — поля через табуляцию или фикс-ширину?

Меняется только разбор: split('\t') для табуляции, срезы по позициям для фикс-ширины (уроки 2 и 9). Скелет чистка → разбор → фильтр → счётчики остаётся тем же.

Почему в проекте списковое включение, а не цикл for?

[line.strip() for line in report.splitlines()] — компактная форма «преобразуй каждую». Вторая строка с условием if — «отфильтруй пустые». Оба приёма — стандарт чтения и записи данных.

Что делать, если формат отчёта сложнее — поля через табуляцию или фикс-ширину?

Меняется только разбор: split('\t') для табуляции, срезы по позициям для фикс-ширины (уроки 2 и 9). Скелет чистка → разбор → фильтр → счётчики остаётся тем же.

Куда двигаться после этого курса строк?

Регулярные выражения — когда startswith и find перестают хватать для шаблонов; Pandas — когда строковые данные превращаются в таблицы. Оба курса Питоники строятся прямо на этом фундаменте.

Курс Регулярные выражения

Как не сломать разбор на грязных данных?

Оберните int(price) в try/except ValueError, считайте битую строку ошибкой и пишите её в отдельный список. Тихий пропуск хуже падения: падение хотя бы видно, а потерянная продажа обнаружится только на сверке с бухгалтерией.

Почему товары нормализуют через capitalize, а не через upper?

Потому что задача — не красота, а совпадение ключей: «Кофе» и «кофе» должны стать одним товаром в словаре-счётчике. capitalize даёт «Кофе» из любого варианта написания, и в отчёте это выглядит аккуратно.

Урок 5: методы регистра

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

«Десять недель вы учили срезы, f-строки, методы и ловушки.»

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

TelegramVK

Что дальше

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

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

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