Работа с Unicode и кодировками строк в Python

Раздел: Основы Python -> Кодирование строк

Основы работы с Unicode в Python

Python 3 использует тип str для представления текста в кодировке Unicode, а тип bytes для последовательностей байтов. Для преобразования между ними применяются методы .encode() и .decode(). Наиболее эффективное решение - всегда указывать кодировку явно, чаще всего utf-8, так как она поддерживает все символы Unicode и эффективна по памяти.

Пример базового преобразования:

text = 'Привет, мир!'
encoded = text.encode('utf-8')
print(encoded)
decoded = encoded.decode('utf-8')
print(decoded)

Python unicode (unicode в python)

b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82, \xd0\xbc\xd0\xb8\xd1\x80!'
Привет, мир!

Метод .encode() возвращает байтовый объект. Явное указание кодировки делает код предсказуемым и предотвращает ошибки на разных платформах.

Как использовать другую кодировку, например cp1251?

Иногда требуется работа с устаревшими кодировками:

text = 'Привет'
encoded_cp1251 = text.encode('cp1251')
print(encoded_cp1251)
decoded_cp1251 = encoded_cp1251.decode('cp1251')
print(decoded_cp1251)
b'\xcf\xf0\xe8\xe2\xe5\xf2'
Привет

Важно: не все символы Unicode представимы в cp1251, что вызовет ошибку.

Что делать, если символ не умещается в кодировку?

Параметр errors позволяет указать стратегию обработки:

text = 'Hello ☺'
# strict (по умолчанию) вызовет UnicodeEncodeError для ascii
try:
    text.encode('ascii')
except UnicodeEncodeError as e:
    print(f'Ошибка: {e}')
# ignore удаляет неподдерживаемые символы
print(text.encode('ascii', errors='ignore'))
# replace заменяет на вопросительный знак
print(text.encode('ascii', errors='replace'))
# backslashreplace использует escape-последовательности
print(text.encode('ascii', errors='backslashreplace'))
# xmlcharrefreplace заменяет на HTML-сущности
print(text.encode('ascii', errors='xmlcharrefreplace'))
Ошибка: 'ascii' codec can't encode character '\u263a' in position 6: ordinal not in range(128)
b'Hello '
b'Hello ?'
b'Hello \u263a'
b'Hello ☺'

Выбор стратегии зависит от задачи: ignore подходит для фильтрации, replace для сохранения длины строки.

Как привести строку к канонической форме?

Разные Unicode-символы могут выглядеть одинаково, но иметь разную кодировку (например, буква «ё» как один символ или как «е» + диакритический знак). Модуль unicodedata предоставляет нормализацию:

import unicodedata

s1 = 'café'           # один символ é
s2 = 'cafe\u0301'     # e + combining acute accent
print(s1 == s2)       # False
s2_nfc = unicodedata.normalize('NFC', s2)
print(s1 == s2_nfc)   # True
False
True

Формы NFC (составная) и NFD (разложенная) наиболее распространены. Нормализация полезна при сравнении строк и работе с текстом из разных источников.

Как работать с символами вне базовой плоскости (суррогатные пары)?

Эмодзи и редкие иероглифы занимают две 16-битные единицы. Python работает с ними корректно, но важно учитывать длину строки:

emoji = '?'   # U+1F600
print(len(emoji))          # 1 (символ)
print(len(emoji.encode('utf-8')))  # 4 байта
print(list(emoji))          # ['?']
1
4
['?']

При работе с индексами Python автоматически оперирует целыми кодовыми точками, поэтому срез emoji[0] вернёт весь смайлик, а не половину.

Типичные ошибки:

  • UnicodeEncodeError - попытка закодировать символ, отсутствующий в целевой кодировке. Решение: использовать ‘utf-8’ или указать подходящий errors.
  • UnicodeDecodeError - попытка декодировать байты с неверной кодировкой. Решение: определить правильную кодировку или использовать errors='replace'.
  • TypeError - смешивание str и bytes (например, конкатенация). Решение: явно преобразовывать к одному типу.
  • Путаница с длиной: len(str) считает символы, len(bytes) - байты. Следует помнить, что один символ может занимать от 1 до 4 байт в UTF-8.

Расширенные примеры работы с Unicode

Чтение и запись файлов с указанием кодировки

