Проект: надёжный разбор файла с грязными данными в Python
Итоговый проект: файл с мусором — пустые строки, слова вместо чисел, пробелы — не роняет отчёт, а попадает в журнал. Три куска: построчный разбор, счётчики с первыми ошибками и функция-парсер со сверкой.
Редакция Питоники
Финальный проект собирает раздел в одну программу. Данные — файл с показаниями датчиков, и файл этот грязный: числа с пробелами по краям, пустые строки, слова вместо значений, десятичные дроби, которые int не понимает. Требования заказчика звучат просто: отчёт должен быть посчитан по всем годным строкам, каждая битая — названа с номером, и ни одна строка не должна исчезнуть молча. Всё это уже было по кусочкам: try внутри цикла, сообщение ошибки через as e, пороги и сверка — остаётся собрать.
| Кусок проекта | Что делает | Какой приём |
|---|---|---|
| Кусок 1 | Построчный разбор с печатью статуса каждой строки | try внутри for, strip, enumerate |
| Кусок 2 | Отчёт: счётчики трёх категорий и первые 3 ошибки | Список журнала, срез, as e |
| Кусок 3 | Функция parse_lines и сверка полноты | Возврат кортежа, assert |
Данные: что в файле
В песочнице файлов нет, поэтому файл представлен списком строк — тем же самым объектом, который вернул бы readlines. Девять строк, три из них битые, две пустые. Первый шаг любого разбора — показать данные как есть, с номерами и границами строки в квадратных скобках, чтобы были видны пробелы:
raw_lines = [
' 125 ',
'',
'семьдесят',
'42',
' ',
'3.14',
'8',
'1e3',
'-7',
]
for n, line in enumerate(raw_lines, 1):
print(f'{n}: [{line}]')
1: [ 125 ] 2: [] 3: [семьдесят] 4: [42] 5: [ ] 6: [3.14] 7: [8] 8: [1e3] 9: [-7]
Диагноз по глазам: первая строка годная, но с пробелами; вторая и пятая пустые — во второй ни одного символа, в пятой одни пробелы; третья — слово, шестая — дробь, восьмая — научная запись. Четвёртая, седьмая и девятая — чистые числа. Перед разбором стоит договориться о классификации: каждая строка попадёт ровно в одну из трёх категорий — разобрано, сбой, пустая.
Контрпример: весь цикл в try
Прежде чем писать правильно, покажем неправильно. Соблазн оставить структуру урока 2 и обнять try-ом весь цикл разом:
raw_lines = ['10', 'битое', '30']
values = []
try:
for line in raw_lines:
values.append(int(line))
print('разобраны все:', values)
except ValueError:
print('весь цикл в try: потеряны и битые, и хорошие')
весь цикл в try: потеряны и битые, и хорошие
try сработал — и заодно убил результат: строка 10 была годной, но в список она не попала, потому что append выполнялся по порядку, а упало на второй строке. Один сбой отменил всю работу. Вот почему в проекте try стоит внутри цикла и обнимает одну строку данных — масштаб страховки должен совпадать с масштабом сбоя.
Кусок 1: построчный разбор
Правильный каркас: для каждой строки — свой try. Пустая строка отсеивается до рискованной операции: strip срезает пробелы, и если после него ничего не осталось — строка пропущена. Разбор — в try, сбой — в except с сообщением из as e:
raw_lines = [
' 125 ',
'',
'семьдесят',
'42',
' ',
'3.14',
'8',
'1e3',
'-7',
]
for n, line in enumerate(raw_lines, 1):
text = line.strip()
if not text:
print(f'{n}: пустая строка, пропущена')
continue
try:
value = int(text)
print(f'{n}: число {value}')
except ValueError as e:
print(f'{n}: сбой - {e}')
print('первый проход закончен')
1: число 125 2: пустая строка, пропущена 3: сбой - invalid literal for int() with base 10: 'семьдесят' 4: число 42 5: пустая строка, пропущена 6: сбой - invalid literal for int() with base 10: '3.14' 7: число 8 8: сбой - invalid literal for int() with base 10: '1e3' 9: число -7 первый проход закончен
Девять строк — девять вердиктов, и цикл дошёл до конца. Порядок внутри тела важен: сначала strip и проверка на пустоту, потом int. Пробелы с краёв int терпит и сам — годная строка разобралась бы и без них, — но без strip строка из одних пробелов не отличалась бы от числа, и проверка пустоты теряла бы смысл. Сообщение ValueError само называет виновника: седьмая строка отчёта показывает и слово, и дробь, и научную запись — каждый сбой с уликой.
Кусок 2: отчёт со счётчиками и первыми ошибками
Печать в моменте показывает процесс, отчёт — результат. Кусок 2 копит три категории: разобранные числа в список, сбои — в журнал с номером строки и сообщением, пустые — счётчиком. В конце печатается сводка и первые три ошибки: срез error_log[:3] берёт начало журнала, номера строк — от enumerate с единицы:
raw_lines = [
' 125 ',
'',
'семьдесят',
'42',
' ',
'3.14',
'8',
'1e3',
'-7',
]
parsed = []
error_log = []
empty = 0
for n, line in enumerate(raw_lines, 1):
text = line.strip()
if not text:
empty += 1
continue
try:
parsed.append(int(text))
except ValueError as e:
error_log.append((n, text, str(e)))
print('строк всего:', len(raw_lines))
print('разобрано:', len(parsed), '- сумма', sum(parsed))
print('сбоев:', len(error_log))
print('пустых пропущено:', empty)
print()
print('первые 3 ошибки:')
for n, text, message in error_log[:3]:
print(f' строка {n}: {text!r} -> {message}')
строк всего: 9 разобрано: 4 - сумма 168 сбоев: 3 пустых пропущено: 2 первые 3 ошибки: строка 3: 'семьдесят' -> invalid literal for int() with base 10: 'семьдесят' строка 6: '3.14' -> invalid literal for int() with base 10: '3.14' строка 8: '1e3' -> invalid literal for int() with base 10: '1e3'
Четыре числа, сумма сто шестьдесят восемь, три сбоя, две пустые — отчёт отвечает на все вопросы заказчика разом. Каждая запись журнала — кортеж из трёх значений: номер, очищенный текст, сообщение ошибки; распаковка в цикле печати даёт колонки без индексов. Спецификация {text!r} печатает значение в кавычках: даже если бы строка состояла из одних пробелов, в отчёте было бы видно, что в ней не пусто. Одна битая строка — это запись в журнале, а не причина ронять весь отчёт.
Сверка: ни одна строка не потеряна
Отчёту нужна гарантия полноты: три категории обязаны сложиться в общее число строк. Это работа для assert из урока 18 — инвариант проекта, нарушение которого означает баг в разборе:
raw_lines = ['10', '', 'x', '20']
parsed = 0
errors = 0
empty = 0
for line in raw_lines:
text = line.strip()
if not text:
empty += 1
else:
try:
int(text)
parsed += 1
except ValueError:
errors += 1
print('разобрано:', parsed, '- сбоев:', errors, '- пустых:', empty)
assert parsed + errors + empty == len(raw_lines), 'потеряны строки'
print('сверка сошлась: ни одна строка не потеряна')
разобрано: 2 - сбоев: 1 - пустых: 1 сверка сошлась: ни одна строка не потеряна
Два плюс один плюс один — четыре, столько же, сколько строк в файле. Проверка стоит после разбора и действует на любых данных: добавьте строку, и обе стороны вырастут синхронно. Если же когда-нибудь категория потеряется — например, строка окажется в двух ветках или ни в одной, — assert уронит программу с сообщением потеряны строки, и это правильная реакция: отчёт с дырой хуже падения.
Кусок 3: функция parse_lines
Разбор в проекте встретится не один раз — файлы приходят каждый день. Время завернуть логику в функцию: на входе список строк, на выходе — кортеж из трёх категорий. Код тела не изменился ни на символ, изменилась упаковка:
raw_lines = [
' 125 ',
'',
'семьдесят',
'42',
' ',
'3.14',
'8',
'1e3',
'-7',
]
def parse_lines(lines):
numbers = []
error_log = []
skipped = 0
for n, line in enumerate(lines, 1):
text = line.strip()
if not text:
skipped += 1
continue
try:
numbers.append(int(text))
except ValueError as e:
error_log.append((n, text, str(e)))
return numbers, error_log, skipped
numbers, errors, skipped = parse_lines(raw_lines)
print('числа:', numbers)
print('сумма:', sum(numbers))
print('сбоев:', len(errors))
print('пустых:', skipped)
числа: [125, 42, 8, -7] сумма: 168 сбоев: 3 пустых: 2
Функция ничего не печатает — она возвращает данные, а печать остаётся вызывающему коду: так parse_lines можно вызвать в тесте, в другом отчёте или в скрипте с другим выводом. Журнал сбоев теперь часть возвращаемого кортежа, и вызывающий сам решает, сколько ошибок показать. Три ошибки этого файла — весь журнал; на файле из тысячи строк пригодился бы порог:
float: спасти ещё две строки
Три сбоя файла — не одинаковые. Слова семьдесят — настоящий мусор, а 3.14 и 1e3 — законные числа, просто не для int. Проверим, что сказал бы float из урока 12:
for text in ['3.14', '1e3', 'семьдесят']:
try:
print(text, '-> float разобрал:', float(text))
except ValueError:
print(text, '-> не число даже для float')
3.14 -> float разобрал: 3.14 1e3 -> float разобрал: 1000.0 семьдесят -> не число даже для float
float забрал бы две строки из трёх — и это решение о требованиях, а не о коде: если датчик умеет дроби, разбор строится на float, а int остаётся только для счётчиков строк. Важно, что тип ошибки не меняется: float падает с тем же ValueError, и весь каркас проекта — try, except, журнал — пережил бы замену без единой правки.
От песочницы к настоящему файлу
Единственное, что отличает проект от настоящей программы, — две строки, где файл превращается в список строк:
with open('data.txt', encoding='utf-8') as f:
raw_lines = f.readlines()
Что проект закрепил
- масштаб try равен масштабу сбоя: одна строка данных — одна попытка, весь цикл в try теряет годные строки вместе с битыми;
- три категории данных — разобрано, сбой, пустая — считаются раздельно и складываются в общий итог;
- журнал сбоев хранит номер, значение и сообщение: улики собираются в момент сбоя, показываются по решению отчёта;
- инвариант полноты с assert ловит потерю строк раньше, чем её увидит заказчик;
- смена int на float не меняет тип ошибки — каркас try и журнала живёт при любой замене разбора.
Раздел «Исключения» окончен: от первого traceback до программы, которая переживает грязный файл и отчитывается о каждой строке. Программы, которым можно доверять данные, написаны именно на этих приёмах — и дальше они понадобятся сразу: файлы Excel в openpyxl падают на чужих книгах так же, как int на слове семьдесят, а тесты в pytest превращают assert-инварианты этого проекта в проверки целого кода.
Сначала предскажи ответ в голове — это главный навык программиста.
lines = ['7', '', 'x']
ok = 0
bad = 0
for line in lines:
text = line.strip()
if not text:
continue
try:
int(text)
ok += 1
except ValueError:
bad += 1
print(ok, bad)
errors = [(3, 'abc'), (6, '3.14')]
for n, text, msg in errors[:3]:
print(f'строка {n}: {text!r}')
print('конец журнала')
values = [' 42 ', '', 'x']
parsed = []
for line in values:
text = line.strip()
if not text:
continue
try:
parsed.append(int(text))
except ValueError:
pass
print(parsed, sum(parsed))
1. Почему в проекте try стоит внутри цикла, а не вокруг всего цикла?
2. Зачем в разборе strip до int, если int и сам терпит пробелы?
3. Из чего состоит запись журнала сбоев в проекте?
4. Что напечатает цикл по error_log[:3], если в журнале две записи?
5. Что гарантирует assert parsed + errors + empty == len(raw_lines)?
6. Почему замена int на float в проекте почти не требует правок?
7. Почему parse_lines возвращает данные, а не печатает их?
Дан список строк file_data: ['15', ' ', 'abc', '7', '']. Разберите его по схеме проекта: пустые строки после strip пропускайте, годные числа складывайте в numbers, сбои считайте счётчиком bad. В конце напечатайте numbers и три строки отчёта: разобрано N, сбоев N, пустых N.
Как разобрать файл с битыми строками и не уронить программу?
Ставить try внутрь цикла вокруг разбора одной строки: for n, line in enumerate(lines, 1): try: numbers.append(int(line.strip())) except ValueError: журнал. Сбой превращается в запись журнала с номером строки, а остальные строки разбираются как ни в чём не бывало.
Урок 16: ошибки в циклахКак показать первые N ошибок разбора?
Копить сбои в список кортежей (номер строки, значение, сообщение) и напечатать начало срезом: for n, text, msg in error_log[:3]: print(f'строка {n}: {text}'). Срез не падает, даже если записей меньше трёх.
Как убедиться, что при разборе не потерялись строки?
Считать три категории — разобранные, сбои, пустые — и сверять их сумму с числом строк: assert parsed + errors + empty == len(lines), 'потеряны строки'. Инвариант ловит и потерю, и двойной счёт раньше, чем отчёт попадёт к заказчику.
Урок 18: assertНужен ли strip, если int сам терпит пробелы?
Да: strip нужен не для int, а для классификации. Строка из одних пробелов без strip упала бы в журнал сбоев как мусор, хотя это просто пустая строка. После strip она опознаётся и пропускается отдельной категорией.
Что делать с дробями вроде 3.14 в файле чисел?
Если данные допускают дроби, разобрать их float-ом: float('3.14') и float('1e3') работают, а падает float с тем же ValueError, что и int, — каркас try и журнала менять не нужно. Слова вроде семьдесят останутся сбоями при любом разборе.
Урок 12: ValueErrorКак перейти от списка строк в коде к настоящему файлу?
Заменить объявление списка на чтение: with open('data.txt', encoding='utf-8') as f: raw_lines = f.readlines(). Строки из файла несут переводы строк на конце — strip в разборе срезает их, поэтому весь остальной проект не меняется.
Урок 13: файлы и FileNotFoundErrorПонравился урок? Сошлитесь на него
«Одна битая строка — это запись в журнале, а не причина ронять весь отчёт.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Исключения · Урок 16
Ошибки в цикле Python: try внутри for и продолжение после сбоя
Сто строк данных, одна битая — и без защиты падает весь проход. Ставим try вокруг одного элемента: сбой обрабатывается, цикл идёт дальше, сбои считаются и попадают в журнал.
Исключения · Урок 5
as e: объект ошибки в переменной
Обработчик узнаёт больше, если взять ошибку в переменную: except ValueError as e даёт тип, сообщение и аргументы исключения.
Исключения · Урок 18
assert в Python: проверки для разработчика и AssertionError
assert — это заявление: здесь должно быть так. Провал превращается в AssertionError с вашим сообщением, а флаг -O умеет вырезать все проверки разом — поэтому инструмент разработчика, а не валидатор ввода.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
json · Урок 20
Проект: каталог книг — от URL до файла
Финал раздела: один скрипт собирает каталог книг — параметры в URL, разбор ответа, фильтр по рейтингу, сортировка по цене и проверенный файл на диске.
python проект json api каталогpython проект пример для начинающих
Datetime · Урок 19
Даты в логах: разбор реального формата
Журнал из строк вида «2026-10-21 09:15:03 ERROR …»: разбираем момент через strptime, сортируем события по времени и вырезаем записи за интервал.
парсинг дат логов pythonpython лог файл обработка дат
Исключения · Урок 7
raise: бросаем исключение сами
До сих пор ошибки бросал Python. Теперь научимся бросать сами: raise ValueError с понятным текстом, когда данные нарушают правила программы.
python свои исключенияpython обработка ошибок