Skip to content

Глава 3. Текст и работа со строками: очистка данных и вывод в отчёт

Задача главы: научиться приводить «грязные» текстовые данные из выгрузок — ФИО с лишними пробелами, разный регистр, опечатки, слитные поля — к единому стандарту, пригодному для объединения таблиц, и выводить результат ровной строкой отчёта средствами f-строк.


3.1 Текстовый тип str и базовая зачистка: .strip(), .lower(), .upper(), .title()

Зачем это нужно. Выгрузки из разных систем — LMS, гугл-формы, база данных — приносят один и тот же человек в трёх написаниях: 'Иванов ', 'иванов иван', 'ИВАНОВ'. Для Python это три разных значения, поэтому таблицы не склеятся, а в отчёте один ученик превратится в трёх.

// PYTHON_EXECUTOR (Pyodide Wasm)

Три родственных метода обрезки:

МетодЧто делает
.strip()срезает пробелы с обеих сторон
.lstrip()только слева (left)
.rstrip()только справа (right) — им чистят «хвост» строк из CSV

Отдельно про регистр в именах:

// PYTHON_EXECUTOR (Pyodide Wasm)

.title() поднимает регистр после каждого нецифробуквенного символа, поэтому двойные фамилии обрабатывает правильно. Но на аббревиатурах он ломается: 'МКОУ СОШ №1'.title() вернёт 'Мкоу Сош №1' — название школы в отчёте испортится.

⚠️ Подводный камень

Строки в Python неизменяемы (immutable). Метод не правит переменную на месте, а возвращает новую строку. Traceback не появится — ошибка тихая, в отчёт просто уйдут неочищенные данные.

// PYTHON_EXECUTOR (Pyodide Wasm)
ОшибкаЧто значит для PythonЧеловеческий перевод
— (тихая ошибка)«Строка неизменяема, метод вернул новое значение»Вызвали .strip(), но результат никуда не записали. Любой строковый метод нужно присваивать: name = name.strip(). Проверять результат удобно в квадратных скобках [{name}] — так видно границы строки и остатки пробелов.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: цепочка .strip().title() читается слева направо — сначала обрезали края, потом привели регистр — и результат сохранён в новую переменную с говорящим именем. Исходный raw_name при этом цел: если очистка окажется неверной, всегда видно, что именно пришло из выгрузки.

⚠️ Одного .strip() мало: он чистит только края. Двойные пробелы внутри строки ('иванов иван') остаются — с ними разберёмся в разделе 3.4.


3.2 Поиск и замена: .replace(), .find() и оператор in

Зачем это нужно. В выгрузках предмет называют как попало: матеша, матем., Математика (проф). Плюс приходят комментарии проверяющих, в которых нужно найти ключевые слова — например, апелляция.

// PYTHON_EXECUTOR (Pyodide Wasm)

Полезные детали:

  • .replace() меняет все вхождения сразу, а не первое: 'а-а-а'.replace('а', 'б') даст 'б-б-б'.
  • .rfind() ищет с конца — нужен, когда разделитель встречается несколько раз.
  • Замены можно ставить в цепочку: .replace('матеша', 'Математика').replace(' ', ' ').

⚠️ Подводный камень

.find() при отсутствии совпадения возвращает -1, а не False. А -1 для Python — истина (Truthy), потому что ноль — единственное «ложное» число. Условие срабатывает всегда.

// PYTHON_EXECUTOR (Pyodide Wasm)
Требуется разбор апелляции
ОшибкаЧто значит для PythonЧеловеческий перевод
— (тихая ошибка)«-1 — не ноль, значит условие истинно»Использовали .find() как проверку «есть или нет». Для проверки наличия существует оператор in, он возвращает честный bool. .find() берите только тогда, когда нужен именно номер позиции, и сравнивайте его явно: if position != -1:.

Вторая ловушка того же семейства — регистр. .replace() и in ищут посимвольно, без всякой догадливости:

// PYTHON_EXECUTOR (Pyodide Wasm)

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: in отвечает на вопрос «есть ли», .find() — на вопрос «где именно». Перед заменой строка приводится к нижнему регистру, поэтому один шаблон 'матеша' покрывает и Матеша, и МАТЕША. Красивый вид наводится последним шагом.


3.3 Индексация и срезы: извлечение частей текста [start:stop:step]

Зачем это нужно. Технические коды в выгрузках — это склеенные поля: '77-2024-001234' — регион, год, номер бланка. Срезы позволяют достать нужный фрагмент без ручного разбора, а также собрать инициалы из имени.

// PYTHON_EXECUTOR (Pyodide Wasm)

Границы можно опускать, а третий параметр задаёт шаг:

// PYTHON_EXECUTOR (Pyodide Wasm)

