Unicode и кодировки символов в Python: простое руководство

Оглавление

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Unicode в Python: Работа с кодировками символов

Обработка кодировок символов в Python или любом другом языке иногда может показаться болезненной. В таких местах, как Stack Overflow, возникают тысячи вопросов, связанных с путаницей в отношении исключений, таких как UnicodeDecodeError и UnicodeEncodeError. Это руководство предназначено для того, чтобы прояснить ситуацию с Exception и показать, что работа с текстовыми и двоичными данными в Python 3 может быть простой. Поддержка Unicode в Python является надежной, но для ее освоения требуется некоторое время.

Это учебное пособие отличается тем, что оно не зависит от языка, а намеренно ориентировано на Python. Вы по-прежнему получите учебник для начинающих, не зависящий от языка, но затем погрузитесь в иллюстрации на Python, при этом количество абзацев, перегруженных текстом, будет сведено к минимуму. Вы увидите, как использовать концепции кодировки символов в живом коде на Python.

К концу этого урока вы поймете:

  • Получите концептуальный обзор кодировок символов и систем нумерации
  • Понять, как кодирование взаимодействует с str Python и bytes
  • Узнайте о поддержке в Python систем нумерации с помощью различных форм int литералов
  • Быть знакомым со встроенными функциями Python, связанными с кодировками символов и системами нумерации

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

Примечание: Эта статья ориентирована на Python 3. В частности, все примеры кода в этом руководстве были сгенерированы с помощью оболочки CPython 3.7.2, хотя все второстепенные версии Python 3 должны вести себя (в основном) одинаково при обработке текста.

Если вы все еще используете Python 2 и вас пугают различия в том, как Python 2 и Python 3 обрабатывают текстовые и двоичные данные, то, надеюсь, это руководство поможет вам переключиться.

Скачать бесплатно: Ознакомьтесь с примером главы из книги "Приемы работы с Python: Книга", в которой показаны лучшие практики Python на простых примерах, которые вы можете подайте заявку немедленно, чтобы написать более красивый + Pythonic код.

Что такое кодировка символов?

Существуют десятки, если не сотни кодировок символов. Лучший способ начать понимать, что это такое, - изучить одну из простейших кодировок символов, ASCII.

Независимо от того, являетесь ли вы самоучкой или имеете специальное образование в области компьютерных наук, есть вероятность, что вы видели таблицу ASCII один или два раза. ASCII - это хорошее место для начала изучения кодировки символов, поскольку это небольшая и лаконичная кодировка. (Как оказалось, слишком маленький.)

Он включает в себя следующее:

  • Строчные английские буквы: от до z
  • Заглавные английские буквы: От до Z
  • Некоторые знаки препинания и символы: "$" и "!", чтобы назвать пару
  • Пробельные символы: настоящий пробел (" "), а также символы новой строки, возврата каретки, горизонтальной и вертикальной табуляции и некоторые другие
  • Некоторые непечатаемые символы: такие символы, как пробел, "\b", которые нельзя напечатать буквально так, как букву A может

Итак, каково более формальное определение кодировки символов?

На очень высоком уровне это способ преобразования символов (таких как буквы, знаки препинания, пробелы и управляющие символы) в целые числа и, в конечном счете, в биты. Каждый символ может быть закодирован в уникальную последовательность битов. Не волнуйтесь, если у вас нет четких представлений о битах, потому что мы скоро к ним вернемся.

Различные категории, указанные в таблице, представляют собой группы символов. Каждому отдельному символу соответствует кодовая точка, которую можно представить просто как целое число. Символы разбиты на различные диапазоны в таблице ASCII:

