Глава 3. Текст и работа со строками: очистка данных и вывод в отчёт
Задача главы: научиться приводить «грязные» текстовые данные из выгрузок — ФИО с лишними пробелами, разный регистр, опечатки, слитные поля — к единому стандарту, пригодному для объединения таблиц, и выводить результат ровной строкой отчёта средствами f-строк.
3.1 Текстовый тип str и базовая зачистка: .strip(), .lower(), .upper(), .title()
Зачем это нужно. Выгрузки из разных систем — LMS, гугл-формы, база данных — приносят один и тот же человек в трёх написаниях: 'Иванов ', 'иванов иван', 'ИВАНОВ'. Для Python это три разных значения, поэтому таблицы не склеятся, а в отчёте один ученик превратится в трёх.
Три родственных метода обрезки:
| Метод | Что делает |
|---|---|
.strip() | срезает пробелы с обеих сторон |
.lstrip() | только слева (left) |
.rstrip() | только справа (right) — им чистят «хвост» строк из CSV |
Отдельно про регистр в именах:
.title() поднимает регистр после каждого нецифробуквенного символа, поэтому двойные фамилии обрабатывает правильно. Но на аббревиатурах он ломается: 'МКОУ СОШ №1'.title() вернёт 'Мкоу Сош №1' — название школы в отчёте испортится.
⚠️ Подводный камень
Строки в Python неизменяемы (immutable). Метод не правит переменную на месте, а возвращает новую строку. Traceback не появится — ошибка тихая, в отчёт просто уйдут неочищенные данные.
| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
| — (тихая ошибка) | «Строка неизменяема, метод вернул новое значение» | Вызвали .strip(), но результат никуда не записали. Любой строковый метод нужно присваивать: name = name.strip(). Проверять результат удобно в квадратных скобках [{name}] — так видно границы строки и остатки пробелов. |
Правильный паттерн
Пояснение логики: цепочка .strip().title() читается слева направо — сначала обрезали края, потом привели регистр — и результат сохранён в новую переменную с говорящим именем. Исходный raw_name при этом цел: если очистка окажется неверной, всегда видно, что именно пришло из выгрузки.
⚠️ Одного .strip() мало: он чистит только края. Двойные пробелы внутри строки ('иванов иван') остаются — с ними разберёмся в разделе 3.4.
3.2 Поиск и замена: .replace(), .find() и оператор in
Зачем это нужно. В выгрузках предмет называют как попало: матеша, матем., Математика (проф). Плюс приходят комментарии проверяющих, в которых нужно найти ключевые слова — например, апелляция.
Полезные детали:
.replace()меняет все вхождения сразу, а не первое:'а-а-а'.replace('а', 'б')даст'б-б-б'..rfind()ищет с конца — нужен, когда разделитель встречается несколько раз.- Замены можно ставить в цепочку:
.replace('матеша', 'Математика').replace(' ', ' ').
⚠️ Подводный камень
.find() при отсутствии совпадения возвращает -1, а не False. А -1 для Python — истина (Truthy), потому что ноль — единственное «ложное» число. Условие срабатывает всегда.
Требуется разбор апелляции| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
| — (тихая ошибка) | «-1 — не ноль, значит условие истинно» | Использовали .find() как проверку «есть или нет». Для проверки наличия существует оператор in, он возвращает честный bool. .find() берите только тогда, когда нужен именно номер позиции, и сравнивайте его явно: if position != -1:. |
Вторая ловушка того же семейства — регистр. .replace() и in ищут посимвольно, без всякой догадливости:
Правильный паттерн
Пояснение логики: in отвечает на вопрос «есть ли», .find() — на вопрос «где именно». Перед заменой строка приводится к нижнему регистру, поэтому один шаблон 'матеша' покрывает и Матеша, и МАТЕША. Красивый вид наводится последним шагом.
3.3 Индексация и срезы: извлечение частей текста [start:stop:step]
Зачем это нужно. Технические коды в выгрузках — это склеенные поля: '77-2024-001234' — регион, год, номер бланка. Срезы позволяют достать нужный фрагмент без ручного разбора, а также собрать инициалы из имени.
Границы можно опускать, а третий параметр задаёт шаг:
⚠️ Обратите внимание: blank_code[2:6] берёт четыре символа, а не пять. Правая граница не включается — это то же правило, что и в range(), которое встретится в главе Циклы.
⚠️ Подводный камень
Строгая индексация падает с IndexError, если строка короче, чем вы ожидали. В выгрузке это норма: незаполненная ячейка приходит как пустая строка ''.
Traceback (most recent call last): File "report.py", line 2, in <module> initial = student_name[0]IndexError: string index out of range| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
IndexError | «Запрошенный индекс за пределами длины строки» | Обратились к символу, которого нет: строка пустая или короче ожидаемого. В строке из 6 символов последний индекс — 5, а не 6. Проверяйте len() перед строгим индексом либо используйте срез. |
Правильный паттерн
Срез, в отличие от индекса, не падает — на короткой строке он просто вернёт то, что есть, а на пустой — пустую строку.
Пояснение логики: text[:1] — «безопасный первый символ»: результат либо буква, либо пустая строка, но программа продолжает работать. Там, где короткие данные нужно поймать и отметить в отчёте, ставится проверка len(). Полноценная защита кривых данных — в главе Ошибки и исключения.
3.4 Разбиение и сборка: .split() и .join()
Зачем это нужно. ФИО в выгрузке чаще всего лежит в одной колонке — 'Иванов Иван Иванович' — а для отчёта нужны отдельные поля «Фамилия», «Имя», «Отчество». Обратная задача — собрать из них короткую подпись 'Иванов И. И.'.
Разделитель задаётся явно — так вручную разбирают строку CSV:
⚠️ Синтаксис .join() поначалу выглядит вывернутым: метод вызывается у разделителя, а список передаётся аргументом. Читайте так: «склей элементы, поставив между ними вот это».
⚠️ Подводный камень
.split() без аргументов сам справляется с любым количеством пробелов. А вот .split(' ') с явным пробелом честно режет по каждому — и из двойного пробела рождается пустой элемент ''.
| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
| — (тихая ошибка) | «Режу строго по каждому символу-разделителю» | Указали .split(' ') там, где нужен .split(). Пустые элементы съезжают по индексам, и вместо имени в отчёт попадает пустота. Для ФИО и любого текста с пробелами вызывайте .split() без аргументов. |
Вторая ловушка — распаковка списка в переменные. Если в ФИО нет отчества, строк в списке окажется меньше, чем переменных слева:
Traceback (most recent call last): File "report.py", line 2, in <module> surname, first_name, patronymic = full_name.split()ValueError: not enough values to unpack (expected 3, got 2)| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
ValueError | «Слева три переменные, справа два значения» | Распаковка требует точного совпадения количества. В реальной выгрузке отчество есть не у всех — либо обращайтесь по индексам с проверкой len(parts), либо распаковывайте только гарантированные поля. |
Правильный паттерн
Пояснение логики: .split() без аргументов — универсальный инструмент для текста, набранного людьми: он игнорирует и краевые пробелы, и двойные, и табуляции. Связка ' '.join(text.split()) — готовый рецепт «нормализовать пробелы внутри строки», который решает проблему, оставшуюся из раздела 3.1. Явный разделитель .split(';') уместен только там, где формат строгий — например, в CSV.
3.5 Форматирование в отчётах: спецификаторы f-строк
Зачем это нужно. Очищенный текст — это полдела: его ещё надо поставить в строку отчёта так, чтобы колонки не разъезжались, а средний балл не печатался как 27.666666666666668. За это отвечает вторая половина f-строки — то, что пишется после двоеточия: {value:спецификатор}.
Базовый синтаксис f-строк разбирался в главе Переменные и ввод данных (раздел 1.2), а :.2f уже выручал при округлении в разделе 2.2 главы Числа и вычисления. Здесь собираем всё вместе — ради главной задачи аналитика — ровной таблицы в консоли:
ФИО Балл СтатусИванов Иван Петрович 87.5 сдалКим Ю 62.0 сдалПетрова-Водкина Анна 41.2 не сдал| Спецификатор | Что делает | Где нужен |
|---|---|---|
:.2f | фиксированная точность | средний балл, прирост |
:.1% | доля → проценты со знаком | конверсия, доля сдавших |
:<20 | текст влево, добить пробелами до 20 | колонка ФИО |
:>6 | значение вправо | числовые колонки |
:^10 | по центру | заголовки |
:, | разделитель тысяч (1,250) | количество работ |
⚠️ Подводный камень
Числовой спецификатор нельзя применять к строке. Из выгрузки балл приходит текстом ('87.5' — это str, помним из раздела 3.4), и :.1f на нём падает.
Traceback (most recent call last): File "report.py", line 3, in <module> print(f'Средний балл: {score_from_csv:.1f}')ValueError: Unknown format code 'f' for object of type 'str'| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
ValueError | «Код формата f не подходит объекту типа str» | Форматируете как число то, что осталось строкой после чтения выгрузки. Приведите тип до вывода: float(score_from_csv). Спецификаторы .f, .%, , работают только с int и float; строке доступно лишь выравнивание :<, :>, :^. |
Вторая ловушка — тихая: ширина поля не обрезает длинное значение. Если ФИО длиннее выделенных 24 символов, строка просто раздвинет колонку, и таблица поедет.
Правильный паттерн
Константинопольский Ко 87.5Пояснение логики: приведение типа — задача слоя чтения данных, форматирование — задача слоя вывода; если смешать их, ValueError вылезет в самой последней строке скрипта, когда все расчёты уже сделаны. А связка «срез плюс выравнивание» (name[:22] и :<24) гарантирует, что ширина колонки известна заранее: два пробела зазора остаются даже у самой длинной фамилии.
3.6 Мини-словарик ошибок третьей главы
| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
IndexError | «Индекс за пределами длины строки» | Взяли text[0] у пустой ячейки выгрузки. Используйте безопасный срез text[:1] или проверку len(). |
AttributeError | «У объекта int / None нет метода lower» | Вызвали строковый метод у числа или пустого значения из выгрузки. Приведите к строке: str(value).lower(). |
TypeError | «join ждал str, получил int» | Передали в .join() список с числами. Элементы нужно заранее превратить в строки. |
ValueError | «Слева три переменные, справа два значения» | Распаковали .split() в фиксированное число переменных, а отчества в ФИО не оказалось. |
ValueError | «Код формата f не подходит объекту типа str» | Применили :.1f к значению, которое после чтения выгрузки осталось строкой. Приведите к float() до вывода. |
| — (тихая ошибка) | «Строка неизменяема, метод вернул новое значение» | Написали text.strip() вместо text = text.strip() — очистка не сохранилась. |
| — (тихая ошибка) | «-1 — не ноль, значит условие истинно» | Проверили наличие подстроки через .find(). Для проверки нужен in. |
| — (тихая ошибка) | «Режу по каждому пробелу» | .split(' ') вместо .split() — в списке появились пустые элементы ''. |
| — (тихая ошибка) | «Ширина поля — минимум, а не максимум» | Значение длиннее :<24 раздвинуло колонку, и таблица отчёта поехала. Подрезайте срезом до вывода. |
Самый частый случай целиком — AttributeError. Он ловит момент, когда в текстовой колонке внезапно оказалось число: например, класс записан как 11 без литеры.
Traceback (most recent call last): File "report.py", line 2, in <module> print(student_class.lower())AttributeError: 'int' object has no attribute 'lower'Лечится приведением типа в момент чтения данных — тем же приёмом, что и int(input()) из главы 1, только в обратную сторону:
Мини-кейс: нормализатор ФИО и генератор краткого отчёта
Задача: принять «грязную» строку ФИО от преподавателя и технический код записи, очистить их, сформировать корректную подпись «Иванов И. П.» и извлечь из кода год, класс и регион.
Пример работы при вводе иванов иван петрович и grp-2024-11а-77:
--- КАРТОЧКА УЧЕНИКА ---Полное ФИО: Иванов Иван ПетровичПодпись в ведомость: Иванов И. П.Ключ для сверки: иванов иван петровичКод записи: GRP-2024-11А-77Год: 2024 | Класс: 11А | Регион: 77Что получилось: одна строка ввода дала три представления одних данных, и каждое решает свою задачу — full_fio для печати, short_fio для ведомости, sort_key для сопоставления с другой выгрузкой. Пробелы нормализованы связкой ' '.join(raw_fio.split()), а не одним .strip(), поэтому двойные пробелы внутри строки не помешали разбору. Инициалы взяты безопасным срезом [:1], а не индексом [0]: на пустой части ФИО скрипт не упадёт с IndexError.
⚠️ Одна дыра осталась: если преподаватель введёт ФИО без отчества, name_parts[2] даст IndexError. Проверка if len(name_parts) == 3: закроет её — оператор if разбирается в главе Условия.
Чек-лист главы
- Помню, что строки неизменяемы, и всегда присваиваю результат:
name = name.strip() - Чищу края через
.strip(), а регистр нормализую.lower()для ключей и.title()для отчётов - Знаю, что
.title()портит аббревиатуры, а.capitalize()— двойные фамилии - Проверяю наличие подстроки через
in, а не через.find(), который возвращает-1 - Помню, что
.replace()иinчувствительны к регистру, и привожу регистр до поиска - Умею резать код записи срезами
[start:stop:step]и помню, что правая граница не включается - Использую безопасный срез
text[:1]вместоtext[0], чтобы не пойматьIndexError - Разбираю ФИО через
.split()без аргументов и знаю, чем плох.split(' ') - Собираю строку обратно через
' '.join(list_of_parts) - Форматирую числа в отчёте спецификаторами
:.2fи:.1%, а не округляю на каждом шаге - Выравниваю колонки через
:<24и:>6, а длинный текст подрезаю срезом до вывода - Помню, что
:.1fдля строки — этоValueError: сначалаfloat(), потом вывод - Узнаю
AttributeErrorи понимаю, что в текстовую колонку попало число илиNone
Дальше: Списки