Python 3: Демистификация методов кодирования и декодирования
Скажем, у меня есть строка в Python:
>>> s = 'python'
>>> len(s)
6
Теперь я encode
эта строка выглядит так:
>>> b = s.encode('utf-8')
>>> b16 = s.encode('utf-16')
>>> b32 = s.encode('utf-32')
То, что я получаю из вышеперечисленных операций, - это массив байтов, то есть b
, b16
и b32
- это просто массивы байтов (каждый байт, конечно, 8-битный).
Но мы закодировали строку. Итак, что это значит? Как мы присоединяем понятие "кодирования" к необработанному массиву байтов?
Ответ заключается в том, что каждый из этих массивов байтов генерируется определенным образом. Посмотрите на эти массивы:
>>> [hex(x) for x in b]
['0x70', '0x79', '0x74', '0x68', '0x6f', '0x6e']
>>> len(b)
6
Этот массив указывает, что для каждого символа мы имеем один байт (потому что все символы падают ниже 127). Следовательно, можно сказать, что "кодирование" строки в "utf-8" собирает каждый символ, соответствующий кодовой точке, и помещает его в массив. Если точка кода не может поместиться в один байт, то utf-8 потребляет два байта. Следовательно, utf-8 потребляет наименьшее количество байтов.
>>> [hex(x) for x in b16]
['0xff', '0xfe', '0x70', '0x0', '0x79', '0x0', '0x74', '0x0', '0x68', '0x0', '0x6f', '0x0', '0x6e', '0x0']
>>> len(b16)
14 # (2 + 6*2)
Здесь мы видим, что "encoding to utf-16" сначала помещает двухбайтную BOM (FF FE
) в массив байтов, а затем для каждого символа он помещает два байта в массив. (В нашем случае второй байт всегда равен нулю)
>>> [hex(x) for x in b32]
['0xff', '0xfe', '0x0', '0x0', '0x70', '0x0', '0x0', '0x0', '0x79', '0x0', '0x0', '0x0', '0x74', '0x0', '0x0', '0x0', '0x68', '0x0', '0x0', '0x0', '0x6f', '0x0', '0x0', '0x0', '0x6e', '0x0', '0x0', '0x0']
>>> len(b32)
28 # (2+ 6*4 + 2)
В случае "кодирования в utf-32" мы сначала помещаем спецификацию, затем для каждого символа мы помещаем четыре байта и, наконец, ставим два байта в массив.
Следовательно, можно сказать, что "процесс кодирования" собирает 1 2 или 4 байта (в зависимости от имени кодировки) для каждого символа в строке и добавляет и добавляет к ним больше байтов для создания конечного массива результатов байтов.
Теперь, мои вопросы:
- Насколько я понимаю процесс кодирования или я что-то упускаю?
- Мы видим, что представление памяти переменных
b
, b16
и b32
на самом деле является списком байтов. Каково представление памяти в строке? Точно что хранится в памяти для строки?
- Мы знаем, что когда мы делаем
encode()
, каждая символьная соответствующая кодовая точка собирается (кодовая точка, соответствующая имени кодировки) и помещается в массив или байты. Что именно происходит, когда мы делаем decode()
?
- Мы можем видеть, что в utf-16 и utf-32 добавлена спецификация, но почему в кодировке utf-32 добавляются два нулевых байта?
Ответы
Ответ 1
Прежде всего, UTF-32 является 4-байтовой кодировкой, поэтому его спецификация также представляет собой четырехбайтную последовательность:
>>> import codecs
>>> codecs.BOM_UTF32
b'\xff\xfe\x00\x00'
И поскольку разные компьютерные архитектуры обрабатывают байтовые порядки по-разному (называемые Endianess), есть два варианта спецификации: маленький и большой endian:
>>> codecs.BOM_UTF32_LE
b'\xff\xfe\x00\x00'
>>> codecs.BOM_UTF32_BE
b'\x00\x00\xfe\xff'
Цель спецификации - передать этот порядок декодеру; прочитайте спецификацию, и вы знаете, если она большая или маленькая. Итак, последние два нулевых байта в вашей строке UTF-32 являются частью последнего закодированного символа.
UTF-16 Таким образом, спецификация аналогична, поскольку существует два варианта:
>>> codecs.BOM_UTF16
b'\xff\xfe'
>>> codecs.BOM_UTF16_LE
b'\xff\xfe'
>>> codecs.BOM_UTF16_BE
b'\xfe\xff'
Это зависит от вашей компьютерной архитектуры, которая используется по умолчанию.
UTF-8 вообще не нуждается в спецификации; UTF-8 использует 1 или более байт на символ (добавление байтов, необходимых для кодирования более сложных значений), но порядок этих байтов определен в стандарте. Microsoft сочла необходимым в любом случае ввести спецификацию UTF-8 (поэтому приложение "Блокнот" может обнаружить UTF-8), но поскольку порядок спецификации не меняется, его использование не рекомендуется.
Что касается того, что хранится в Python для строк unicode; что фактически изменилось в Python 3.3. До 3.3, внутренне на уровне C, Python либо сохранял комбинации байтов UTF16, либо UTF32, в зависимости от того, был ли Python скомпилирован с поддержкой широкого символа (см. Как узнать, скомпилирован ли Python с UCS-2 или UCS-4?, UCS-2 по существу UTF-16, а UCS-4 - UTF-32). Таким образом, каждый символ занимает 2 или 4 байта памяти.
Начиная с Python 3.3, внутреннее представление использует минимальное количество байтов, необходимых для представления всех символов в строке. Для обычного ASCII и латинского кодируемого текста используется 1 байт, для остальных BMP используются 2 байта, а текст, содержащий символы за пределами этого Используются 4 байта. Python переключается между форматами по мере необходимости. Таким образом, в большинстве случаев хранилище стало намного более эффективным. Более подробно см. Что нового в Python 3.3.
Я могу настоятельно рекомендовать вам читать Unicode и Python с помощью:
Ответ 2
- Ваше понимание по существу правильное, насколько это возможно, хотя это не действительно "1, 2 или 4 байта". Для UTF-32 это будет 4 байта. Для UTF-16 и UTF-8 количество байтов зависит от кодируемого символа. Для UTF-16 это будет либо 2, либо 4 байта. Для UTF-8 это может быть 1, 2, 3 или 4 байта. Но да, в основном кодирование принимает точку кода юникода и сопоставляет ее с последовательностью байтов. Как это сопоставление выполняется, зависит от кодировки. Для UTF-32 это просто прямое шестнадцатеричное представление номера кодовой точки. Для UTF-16 это обычно так, но будет немного отличаться для необычных символов (вне базовой многоязычной плоскости). Для UTF-8 кодировка более сложна (см. Wikipedia.) Что касается дополнительных байтов в начале, это маркеры байтов которые определяют, в каком порядке фрагменты кодовой точки поступают в UTF-16 или UTF-32.
- Я думаю, вы могли бы посмотреть на внутренности, но точка типа строки (или тип Unicode в Python 2) должна защитить вас от этой информации, точно так же, как точка списка Python - защитить вас от необходимости манипулировать необработанной структурой памяти этого списка. Строковый тип данных существует, поэтому вы можете работать с кодами Unicode, не беспокоясь о представлении памяти. Если вы хотите работать с необработанными байтами, закодируйте строку.
- Когда вы выполняете декодирование, он в основном сканирует строку, ища куски байтов. Схемы кодирования по существу обеспечивают "подсказки", которые позволяют декодеру видеть, когда один символ заканчивается, а другой начинается. Таким образом, декодер просматривает и использует эти подсказки, чтобы найти границы между символами, затем просматривает каждую часть, чтобы увидеть, какой символ она представляет в этой кодировке. Вы можете просмотреть отдельные кодировки в Википедии или тому подобное, если хотите просмотреть сведения о том, как каждый код кода кодирования указывает туда и обратно с байтами.
- Два нулевых байта являются частью маркера байтового байта для UTF-32. Поскольку UTF-32 всегда использует 4 байта на кодовую точку, спецификация также имеет четыре байта. В основном маркер FFFE, который вы видите в UTF-16, имеет нулевое дополнение с двумя дополнительными нулевыми байтами. Эти маркеры байтового байта указывают, соответствуют ли числа, составляющие кодовую точку, от самого большого до наименьшего или наименьшего к наибольшему. В основном это похоже на выбор того, следует ли записывать число "одна тысяча двести тридцать четыре" как 1234 или 4321. Различные компьютерные архитектуры делают разные варианты в этом вопросе.
Ответ 3
Я предполагаю, что вы используете Python 3 (в Python 2 "строка" на самом деле представляет собой массив байтов, что вызывает боль в Unicode).
Строка A (Unicode) представляет собой концепцию последовательности кодовых точек Unicode, которые являются абстрактными объектами, соответствующими "символам". Вы можете увидеть фактическую реализацию С++ в репозитории Python. Поскольку компьютеры не имеют неотъемлемой концепции кодовой точки, "кодировка" указывает частичную биекцию между кодовыми точками и байтовыми последовательностями.
Кодировки настроены так, что в кодировке переменной ширины нет двусмысленности - если вы видите байт, вы всегда знаете, завершает ли он текущую кодовую точку или нужно ли читать другую. Технически это называется без префикса. Итак, когда вы делаете .decode()
, Python ходит по байтовому массиву, создавая закодированные символы на одном времени и вывода их.
Два нулевых байта являются частью спецификации utf32: big-endian UTF32 имеет 0x0 0x0 0xff 0xfe
.