Learning Platform
Урок 04.02 · 25 мин
Начальный
Stringsf-stringsEncodingUTF-8bytes
CSV и кодировки в REST API

Почему строкам отдельный урок

В прошлом уроке мы сказали, что в Python 3 строка — это последовательность

кодовых точек Unicode
. Это и подарок, и проблема. Подарок — никаких багов с обрезанной кириллицей внутри программы. Проблема — на каждой границе с внешним миром (файл, сеть, БД, чужой API) нужно знать кодировку, в которой текст хранится снаружи. И знать не «вообще», а конкретно: ASCII, UTF-8, cp1251, latin-1, UTF-16. Когда вы как DE загружаете CSV из 2003 года, который кто-то выгрузил из 1С под Windows XP, кодировка — главное препятствие.

Этот урок про две темы: методы строк (которые вы будете применять каждый день) и кодировки (которые ломают каждый второй 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;Москва'
TIP

split() без аргумента — особый случай: разбивает по любым пробелам/табам/переносам строк, причём схлопывает их. То есть "a b\tc".split() вернёт ['a', 'b', 'c']. С аргументом split(" ") поведение другое: точно один пробел, и пустые элементы сохраняются.

Поиск подстрок через find возвращает индекс или -1. Чаще удобнее проверять через in:

if "error" in line:
    log.warning(line)

f-strings — единственный способ форматировать

В Python было четыре способа собрать строку из переменных:

  1. Конкатенация: "Hello, " + name + "!"
  2. Старый процентный формат: "Hello, %s!" % name
  3. Метод .format(): "Hello, {}!".format(name)
  4. 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)}")

Внутри {...} можно писать любое выражение, которое возвращает значение — арифметику, методы, вызовы, тернарный оператор. Не нужно — присваивания, циклы, многострочные конструкции.

Форматирование чисел

После имени переменной можно поставить : и

format spec
— описание, как печатать:

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 писать

strftime-коды
:

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: переход между внутренним и внешним миром

Внутри программы — символы (str). Снаружи (файл, сеть) — байты (bytes). Переход через кодировку.

str"Привет"Внутри Python — последовательность кодовых точек Unicode
что внутри[1055, 1088, 1080, ...]Кодовые точки символов: П=1055, р=1088...
.encode(enc)Превращает str → bytes по правилам кодировки
.decode(enc)Превращает bytes → str по правилам кодировки
bytes (UTF-8)b'\\xd0\\x9f\\xd1\\x80...'12 байт для слова из 6 русских букв — каждая в UTF-8 занимает 2 байта
bytes (cp1251)b'\\xcf\\xf0\\xe8\\xe2\\xe5\\xf2'6 байт для слова из 6 русских букв — однобайтовая кодировка

Базовый переход:

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)
WARNING

Параметр 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 строит каждый день.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 8