⚠️ Обратите внимание: blank_code[2:6] берёт четыре символа, а не пять. Правая граница не включается — это то же правило, что и в range(), которое встретится в главе Циклы.

⚠️ Подводный камень

Строгая индексация падает с IndexError, если строка короче, чем вы ожидали. В выгрузке это норма: незаполненная ячейка приходит как пустая строка ''.

// PYTHON_EXECUTOR (Pyodide Wasm)
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() перед строгим индексом либо используйте срез.

Правильный паттерн

Срез, в отличие от индекса, не падает — на короткой строке он просто вернёт то, что есть, а на пустой — пустую строку.

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: text[:1] — «безопасный первый символ»: результат либо буква, либо пустая строка, но программа продолжает работать. Там, где короткие данные нужно поймать и отметить в отчёте, ставится проверка len(). Полноценная защита кривых данных — в главе Ошибки и исключения.


3.4 Разбиение и сборка: .split() и .join()

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

// PYTHON_EXECUTOR (Pyodide Wasm)

Разделитель задаётся явно — так вручную разбирают строку CSV:

// PYTHON_EXECUTOR (Pyodide Wasm)

⚠️ Синтаксис .join() поначалу выглядит вывернутым: метод вызывается у разделителя, а список передаётся аргументом. Читайте так: «склей элементы, поставив между ними вот это».

⚠️ Подводный камень

.split() без аргументов сам справляется с любым количеством пробелов. А вот .split(' ') с явным пробелом честно режет по каждому — и из двойного пробела рождается пустой элемент ''.

// PYTHON_EXECUTOR (Pyodide Wasm)
ОшибкаЧто значит для PythonЧеловеческий перевод
— (тихая ошибка)«Режу строго по каждому символу-разделителю»Указали .split(' ') там, где нужен .split(). Пустые элементы съезжают по индексам, и вместо имени в отчёт попадает пустота. Для ФИО и любого текста с пробелами вызывайте .split() без аргументов.

Вторая ловушка — распаковка списка в переменные. Если в ФИО нет отчества, строк в списке окажется меньше, чем переменных слева:

// PYTHON_EXECUTOR (Pyodide Wasm)
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), либо распаковывайте только гарантированные поля.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: .split() без аргументов — универсальный инструмент для текста, набранного людьми: он игнорирует и краевые пробелы, и двойные, и табуляции. Связка ' '.join(text.split()) — готовый рецепт «нормализовать пробелы внутри строки», который решает проблему, оставшуюся из раздела 3.1. Явный разделитель .split(';') уместен только там, где формат строгий — например, в CSV.


3.5 Форматирование в отчётах: спецификаторы f-строк

Зачем это нужно. Очищенный текст — это полдела: его ещё надо поставить в строку отчёта так, чтобы колонки не разъезжались, а средний балл не печатался как 27.666666666666668. За это отвечает вторая половина f-строки — то, что пишется после двоеточия: {value:спецификатор}.

// PYTHON_EXECUTOR (Pyodide Wasm)

Базовый синтаксис f-строк разбирался в главе Переменные и ввод данных (раздел 1.2), а :.2f уже выручал при округлении в разделе 2.2 главы Числа и вычисления. Здесь собираем всё вместе — ради главной задачи аналитика — ровной таблицы в консоли:

// PYTHON_EXECUTOR (Pyodide Wasm)
ФИО Балл Статус
Иванов Иван Петрович 87.5 сдал
Ким Ю 62.0 сдал
Петрова-Водкина Анна 41.2 не сдал
СпецификаторЧто делаетГде нужен
:.2fфиксированная точностьсредний балл, прирост
:.1%доля → проценты со знакомконверсия, доля сдавших
:<20текст влево, добить пробелами до 20колонка ФИО
:>6значение вправочисловые колонки
:^10по центрузаголовки
:,разделитель тысяч (1,250)количество работ

⚠️ Подводный камень

Числовой спецификатор нельзя применять к строке. Из выгрузки балл приходит текстом ('87.5' — это str, помним из раздела 3.4), и :.1f на нём падает.

// PYTHON_EXECUTOR (Pyodide Wasm)
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 символов, строка просто раздвинет колонку, и таблица поедет.

// PYTHON_EXECUTOR (Pyodide Wasm)

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)
Константинопольский Ко 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 без литеры.

// PYTHON_EXECUTOR (Pyodide Wasm)
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, только в обратную сторону:

// PYTHON_EXECUTOR (Pyodide Wasm)

Мини-кейс: нормализатор ФИО и генератор краткого отчёта

Задача: принять «грязную» строку ФИО от преподавателя и технический код записи, очистить их, сформировать корректную подпись «Иванов И. П.» и извлечь из кода год, класс и регион.

// PYTHON_EXECUTOR (Pyodide Wasm)

Пример работы при вводе иванов иван петрович и 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

Дальше: Списки