Пример
# Запись в файл в UTF-8
with open('hello.txt', 'w', encoding='utf-8') as f:
    f.write('Привет, мир! ☺')

# Чтение с другой кодировкой (например, латиница)
with open('hello.txt', 'r', encoding='utf-8') as f:
    content = f.read()
print(content)  # Привет, мир! ☺
Привет, мир! ☺

Если не указать encoding, будет использована системная кодировка, что может привести к ошибкам на разных ОС.

Автоматическое определение кодировки с помощью chardet

Пример
import chardet

# Некоторый байтовый поток (например, из файла)
b_data = b'\xff\xfeP\x00r\x00i\x00v\x00e\x00t\x00'
result = chardet.detect(b_data)
print(result)
# {'encoding': 'UTF-16LE', 'confidence': 1.0, 'language': ''}
decoded = b_data.decode(result['encoding'])
print(decoded)  # Привет
{'encoding': 'UTF-16LE', 'confidence': 1.0, 'language': ''}
Привет

Библиотека chardet полезна при работе с данными неизвестной кодировки.

Использование юникодных escape-последовательностей

Пример
# Представление строки через \u и \U
text = '\u041f\u0440\u0438\u0432\u0435\u0442'  # Привет
print(text)
# Для символов вне BMP используем \U0001F600
emoji = '\U0001F600'  # ?
print(emoji)
Привет
?

Этот подход удобен для вставки символов, которые нельзя набрать напрямую.

Нормализация в реальном сценарии: сравнение строк

Пример
import unicodedata

s1 = 'café'
s2 = 'cafe\u0301'  # канонически эквивалентная форма
# Прямое сравнение
print(s1 == s2)  # False
# Нормализация обеих строк к NFC
s1_nfc = unicodedata.normalize('NFC', s1)
s2_nfc = unicodedata.normalize('NFC', s2)
print(s1_nfc == s2_nfc)  # True
False
True

Без нормализации две казалось бы одинаковые строки могут считаться разными.

Регулярные выражения с Unicode

Пример
import re

text = 'Русский текст с цифрами 123 и эмодзи ?'
# Поиск всех слов (букв Unicode)
pattern = re.compile(r'\w+', re.UNICODE)
print(pattern.findall(text))
# ['Русский', 'текст', 'с', 'цифрами', '123', 'и', 'эмодзи']
['Русский', 'текст', 'с', 'цифрами', '123', 'и', 'эмодзи']

Флаг re.UNICODE заставляет \w совпадать с любыми буквами Unicode, включая кириллицу.

Работа с кодовыми точками напрямую

Пример
# Получение кода символа
code = ord('?')
print(hex(code))  # 0x1f600

# Создание символа из кода
char = chr(0x1f600)
print(char)  # ?

# Суррогатная пара (внутреннее представление Python скрывает суррогаты)
s = '\U0001F600'
print(len(s))  # 1
print(hex(ord(s)))  # 0x1f600
0x1f600
?
1
0x1f600

Функции ord() и chr() работают с полными кодовыми точками, а не с суррогатными единицами.

Кодирование с использованием модуля codecs

Пример
import codecs

# Кодирование в hex с разделителями
hex_str = codecs.encode('Привет', 'hex')
print(hex_str)  # b'd09f d180 d0b8 d0b2 d0b5 d182' (в зависимости от кодировки)

# Декодирование из unicode-escape
s = codecs.decode(b'\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442', 'unicode-escape')
print(s)  # Привет
b'd09fd180d0b8d0b2d0b5d182'
Привет

Модуль codecs предоставляет гибкие методы для работы с различными кодеками.

Обработка ошибок при декодировании с заменой

Пример
# Байты, содержащие как корректные, так и некорректные данные
b = b'\xc3\xa9\xc3\x28'  # первый байт é, второй - неполный
# strict
try:
    b.decode('utf-8')
except UnicodeDecodeError as e:
    print(f'Ошибка: {e}')
# replace
decoded = b.decode('utf-8', errors='replace')
print(decoded)  # é� (замена на �)
Ошибка: 'utf-8' codec can't decode byte 0xc3 in position 2: invalid continuation byte
é�

При работе с повреждёнными данными стратегия replace позволяет получить строку, не прерывая выполнение.

Unicode в Python - comments

En
Python unicode (python)