Проект: анализатор текстового отчёта
Финальный проект курса: настоящий текстовый отчёт магазина, полный ловушек из уроков 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']))
1. Зачем в разборе нужна строка price.strip() перед int()?
2. Почему названия приводятся через capitalize()?
3. Что делает counts.get(name, 0)?
4. Что делает max(counts, key=counts.get)?
5. Какой скелет у проекта?
6. Какой скелет у проекта?
7. Что произойдёт, если в отчёте окажется строка без цены — 'Кофе,'?
8. Как найти самый продаваемый товар в списке продаж?
Дан текст orders = 'чай, 80\nКофе, 150\n чай, 70'. Посчитайте и напечатайте: количество заказов, выручку и строку-лидер в формате «Лидер: N продаж» (товары нормализуйте capitalize, лидера найдите словарём-счётчиком).
Что делать, если формат отчёта сложнее — поля через табуляцию или фикс-ширину?
Меняется только разбор: 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-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Строки · Урок 19
Подводные камни строк
Пять ловушек, на которых спотыкается каждый: код «не работает», а причины не видно. Разбираем их на реальных примерах — и с готовыми решениями.
re · Урок 1
Регулярные выражения Python с нуля: первый шаблон и модуль re
Первый шаблон, который находит любую дату в тексте, а не одну конкретную: import re, re.search, raw-строки и объект Match — всё, чтобы перестать бояться регулярки.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Datetime · Урок 19
Даты в логах: разбор реального формата
Журнал из строк вида «2026-10-21 09:15:03 ERROR …»: разбираем момент через strptime, сортируем события по времени и вырезаем записи за интервал.
парсинг дат логов pythondatetime из строки лога
Циклы · Урок 16
Циклы и строки в Python: разбор текста на слова
Разбор текста начинается со split: строка превращается в список слов, цикл считает, ищет и собирает отчёт.
split pythonpython цикл по буквам строки
Строки · Урок 9
split: разобрать строку
Одна строка данных редко живёт одна: CSV-запись, строка лога, поля формы. Метод split режет строку по разделителю и возвращает список частей — разбираем все варианты, от запятой до splitlines.
split pythonsplit в python