Почему строкам отдельный урок
В прошлом уроке мы сказали, что в Python 3 строка — это последовательность
Этот урок про две темы: методы строк (которые вы будете применять каждый день) и кодировки (которые ломают каждый второй ETL на первой неделе работы).
Методы строк — арсенал
В Python у str много встроенных методов. Запоминать все наизусть не нужно — IDE подскажет. Знать про существование — нужно.
text = " Hello, World! "
# чистка
print(text.strip()) # "Hello, World!" — убрать пробелы с краёв
print(text.lstrip()) # "Hello, World! " — только слева
print(text.rstrip()) # " Hello, World!" — только справа
print("xxxhelloxxx".strip("x")) # "hello" — убрать любые из набора
# регистр
print("ABC".lower()) # "abc"
print("abc".upper()) # "ABC"
print("hello world".title()) # "Hello World"
# поиск
print("hello".startswith("he")) # True
print("hello.csv".endswith(".csv")) # True
print("hello".find("ll")) # 2 — индекс или -1
print("hello".count("l")) # 2
# замена
print("a,b,c".replace(",", ";")) # "a;b;c"
# разбиение и склейка
print("a,b,c".split(",")) # ['a', 'b', 'c']
print(",".join(["a", "b", "c"])) # "a,b,c"
split и join — главные методы DE для разбора текстов и сборки выходных строк. Запомните схему:
parts = "Анна;30;Москва".split(";") # ['Анна', '30', 'Москва']
back = ";".join(parts) # 'Анна;30;Москва'
split() без аргумента — особый случай: разбивает по любым пробелам/табам/переносам строк, причём схлопывает их. То есть "a b\tc".split() вернёт ['a', 'b', 'c']. С аргументом split(" ") поведение другое: точно один пробел, и пустые элементы сохраняются.
Поиск подстрок через find возвращает индекс или -1. Чаще удобнее проверять через in:
if "error" in line:
log.warning(line)
f-strings — единственный способ форматировать
В Python было четыре способа собрать строку из переменных:
- Конкатенация:
"Hello, " + name + "!" - Старый процентный формат:
"Hello, %s!" % name - Метод
.format():"Hello, {}!".format(name) - :f-strings
f"Hello, {name}!"— рекомендуемый способ с 2016 года.
f-strings выигрывают по всем критериям: читаемость, скорость, гибкость. Запоминаем — и забываем остальное.
name = "Анна"
age = 30
# простая подстановка
print(f"Привет, {name}! Тебе {age} лет.")
# выражения внутри {...}
print(f"Через год тебе будет {age + 1}.")
print(f"Имя в верхнем регистре: {name.upper()}.")
print(f"Длина имени: {len(name)}.")
# вызовы функций — пожалуйста
def greet(n: str) -> str:
return f"Hello, {n}"
print(f"Из функции: {greet(name)}")
Внутри {...} можно писать любое выражение, которое возвращает значение — арифметику, методы, вызовы, тернарный оператор. Не нужно — присваивания, циклы, многострочные конструкции.
Форматирование чисел
После имени переменной можно поставить : и
price = 1234.5678
print(f"{price:.2f}") # "1234.57" — два знака после точки
print(f"{price:,.2f}") # "1,234.57" — с разделителем тысяч
print(f"{price:>10.2f}") # " 1234.57" — выровнено по правому краю, ширина 10
print(f"{price:<10.2f}") # "1234.57 " — по левому
print(f"{price:^10.2f}") # " 1234.57 " — по центру
print(f"{price:+.2f}") # "+1234.57" — всегда со знаком
count = 42
print(f"{count:05d}") # "00042" — паддинг нулями до 5 знаков
print(f"{count:#b}") # "0b101010" — в двоичной системе
print(f"{count:#x}") # "0x2a" — в шестнадцатеричной
ratio = 0.847
print(f"{ratio:.1%}") # "84.7%" — как процент
Это удобно для логов, отчётов, временных файлов. Понадобится в каждом ETL.
Форматирование дат
Для datetime можно прямо в f-string писать
from datetime import datetime, date
now = datetime.now()
print(f"{now:%Y-%m-%d}") # "2026-05-13"
print(f"{now:%H:%M:%S}") # "14:30:45"
print(f"{now:%Y-%m-%dT%H:%M:%S}") # "2026-05-13T14:30:45" — ISO 8601
# партиция по дате — типовая DE-задача
today = date.today()
path = f"data/raw/year={today:%Y}/month={today:%m}/day={today:%d}/events.parquet"
print(path) # "data/raw/year=2026/month=05/day=13/events.parquet"
Debug-формат = (3.8+)
Невероятно удобная фишка для дебага — добавьте = после имени переменной:
x = 42
name = "Анна"
print(f"{x=}, {name=}")
# Output: x=42, name='Анна'
Это эквивалент f"x={x!r}, name={name!r}". Имя переменной попадает в строку автоматически. В двухчасовом debug-сессии экономит десятки минут.
str и bytes — что когда
Внутри Python мы работаем с str. На границе с внешним миром появляются bytes. Понимать переход — обязательная часть работы DE.
Внутри программы — символы (str). Снаружи (файл, сеть) — байты (bytes). Переход через кодировку.
Базовый переход:
text = "Привет"
# str → bytes (для записи в файл, отправки по сети)
utf8_bytes = text.encode("utf-8")
print(utf8_bytes) # b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
print(len(utf8_bytes)) # 12 байт
cp1251_bytes = text.encode("cp1251")
print(cp1251_bytes) # b'\xcf\xf0\xe8\xe2\xe5\xf2'
print(len(cp1251_bytes)) # 6 байт
# bytes → str (для парсинга прочитанного из файла или из ответа API)
back = utf8_bytes.decode("utf-8")
print(back) # "Привет"
Кодировка — это соглашение, как символы превращаются в байты и обратно. Та же буква «П» — это байт 0xCF в cp1251 и два байта 0xD0 0x9F в UTF-8. Если читать UTF-8-файл как cp1251 — получите крокозябры. И наоборот.
Кодировки в реальном мире
Какие кодировки вам встретятся как junior DE:
- UTF-8 — стандарт XXI века. Любой современный API, любой свежий CSV, любой JSON — UTF-8. На Linux/macOS системная кодировка тоже UTF-8. Если не сказано иное — это UTF-8.
- cp1251 (Windows-1251) — старый русскоязычный стандарт. CSV-выгрузки из 1С, Excel в дефолтной локали русской Windows до 2018, базы Access.
- latin-1 (ISO-8859-1) — старый европейский стандарт. Встречается в старых англо/немецких/французских данных. Особенно полезен «трюк»: latin-1 — единственная кодировка, в которой ровно 256 символов и любые байты декодируются без ошибки. Иногда используется как «прозрачное» преобразование bytes → str.
- UTF-16 — внутреннее представление строк в Windows API и в Java. В файлах редко, но встречается у систем, экспортирующих из Excel «как unicode».
- ASCII — подмножество всех остальных, только латинские буквы + цифры + базовая пунктуация. На практике как отдельная кодировка почти не встречается — её всегда заменяет UTF-8.
DE-кейс: CSV из 1С в cp1251
Самый частый сценарий. Вам прислали CSV выгрузку. Открываете в Python:
# Наивный подход — упадёт или выдаст мусор
with open("export.csv", "r") as f:
text = f.read()
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcf in position 0
По умолчанию Python пытается прочитать как UTF-8 (если на macOS/Linux). Файл в cp1251 → ошибка. Что делаем:
with open("export.csv", "r", encoding="cp1251") as f:
text = f.read()
print(text)
# Теперь читается нормально
И всегда сразу сохраняем в UTF-8 для дальнейшей работы. Договорённость команды: в нашем хранилище — только UTF-8.
# Читаем как cp1251, записываем как UTF-8
with open("export.csv", "r", encoding="cp1251") as src:
text = src.read()
with open("export_utf8.csv", "w", encoding="utf-8") as dst:
dst.write(text)
Параметр encoding= в open() обязателен для текстовых файлов в production-коде. Полагаться на «системную дефолтную» — путь к багам, которые проявятся только на машине с другой локалью. Pylint/ruff будут ругаться, если вы забудете.
Что делать, если кодировка неизвестна
Иногда отправитель сам не знает, в чём файл. Тогда полезна библиотека charset-normalizer (преемник chardet):
from charset_normalizer import detect
with open("mystery.csv", "rb") as f: # читаем как bytes
raw = f.read()
result = detect(raw)
print(result["encoding"]) # например, 'Windows-1251'
text = raw.decode(result["encoding"])
Это эвристика, не магия. На длинных файлах работает почти всегда, на коротких может ошибиться. В продакшен-ETL — фиксируйте кодировку в конфиге, эвристику используйте только при первичной разведке.
errors=
Если в bytes попали байты, не валидные для указанной кодировки, по умолчанию .decode() падает. У него есть второй параметр errors=:
broken = b"hello\xff\xfeworld" # \xff\xfe — не валидный UTF-8
# вариант 1 — упадёт
broken.decode("utf-8")
# UnicodeDecodeError
# вариант 2 — заменить кракозябру на U+FFFD
broken.decode("utf-8", errors="replace")
# 'hello��world'
# вариант 3 — выбросить плохие байты
broken.decode("utf-8", errors="ignore")
# 'helloworld'
errors="replace" — компромисс для логов и отображения. errors="ignore" — почти всегда плохо, теряете данные молча. Для ETL-пайплайнов лучшая стратегия — поймать UnicodeDecodeError и записать строку в карантин (об этом в уроке 08).
Сырые и многострочные строки
Две полезные мелочи, которые часто встречаются.
Сырая строка (r"...") отключает экранирование \. Незаменимо для regex и путей Windows:
# обычная — \n превращается в перенос строки
path = "C:\new\test"
print(path) # C:
# ew est — катастрофа
# raw — \n остаётся как есть
path = r"C:\new\test"
print(path) # C:\new\test
Многострочная строка через """...""" или '''...''':
sql = """
SELECT id, name
FROM users
WHERE country = 'RU'
"""
Удобно для длинных SQL, документации, шаблонов.
Упражнение
В терминале REPL (uv run python) выполните и объясните каждое выражение:
# 1. Что получится и почему?
" hello ".strip().upper()
# 2. Что получится и почему?
",".join(["a", "b", "c"]).split(",")
# 3. Что получится и почему?
f"{1234567:,}"
# 4. Что получится и почему?
"привет".encode("utf-8")
"привет".encode("cp1251")
len("привет".encode("utf-8"))
len("привет".encode("cp1251"))
# 5. Прочитайте файл в неизвестной кодировке (заранее запишем в cp1251):
# создайте файл
with open("test_cp1251.txt", "wb") as f:
f.write("Привет, мир!".encode("cp1251"))
# Прочитайте его как UTF-8 (увидите ошибку), потом как cp1251 (получите текст).
Критерии приёмки:
- Для каждого выражения вы знаете результат до запуска.
- Понимаете, почему
len()для одной и той же строки в cp1251 и UTF-8 даёт разные числа. - Умеете объяснить, что произойдёт, если в
errors="ignore"спрятать ошибку декодирования.
В следующем уроке — коллекции: list, dict, set, tuple. Тут начинается настоящая боль с mutable defaults и lookup-таблицами, которые DE строит каждый день.