Code Point Range Class
0 through 31 Control/non-printable characters
32 through 64 Punctuation, symbols, numbers, and space
65 through 90 Uppercase English alphabet letters
91 through 96 Additional graphemes, such as [ and \
97 through 122 Lowercase English alphabet letters
123 through 126 Additional graphemes, such as { and |
127 Control/non-printable character (DEL)

Вся таблица ASCII содержит 128 символов. В этой таблице представлен полный набор символов , который разрешен в ASCII. Если вы не видите здесь символ, то вы просто не можете представить его в виде печатного текста в соответствии со схемой кодировки ASCII.

Code Point Character (Name) Code Point Character (Name)
0 NUL (Null) 64 @
1 SOH (Start of Heading) 65 A
2 STX (Start of Text) 66 B
3 ETX (End of Text) 67 C
4 EOT (End of Transmission) 68 D
5 ENQ (Enquiry) 69 E
6 ACK (Acknowledgment) 70 F
7 BEL (Bell) 71 G
8 BS (Backspace) 72 H
9 HT (Horizontal Tab) 73 I
10 LF (Line Feed) 74 J
11 VT (Vertical Tab) 75 K
12 FF (Form Feed) 76 L
13 CR (Carriage Return) 77 M
14 SO (Shift Out) 78 N
15 SI (Shift In) 79 O
16 DLE (Data Link Escape) 80 P
17 DC1 (Device Control 1) 81 Q
18 DC2 (Device Control 2) 82 R
19 DC3 (Device Control 3) 83 S
20 DC4 (Device Control 4) 84 T
21 NAK (Negative Acknowledgment) 85 U
22 SYN (Synchronous Idle) 86 V
23 ETB (End of Transmission Block) 87 W
24 CAN (Cancel) 88 X
25 EM (End of Medium) 89 Y
26 SUB (Substitute) 90 Z
27 ESC (Escape) 91 [
28 FS (File Separator) 92 \
29 GS (Group Separator) 93 ]
30 RS (Record Separator) 94 ^
31 US (Unit Separator) 95 _
32 SP (Space) 96 `
33 ! 97 a
34 " 98 b
35 # 99 c
36 $ 100 d
37 % 101 e
38 & 102 f
39 ' 103 g
40 ( 104 h
41 ) 105 i
42 * 106 j
43 + 107 k
44 , 108 l
45 - 109 m
46 . 110 n
47 / 111 o
48 0 112 p
49 1 113 q
50 2 114 r
51 3 115 s
52 4 116 t
53 5 117 u
54 6 118 v
55 7 119 w
56 8 120 x
57 9 121 y
58 : 122 z
59 ; 123 {
60 < 124 |
61 = 125 }
62 > 126 ~
63 ? 127 DEL (delete)

Модуль string

Модуль string в Python - это удобный универсальный инструмент для строковых констант, которые входят в набор символов ASCII.

Вот ядро модуля во всей его красе:

# From lib/python3.7/string.py

whitespace = ' \t\n\r\v\f'
ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ascii_letters = ascii_lowercase + ascii_uppercase
digits = '0123456789'
hexdigits = digits + 'abcdef' + 'ABCDEF'
octdigits = '01234567'
punctuation = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
printable = digits + ascii_letters + punctuation + whitespace


Большинство из этих констант должны быть самодокументированы в своем идентификаторе. Вскоре мы рассмотрим, что такое hexdigits и octdigits.

Вы можете использовать эти константы для повседневных манипуляций со строками:

>>> import string

>>> s = "What's wrong with ASCII?!?!?"
>>> s.rstrip(string.punctuation)
'What's wrong with ASCII'


Примечание: string.printable включает в себя все string.whitespace. Это немного расходится с другим методом проверки того, считается ли символ пригодным для печати, а именно str.isprintable(), который покажет вам, что ни один из {'\v', '\n', '\r', '\f', '\t'} не считается пригодным для печати.

Тонкое различие заключается в определении: str.isprintable() считает что-либо пригодным для печати, если “все его символы считаются пригодными для печати в repr().”

Немного освежаю в памяти

Сейчас самое подходящее время для краткого ознакомления с битом, самой фундаментальной единицей информации, известной компьютеру.

Бит - это сигнал, который имеет только два возможных состояния. Существуют различные способы символического представления бита, которые все означают одно и то же:

  • 0 или 1
  • “да” или “нет”
  • True или False
  • “вкл.” или “выкл.”

В нашей таблице ASCII из предыдущего раздела используется то, что мы с вами назвали бы просто числами (от 0 до 127), но что более точно называется числами с основанием 10 (десятичными).

Вы также можете выразить каждое из этих чисел по основанию 10 с помощью последовательности битов (по основанию 2). Вот двоичные варианты значений от 0 до 10 в десятичной системе счисления:

Decimal Binary (Compact) Binary (Padded Form)
0 0 00000000
1 1 00000001
2 10 00000010
3 11 00000011
4 100 00000100
5 101 00000101
6 110 00000110
7 111 00000111
8 1000 00001000
9 1001 00001001
10 1010 00001010

Обратите внимание, что по мере увеличения десятичного числа n требуется больше значащих битов для представления символа, установленного до этого числа и включающего это число.

Вот удобный способ представления строк ASCII в виде последовательностей битов в Python. Каждый символ из строки ASCII псевдокодируется в 8 бит с пробелами между 8-битными последовательностями, каждая из которых представляет один символ:

>>> def make_bitseq(s: str) -> str:
...     if not s.isascii():
...         raise ValueError("ASCII only allowed")
...     return " ".join(f"{ord(i):08b}" for i in s)

>>> make_bitseq("bits")
'01100010 01101001 01110100 01110011'

>>> make_bitseq("CAPS")
'01000011 01000001 01010000 01010011'

>>> make_bitseq("$25.43")
'00100100 00110010 00110101 00101110 00110100 00110011'

>>> make_bitseq("~5")
'01111110 00110101'


Примечание: .isascii() было введено в Python 3.7.

В f-строке f"{ord(i):08b}" используется мини-язык спецификации формата Python , который является способом задание форматирования для заменяющих полей в строках формата:

  • Левая сторона двоеточия, ord(i), является фактическим объектом, значение которого будет отформатировано и вставлено в выходные данные. Использование функции Python ord() дает вам кодовую точку с основанием 10 для одного символа str.

  • В правой части двоеточия указан спецификатор формата. 08 означает ширина 8, заполнение 0, а b служит знаком для вывода результирующего числа по основанию 2 (двоичное).

Этот трюк, в основном, просто для развлечения, и он не сработает для любого символа, которого нет в таблице ASCII. Позже мы обсудим, как другие кодировки устраняют эту проблему.

Нам нужно больше Битов!

Существует критически важная формула, связанная с определением бита. При заданном количестве битов, n, количество различных возможных значений, которые могут быть представлены в n битах, равно 2<<>n:

def n_possible_values(nbits: int) -> int:
    return 2 ** nbits


Вот что это значит:

  • 1 бит позволит вам выразить 21 == 2 возможные значения.
  • 8 битов позволят вам выразить 28 == 256 возможные значения.
  • 64 бита позволят вам выразить 2<<64> == 18,446,744,073,709,551,616 возможные значения.

Из этой формулы следует вывод: учитывая диапазон различных возможных значений, как мы можем найти количество битов, n, которое требуется для полного представления диапазона? То, для чего вы пытаетесь решить, - это n в уравнении 2n = x (где вы уже знаете x).

Вот к чему это приводит:

>>> from math import ceil, log

>>> def n_bits_required(nvalues: int) -> int:
...     return ceil(log(nvalues) / log(2))

>>> n_bits_required(256)
8


Причина, по которой вам нужно использовать верхний предел в n_bits_required(), заключается в том, что необходимо учитывать значения, которые не являются чистыми степенями 2. Допустим, вам нужно сохранить набор символов, состоящий всего из 110 символов. По наивности, это должно занимать log(110) / log(2) == 6.781 бит, но такого понятия, как 0,781 бит, не существует. Для 110 значений потребуется 7 бит, а не 6, при этом конечные интервалы не нужны:

>>> n_bits_required(110)
7


Все это служит доказательством одной концепции: ASCII - это, строго говоря, 7-разрядный код. Таблица ASCII, которую вы видели выше, содержит 128 кодовых точек и символов от 0 до 127 включительно. Для этого требуется 7 бит:

>>> n_bits_required(128)  # 0 through 127
7
>>> n_possible_values(7)
128


Проблема в том, что современные компьютеры практически ничего не хранят в 7-разрядных слотах. Они передают данные в виде блоков по 8 бит, обычно называемых байт.

Примечание: На протяжении всего этого руководства я предполагаю, что байт относится к 8 битам, как это было с 1960-х годов, а не к какой-либо другой единице хранения. Вы можете называть это число октетом, если хотите.

Чтобы узнать больше об объектах bytes в Python, ознакомьтесь с руководством Объекты Bytes: обработка двоичных данных в Python.

Это означает, что место для хранения, используемое ASCII, наполовину пусто. Если вам непонятно, почему это так, вспомните таблицу преобразования десятичной дроби в двоичную, приведенную выше. Вы можете выразить числа 0 и 1 всего с помощью 1 бита, или вы можете использовать 8 битов, чтобы выразить их как 00000000 и 00000001 соответственно.

Вы можете выразить числа от 0 до 3 всего двумя битами или от 00 до 11, или вы можете использовать 8 битов, чтобы выразить их как 00000000, 00000001, 00000010 и 00000011 соответственно. Самая высокая точка ASCII-кода, 127, требует всего 7 значащих битов.

Зная это, вы можете видеть, что make_bitseq() преобразует строки ASCII в str представление в байтах, где каждый символ занимает один байт:

>>> make_bitseq("bits")
'01100010 01101001 01110100 01110011'


Недостаточное использование ASCII 8-разрядных байт, предлагаемых современными компьютерами, привело к появлению семейства конфликтующих неформализованных кодировок, в каждой из которых указывались дополнительные символы, которые должны были использоваться с оставшимися 128 доступными кодовыми точками, разрешенными в 8-разрядной схеме кодирования символов.

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

С годами одна мегасхема кодирования символов стала управлять всеми ними. Однако, прежде чем мы перейдем к этому, давайте немного поговорим о системах нумерации, которые являются фундаментальной основой схем кодирования символов.

Охватывает все основы: Другие системы счисления

При обсуждении ASCII выше вы видели, что каждый символ соответствует целому числу в диапазоне от 0 до 127.

Этот диапазон чисел выражается в десятичной системе счисления (основание 10). Именно так вы, я и все остальные люди привыкли считать, и это не сложнее, чем то, что у нас 10 пальцев.

Но существуют и другие системы нумерации, которые особенно распространены в исходном коде CPython. Хотя “базовый номер” один и тот же, все системы нумерации - это просто разные способы выражения одного и того же числа.

Если бы я спросил вас, какое число обозначает строка "11", вы были бы правы, если бы странно посмотрели на меня, прежде чем ответить, что оно обозначает одиннадцать.

Однако это строковое представление может выражать различные базовые числа в разных системах счисления. В дополнение к десятичной системе счисления, альтернативные варианты включают следующие распространенные системы счисления:

  • Двоичный: базовый 2
  • Восьмеричный: основание 8
  • Шестнадцатеричный (hex): базовый 16

Но что для нас означает утверждение, что в определенной системе счисления числа представлены в базе N?

Вот лучший известный мне способ объяснить, что это значит: это количество пальцев, на которое вы рассчитываете в этой системе.

Если вы хотите получить более полное, но в то же время ненавязчивое представление о системах нумерации, ознакомьтесь с книгой Чарльза Петцольда Code - невероятно интересной книгой, в которой подробно рассматриваются основы компьютерного кода.

Один из способов продемонстрировать, как разные системы нумерации интерпретируют одно и то же, - это использовать конструктор int() в Python. Если вы передадите str в int(), По умолчанию Python будет считать, что строка выражает число по основанию 10, если вы не укажете иное:

>>> int('11')
11
>>> int('11', base=10)  # 10 is already default
11
>>> int('11', base=2)  # Binary
3
>>> int('11', base=8)  # Octal
9
>>> int('11', base=16)  # Hex
17


Существует более распространенный способ сообщить Python, что ваше целое число введено с основанием, отличным от 10. Python принимает буквенные формы каждой из трех альтернативных систем нумерации, приведенных выше:

Type of Literal Prefix Example
n/a n/a 11
Binary literal 0b or 0B 0b11
Octal literal 0o or 0O 0o11
Hex literal 0x or 0X 0x11

Все они являются подформами целочисленных литералов. Вы можете видеть, что они дают те же результаты, что и вызовы int() со значениями, отличными от значений по умолчанию base. Все они просто int для Python:

>>> 11
11
>>> 0b11  # Binary literal
3
>>> 0o11  # Octal literal
9
>>> 0x11  # Hex literal
17


Вот как вы могли бы ввести двоичные, восьмеричные и шестнадцатеричные эквиваленты десятичных чисел от 0 до 20. Любой из них идеально подходит для оболочки интерпретатора Python или исходного кода, и все они имеют тип int:

Decimal Binary Octal Hex
0 0b0 0o0 0x0
1 0b1 0o1 0x1
2 0b10 0o2 0x2
3 0b11 0o3 0x3
4 0b100 0o4 0x4
5 0b101 0o5 0x5
6 0b110 0o6 0x6
7 0b111 0o7 0x7
8 0b1000 0o10 0x8
9 0b1001 0o11 0x9
10 0b1010 0o12 0xa
11 0b1011 0o13 0xb
12 0b1100 0o14 0xc
13 0b1101 0o15 0xd
14 0b1110 0o16 0xe
15 0b1111 0o17 0xf
16 0b10000 0o20 0x10
17 0b10001 0o21 0x11
18 0b10010 0o22 0x12
19 0b10011 0o23 0x13
20 0b10100 0o24 0x14

Просто удивительно, насколько широко распространены эти выражения в стандартной библиотеке Python. Если вы хотите убедиться в этом сами, перейдите туда, где находится ваш каталог lib/python3.7/, и ознакомьтесь с использованием шестнадцатеричных литералов, подобных этому:

$ grep -nri --include "*\.py" -e "\b0x" lib/python3.7


Это должно работать в любой системе Unix, которая имеет grep. Вы можете использовать "\b0o" для поиска восьмеричных литералов или “\b0b” для поиска двоичных литералов.

Каковы аргументы в пользу использования этих альтернативных int буквальных синтаксисов? Короче говоря, это потому, что 2, 8 и 16 являются степенями 2, в то время как 10 - нет. Эти три альтернативные системы счисления иногда позволяют выразить значения удобным для компьютера способом. Например, число 65536 или 216 равно всего лишь 10000 в шестнадцатеричном формате или 0x10000 в виде шестнадцатеричного литерала Python.

Введите Юникод

Как вы видели, проблема с ASCII заключается в том, что его набор символов недостаточно велик, чтобы вместить все существующие в мире языки, диалекты, символы и иероглифы. (Это недостаточно даже для одного только английского языка.)

Юникод, по сути, служит той же цели, что и ASCII, но он просто включает в себя способ, способ, способ, больший набор кодовых точек. Существует несколько кодировок, которые появились в хронологическом порядке между ASCII и Unicode, но о них пока не стоит упоминать, потому что Unicode и одна из его схем кодирования, UTF-8, стали широко использоваться.

Представьте себе Unicode как расширенную версию таблицы ASCII, которая содержит 1 114 112 возможных кодовых точек. Это от 0 до 1,114,111, или от 0 до 17 * (2<>16) - 1, или 0x10ffff в шестнадцатеричном формате. На самом деле, ASCII - это идеальное подмножество Unicode. Первые 128 символов в таблице Unicode в точности соответствуют символам ASCII, которые вы могли бы ожидать от них.

В целях технической строгости, Юникод сам по себе является , а не кодировкой. Скорее, Юникод реализован различными кодировками символов, которые вы скоро увидите. Юникод лучше рассматривать как карту (что-то вроде dict) или таблицу базы данных из 2 столбцов. Он преобразует символы (например, "a", "¢", или даже "ቈ") в отдельные положительные целые числа. Кодировка символов должна предлагать немного больше.

Юникод содержит практически все символы, которые вы можете себе представить, включая дополнительные, непечатаемые. Один из моих любимых - надоедливый знак справа налево, который имеет кодовую точку 8207 и используется в тексте, написанном как слева направо, так и справа налево, например, в статье, содержащей абзацы как на английском, так и на арабском языках.

Примечание: Мир кодировок символов - это одна из многих мелких технических деталей, к которым некоторые люди любят придираться. Одна из таких деталей заключается в том, что только 1 111 998 кодовых точек Unicode действительно пригодны для использования по нескольким архаичным причинам.

Юникод против UTF-8

Людям не потребовалось много времени, чтобы понять, что все символы мира не могут быть упакованы в один байт каждый. Из этого видно, что современные, более полные кодировки должны были бы использовать несколько байт для кодирования некоторых символов.

Вы также видели выше, что Юникод технически не является полноценной кодировкой символов. Почему это так?

Есть одна вещь, о которой Unicode умалчивает: он не говорит вам, как извлекать фактические биты из текста — только кодовые точки. В нем недостаточно информации о том, как преобразовать текст в двоичные данные и наоборот.

Юникод - это абстрактный стандарт кодирования, а не сама кодировка. Вот тут-то и вступают в действие UTF-8 и другие схемы кодирования. Стандарт Unicode (сопоставление символов с кодовыми точками) определяет несколько различных кодировок из своего единственного набора символов.

UTF-8, а также его менее распространенные собратья, UTF-16 и UTF-32, являются форматами кодировки для представления символов Unicode в виде двоичных данных по одному или более байтам на символ. Чуть позже мы обсудим UTF-16 и UTF-32, но на сегодняшний день UTF-8 занимает самую большую долю рынка.

Это подводит нас к определению, которое давно назрело. Что формально означает кодировать и декодировать?

Кодирование и декодирование в Python 3

Тип str в Python 3 предназначен для представления текста, удобочитаемого человеком, и может содержать любой символ Юникода.

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

Кодирование и декодирование - это процесс перехода от одного к другому:

Encode versus decode Кодирование против декодирования (изображение: Реальный Python)

В .encode() и .decode() параметром encoding по умолчанию является "utf-8", хотя, как правило, безопаснее и недвусмысленнее указывать его:

>>> "résumé".encode("utf-8")
b'r\xc3\xa9sum\xc3\xa9'
>>> "El Niño".encode("utf-8")
b'El Ni\xc3\xb1o'

>>> b"r\xc3\xa9sum\xc3\xa9".decode("utf-8")
'résumé'
>>> b"El Ni\xc3\xb1o".decode("utf-8")
'El Niño'


Результатом str.encode() является объект bytes. Как байтовые литералы (такие как b"r\xc3\xa9sum\xc3\xa9"), так и представления байтов допускают только символы ASCII.

Вот почему при вызове "El Niño".encode("utf-8") допускается представление ASCII-совместимого "El" как есть, но n с тильдой преобразуется в "\xc3\xb1". Эта беспорядочно выглядящая последовательность представляет собой два байта, 0xc3 и 0xb1 в шестнадцатеричном формате:

>>> " ".join(f"{i:08b}" for i in (0xc3, 0xb1))
'11000011 10110001'


То есть символ ñ для его двоичного представления в UTF требуется два байта-8.

Примечание: Если вы введете help(str.encode), то, вероятно, увидите значение по умолчанию encoding='utf-8'. Будьте осторожны, исключая это и просто используя "résumé".encode(), потому что значение по умолчанию может отличаться в Windows до версии Python 3.6.

Python 3: Олл-ин на Unicode

Python 3 полностью поддерживает Unicode и, в частности, UTF-8. Вот что это означает:

  • По умолчанию предполагается, что исходный код Python 3 содержит UTF-8. Это означает, что вам не нужно # -*- coding: UTF-8 -*- в начале .py файлов в Python 3.

  • Весь текст (str) по умолчанию представлен в Юникоде. Текст, закодированный в Юникоде, представлен в виде двоичных данных (bytes). Тип str может содержать любой буквенный символ Юникода, такой как "Δv / Δt", и все они будут сохранены как Unicode.

  • Python 3 принимает множество кодовых точек Unicode в идентификаторах, что означает, что résumé = "~/Documents/resume.pdf" допустимо, если это вам нравится.

  • В модуле Python re по умолчанию используется флаг re.UNICODE, а не re.ASCII. Это означает, например, что r"\w" соответствует символам слов в Юникоде, а не только буквам ASCII.

  • По умолчанию encoding в str.encode() и bytes.decode() используется UTF-8.

Есть еще одно свойство, более тонкое, которое заключается в том, что значение по умолчанию encoding для встроенного open() зависит от платформы и зависит от значения locale.getpreferredencoding():

>>> # Mac OS X High Sierra
>>> import locale
>>> locale.getpreferredencoding()
'UTF-8'

>>> # Windows Server 2012; other Windows builds may use UTF-16
>>> import locale
>>> locale.getpreferredencoding()
'cp1252'


И снова, урок состоит в том, чтобы быть осторожным в предположениях, когда речь заходит об универсальности UTF-8, даже если это преобладающая кодировка. Никогда не помешает быть ясным в своем коде.

Один байт, Два байта, Три байта, Четыре

Важной особенностью является то, что UTF-8 - это кодировка переменной длины. Возникает соблазн умолчать о том, что это значит, но в этом стоит разобраться.

Вспомните раздел, посвященный ASCII. Все, что находится в расширенном формате ASCII, занимает не более одного байта пространства. Вы можете быстро доказать это с помощью следующего генераторного выражения:

>>> all(len(chr(i).encode("ascii")) == 1 for i in range(128))
True


Кодировка UTF-8 сильно отличается. Данный символ Юникода может занимать от одного до четырех байт. Вот пример одного символа Юникода, занимающего четыре байта:

>>> ibrow = "🤨"
>>> len(ibrow)
1
>>> ibrow.encode("utf-8")
b'\xf0\x9f\xa4\xa8'
>>> len(ibrow.encode("utf-8"))
4

>>> # Calling list() on a bytes object gives you
>>> # the decimal value for each byte
>>> list(b'\xf0\x9f\xa4\xa8')
[240, 159, 164, 168]


Это тонкая, но важная особенность len():

  • Длина одного символа Unicode в Python str будет всегда равна 1, независимо от того, сколько байт он занимает.
  • Длина того же символа, закодированного как bytes, будет находиться в диапазоне от 1 до 4.

В таблице ниже приведены общие типы символов, которые помещаются в каждую ячейку длиной в байт:

Decimal Range Hex Range What’s Included Examples
0 to 127 "\u0000" to "\u007F" U.S. ASCII "A", "\n", "7", "&"
128 to 2047 "\u0080" to "\u07FF" Most Latinic alphabets* "ę", "±", "ƌ", "ñ"
2048 to 65535 "\u0800" to "\uFFFF" Additional parts of the multilingual plane (BMP)** "ത", "ᄇ", "ᮈ", "‰"
65536 to 1114111 "\U00010000" to "\U0010FFFF" Other*** "𝕂", "𐀀", "😓", "🂲",

*Например, английский, арабский, греческий и ирландский
** Огромное количество языков и символов — в основном китайский, японский и корейский (также ASCII и латинский алфавиты)
*** Дополнительные китайские, японские, корейские и вьетнамские иероглифы, а также дополнительные символы и эмодзи

Примечание: Чтобы не упускать из виду общую картину, существует дополнительный набор технических возможностей UTF-8, которые здесь не рассматриваются, поскольку они редко видны пользователю Python .

Например, UTF-8 фактически использует префиксные коды, которые указывают количество байт в последовательности. Это позволяет декодеру определять, какие байты принадлежат друг другу в кодировке переменной длины, и позволяет первому байту служить индикатором количества байтов в следующей последовательности.

В статье Википедии UTF-8 не обходится без технических подробностей, и всегда есть официальный Стандарт Unicode для вашего ознакомления и удовольствие тоже.

А как насчет UTF-16 и UTF-32?

Давайте вернемся к двум другим вариантам кодировки, UTF-16 и UTF-32.

На практике разница между ними и UTF-8 существенна. Вот пример того, насколько существенна разница при преобразовании в оба конца:

>>> letters = "αβγδ"
>>> rawdata = letters.encode("utf-8")
>>> rawdata.decode("utf-8")
'αβγδ'
>>> rawdata.decode("utf-16")  # 😧
'뇎닎돎듎'


В этом случае кодировка четырех греческих букв в UTF-8, а затем их декодирование обратно в текст в UTF-16 приведет к получению текста str, написанного на совершенно другом языке (корейском).

Подобные вопиюще неправильные результаты возможны, если одна и та же кодировка не используется двунаправленно. Два варианта декодирования одного и того же объекта bytes могут привести к результатам, которые даже не соответствуют одному и тому же языку.

В этой таблице суммируется диапазон или количество байт в кодировках UTF-8, UTF-16 и UTF-32:

Encoding Bytes Per Character (Inclusive) Variable Length
UTF-8 1 to 4 Yes
UTF-16 2 to 4 Yes
UTF-32 4 No

Еще одним любопытным аспектом семейства UTF является то, что UTF-8 не всегда будет занимать меньше места, чем UTF-16. Это может показаться математически нелогичным, но это вполне возможно:

>>> text = "記者 鄭啟源 羅智堅"
>>> len(text.encode("utf-8"))
26
>>> len(text.encode("utf-16"))
22


Причина этого в том, что кодовые точки в диапазоне от U+0800 до U+FFFF (от 2048 до 65535 в десятичной системе счисления) занимают три байта в UTF-8 по сравнению только с двумя в UTF-16.

Я ни в коем случае не рекомендую вам садиться в поезд с UTF-16, независимо от того, работаете ли вы на языке, символы которого обычно находятся в этом диапазоне. Среди прочих причин, одним из веских аргументов в пользу использования UTF-8 является то, что в мире кодирования это отличная идея слиться с толпой.

Не говоря уже о том, что на дворе 2019 год: компьютерная память стоит дешево, поэтому экономить 4 байта, используя UTF-16, вряд ли стоит.

Встроенные функции Python

Самое сложное позади. Пришло время использовать то, что вы уже видели в Python.

В Python есть группа встроенных функций, которые каким-то образом связаны с системами нумерации и кодировкой символов:

Они могут быть логически сгруппированы в зависимости от их назначения:

  • ascii(), bin(), hex(), и oct() предназначены для получения другого представления входных данных. Каждый из них выдает str. Первый, ascii(), выдает представление объекта только в формате ASCII, при этом символы, отличные от ASCII, экранируются. Остальные три дают двоичное, шестнадцатеричное и восьмеричное представления целого числа соответственно. Это всего лишь представления, а не фундаментальные изменения во входных данных.

  • bytes(), str(), и int() являются конструкторами классов для соответствующих типов, bytes, str и int. Каждый из них предлагает способы приведения входных данных к желаемому типу. Например, как вы видели ранее, хотя int(11.0), вероятно, более распространен, вы также можете увидеть int('11', base=16).

  • ord() и chr() являются обратными друг другу в том смысле, что функция Python ord() преобразует символ str в его кодовую точку с базой 10, в то время как chr() делает обратное.

Вот более подробный обзор каждой из этих девяти функций:

Function Signature Accepts Return Type Purpose
ascii() ascii(obj) Varies str ASCII only representation of an object, with non-ASCII characters escaped
bin() bin(number) number: int str Binary representation of an integer, with the prefix "0b"
bytes() bytes(iterable_of_ints)

bytes(s, enc[, errors])

bytes(bytes_or_buffer)

bytes([i])
Varies bytes Coerce (convert) the input to bytes, raw binary data
chr() chr(i) i: int

i>=0

i<=1114111
str Convert an integer code point to a single Unicode character
hex() hex(number) number: int str Hexadecimal representation of an integer, with the prefix "0x"
int() int([x])

int(x, base=10)
Varies int Coerce (convert) the input to int
oct() oct(number) number: int str Octal representation of an integer, with the prefix "0o"
ord() ord(c) c: str

len(c) == 1
int Convert a single Unicode character to its integer code point
str() str(object=’‘)

str(b[, enc[, errors]])
Varies str Coerce (convert) the input to str, text

Вы можете расширить раздел ниже, чтобы увидеть несколько примеров каждой функции.

ascii() предоставляет вам представление объекта только в формате ASCII с экранированными символами, отличными от ASCII:

>>> ascii("abcdefg")
"'abcdefg'"

>>> ascii("jalepeño")
"'jalepe\\xf1o'"

>>> ascii((1, 2, 3))
'(1, 2, 3)'

>>> ascii(0xc0ffee)  # Hex literal (int)
'12648430'


bin() дает вам двоичное представление целого числа с префиксом "0b":

>>> bin(0)
'0b0'

>>> bin(400)
'0b110010000'

>>> bin(0xc0ffee)  # Hex literal (int)
'0b110000001111111111101110'

>>> [bin(i) for i in [1, 2, 4, 8, 16]]  # `int` + list comprehension
['0b1', '0b10', '0b100', '0b1000', '0b10000']


bytes() преобразует входные данные в bytes, представляя необработанные двоичные данные:

>>> # Iterable of ints
>>> bytes((104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100))
b'hello world'

>>> bytes(range(97, 123))  # Iterable of ints
b'abcdefghijklmnopqrstuvwxyz'

>>> bytes("real 🐍", "utf-8")  # String + encoding
b'real \xf0\x9f\x90\x8d'

>>> bytes(10)
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

>>> bytes.fromhex('c0 ff ee')
b'\xc0\xff\xee'

>>> bytes.fromhex("72 65 61 6c 70 79 74 68 6f 6e")
b'realpython'


chr() преобразует целочисленную кодовую точку в один символ Юникода:

>>> chr(97)
'a'

>>> chr(7048)
'ᮈ'

>>> chr(1114111)
'\U0010ffff'

>>> chr(0x10FFFF)  # Hex literal (int)
'\U0010ffff'

>>> chr(0b01100100)  # Binary literal (int)
'd'


hex() задает шестнадцатеричное представление целого числа с префиксом "0x":

>>> hex(100)
'0x64'

>>> [hex(i) for i in [1, 2, 4, 8, 16]]
['0x1', '0x2', '0x4', '0x8', '0x10']

>>> [hex(i) for i in range(16)]
['0x0', '0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7',
 '0x8', '0x9', '0xa', '0xb', '0xc', '0xd', '0xe', '0xf']


int() преобразует входные данные в int, при необходимости интерпретируя входные данные в заданной базе:

>>> int(11.0)
11

>>> int('11')
11

>>> int('11', base=2)
3

>>> int('11', base=8)
9

>>> int('11', base=16)
17

>>> int(0xc0ffee - 1.0)
12648429

>>> int.from_bytes(b"\x0f", "little")
15

>>> int.from_bytes(b'\xc0\xff\xee', "big")
12648430


Функция Python ord() преобразует отдельный символ Unicode в его целочисленную кодовую точку:

>>> ord("a")
97

>>> ord("ę")
281

>>> ord("ᮈ")
7048

>>> [ord(i) for i in "hello world"]
[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]


str() преобразует вводимые данные в str, представляя текст:

>>> str("str of string")
'str of string'

>>> str(5)
'5'

>>> str([1, 2, 3, 4])  # Like [1, 2, 3, 4].__str__(), but use str()
'[1, 2, 3, 4]'

>>> str(b"\xc2\xbc cup of flour", "utf-8")
'¼ cup of flour'

>>> str(0xc0ffee)
'12648430'


Строковые литералы Python: способы освежевать кошку

Вместо использования конструктора str() обычно вводится str буквально:

>>> meal = "shrimp and grits"


Это может показаться достаточно простым. Но самое интересное заключается в том, что, поскольку Python 3 полностью ориентирован на Unicode, вы можете “набирать” символы Unicode, которые, вероятно, даже не найдете на своей клавиатуре. Вы можете скопировать и вставить это прямо в оболочку интерпретатора Python 3:

>>> alphabet = 'αβγδεζηθικλμνξοπρςστυφχψ'
>>> print(alphabet)
αβγδεζηθικλμνξοπρςστυφχψ


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

Одним из самых объемных разделов документации по Python является раздел, посвященный лексическому анализу, в частности раздел, посвященный строковым и байтовым литералам. Лично мне пришлось прочитать этот раздел один, два, а может быть, и девять раз, чтобы он действительно дошел до меня.

В нем говорится, что существует до шести способов, с помощью которых Python позволит вам вводить один и тот же символ Юникода.

Первый и наиболее распространенный способ - ввести сам символ буквально, как вы уже видели. Самое сложное в этом методе - найти фактические нажатия клавиш. Вот тут-то и вступают в действие другие методы получения и представления символов. Вот полный список:

Escape Sequence Meaning How To Express "a"
"\ooo" Character with octal value ooo "\141"
"\xhh" Character with hex value hh "\x61"
"\N{name}" Character named name in the Unicode database "\N{LATIN SMALL LETTER A}"
"\uxxxx" Character with 16-bit (2-byte) hex value xxxx "\u0061"
"\Uxxxxxxxx" Character with 32-bit (4-byte) hex value xxxxxxxx "\U00000061"

Вот некоторые доказательства и валидация вышесказанного:

>>> (
...     "a" ==
...     "\x61" == 
...     "\N{LATIN SMALL LETTER A}" ==
...     "\u0061" ==
...     "\U00000061"
... )
True


Итак, есть два основных предостережения:

  1. Не все эти формы подходят для всех персонажей. Шестнадцатеричное представление целого числа 300 равно 0x012c, что просто не вписывается в двухзначный escape-код "\xhh". Самая высокая кодовая точка, которую вы можете вставить в эту escape-последовательность, - это "\xff" ("ÿ"). Аналогично для "\ooo", это будет работать только до "\777" ("ǿ").

  2. Для \xhh, \uxxxx, и \Uxxxxxxxx требуется ровно столько цифр, сколько показано в этих примерах. Это может поставить вас в тупик из-за того, что в таблицах Unicode обычно отображаются коды символов с начальным U+ и переменным количеством шестнадцатеричных символов. Суть в том, что в таблицах Unicode эти коды чаще всего не обнуляются.

Например, если вы обратитесь к unicode-table.com для получения информации о готической букве файху (или fehu), "𐍆",), вы увидите, что она указана как имеющая код U+10346.

Как вы выразите это в "\uxxxx" или "\Uxxxxxxxx"? Ну, вы не можете поместить его в "\uxxxx", потому что это 4-байтовый символ, и чтобы использовать "\Uxxxxxxxx" для представления этого символа, вам нужно ввести последовательность слева направо:

>>> "\U00010346"
'𐍆'


Это также означает, что форма "\Uxxxxxxxx" является единственной управляющей последовательностью, которая может содержать любой символ Юникода.

Примечание: Вот короткая функция для преобразования строк, которые выглядят как "U+10346", во что-то, с чем может работать Python. Он использует str.zfill():

>>> def make_uchr(code: str):
...     return chr(int(code.lstrip("U+").zfill(8), 16))
>>> make_uchr("U+10346")
'𐍆'
>>> make_uchr("U+0026")
'&'


Другие кодировки, доступные в Python

До сих пор вы видели четыре кодировки символов:

  1. ASCII
  2. UTF-8
  3. Кодировка UTF-16
  4. Кодировка UTF-32

Существует множество других вариантов.

Одним из примеров является Latin-1 (также называемый ISO-8859-1), который технически используется по умолчанию для протокола передачи гипертекста (HTTP) согласно RFC 2616. В Windows есть свой собственный вариант на латинице-1, который называется cp1252.

Примечание: Стандарт ISO-8859-1 все еще широко распространен в дикой природе. В requests библиотека следует RFC 2616 “на букву” в использовании его в качестве кодировки по умолчанию для содержимого HTTP или протокол HTTPS ответ. Если в заголовке Content-Type встречается слово “текст” и никакая другая кодировка не указана, то requests будет использоваться ISO-8859-1.

Полный список принятых кодировок находится в документации к модулю codecs, который является частью стандартной библиотеки Python.

Есть еще одна полезная распознанная кодировка, о которой следует знать, - это "unicode-escape". Если у вас есть декодированный str и вы хотите быстро получить представление его экранированного литерала Unicode, то вы можете указать эту кодировку в .encode():

>>> alef = chr(1575)  # Or "\u0627"
>>> alef_hamza = chr(1571)  # Or "\u0623"
>>> alef, alef_hamza
('ا', 'أ')
>>> alef.encode("unicode-escape")
b'\\u0627'
>>> alef_hamza.encode("unicode-escape")
b'\\u0623'


Вы Знаете, Что Говорят О Предположениях...

Только потому, что Python использует кодировку UTF-8 для файлов и кода, которые вы генерируете, не означает, что вы, программист, должны оперировать таким же предположением для внешних данных.

Давайте повторим это еще раз, потому что это правило, которому нужно следовать: когда вы получаете двоичные данные (байты) из стороннего источника, будь то из файла или по сети, лучше всего проверить, задана ли в данных кодировка. Если это не так, то вы сами должны спросить.

Весь ввод-вывод выполняется в байтах, а не в тексте, и байты - это просто единицы и нули для компьютера, пока вы не скажете ему обратное, сообщив кодировку.

Вот пример того, где что-то может пойти не так. Вы подписаны на API, который отправляет вам рецепт дня, который вы получаете в bytes и всегда без проблем расшифровываете с помощью .decode("utf-8"). В этот конкретный день часть рецепта выглядит следующим образом:

>>> data = b"\xbc cup of flour"


Похоже, что в рецепте требуется немного муки, но мы не знаем, сколько именно:

>>> data.decode("utf-8")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbc in position 0: invalid start byte


О-о-о. Есть один неприятный момент UnicodeDecodeError, который может вас подкосить, когда вы делаете предположения о кодировке. Вы уточняете это у администратора API. О чудо, данные на самом деле передаются в зашифрованном виде на латинице-1:

>>> data.decode("latin-1")
'¼ cup of flour'


Вот и все. В латинице-1, каждый символ помещается в один байт, в то время как символ “¼” в UTF занимает два байта-8 ("\xc2\xbc").

Урок здесь заключается в том, что может быть опасно использовать кодировку любых данных, которые вам передаются. В наши дни это обычно UTF-8, но это небольшой процент случаев, когда это не так, что может привести к взрыву.

Если вам действительно нужно отвлечься и угадать кодировку, то загляните в библиотеку chardet, которая использует методологию Mozilla для составления обоснованного предположения о неоднозначно закодированном тексте. Тем не менее, такой инструмент, как chardet, должен быть вашим последним средством, а не первым.

Всякие мелочи: unicodedata

Было бы упущением не упомянуть unicodedata из стандартной библиотеки Python, которая позволяет взаимодействовать с базой данных символов Unicode (UCD) и выполнять поиск в ней:

>>> import unicodedata

>>> unicodedata.name("€")
'EURO SIGN'
>>> unicodedata.lookup("EURO SIGN")
'€'


Завершение

В этой статье вы раскрыли обширную и впечатляющую тему кодирования символов в Python.

Вы уже многое обсудили здесь:

  • Основные понятия о кодировках символов и системах нумерации
  • Целые, двоичные, восьмеричные, шестнадцатеричные, str и байтовые литералы в Python
  • Встроенные функции Python, связанные с кодировкой символов и системами нумерации
  • Обработка текста в Python 3 в сравнении с двоичными данными

А теперь приступайте к кодированию!

Ресурсы

Для получения более подробной информации по рассматриваемым здесь темам ознакомьтесь с этими ресурсами:

В документах по Python есть две страницы, посвященные этой теме:

<статус завершения article-slug="руководство по кодировкам python" class="btn-group mb-0" data-api-article-bookmark-url="/api/v1/статьи/руководство по кодировкам python/bookmark/" data-api-article-завершение-статус-url="/api/версия 1/статьи/руководство по кодировкам на python/завершение_статуса/"> <кнопка поделиться bluesky-text="Интересная статья на #Python от @realpython.com :" email-body="Ознакомьтесь с этой статьей о Python:%0A%0AUnicode и кодировки символов в Python: руководство по безболезненному использованию" email-subject="Статья о Python для вас" twitter-text="Интересная статья на #Python от @realpython:" url="https://realpython.com/python-encodings-guide/" url-title="Юникод и кодировки символов в Python: руководство по безболезненному использованию">

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Unicode в Python: Работа с кодировками символов

Back to Top