perl, utf8 и encode & decode термины

Полезно будет знать Perl программерам. Дело в том, что часто в документациях к perl (perldoc utf8) и модулям (например JSON) употреблаются такие термины, которые лично меня часто путали своим смыслом, а именно: encode to UTF-8, decode to UTF-8. Что подумает нормальный программер, услышав слово encode to? То, что что-то кодируется во что-то, а именно «encode to UTF-8» -> «кодирование в UTF-8». Я знаю, что perl хранит внутри себя строки UTF-8 со специальным флагом — если он установлен — строка UTF-8, если нет — бинарная. Поэтому, когда в доке я читал encode to UTF-8, я все время путался — думал, что говорят про установку этого флага. Оказалось, все в точности наоборот.

Правильно так: когда пишут encode to UTF-8, это значит, что внутри перла строка с UTF-8 данными остается как есть, но флаг UTF-8 убирается, и perl начинает вести себя со строкой так, как будто там бинарные (octet) байтовые данные (то есть посути, строка внутри перла перестает быть UTF8, хотя слово «encode» несет в себе другой смысл). И наоборот — decode to UTF-8 -> UTF8 флаг ставится (то есть для perl-а строка становится UTF8).

P.S. В Perl есть такая функция — encode (модуль Encode). Смысл ее в том, чтобы перевести строку из внутреннего содержания на perl (которое всегда UTF-8 внутри, т.е. Unicode символы) в байтовую последовательность, которая бы отражала ту кодировку, которую ей указали (то есть раз байтовая — значит всегда utf8 флаг в результате отсутсвует). Если мы делаем $result = encode(‘utf-8’, $string), то результат как раз будет в том, что флаг utf8 будет просто снят для $result и в реальности никакого переводирования не будет (то есть побайтово внутри perl-а $string & $result будут содержать одни и те же байты, только $string еще будет иметь взведенный utf8 флаг). И это как раз вписывается в те понятия, которые я описал ранее. Для decode и байтового UTF8 всё обратно — флаг utf8 просто ставится в $result.

Вот такая абракодабра… Будьте внимательны!

Кратко об UTF-8

Просто об UTF-8

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

Несколько простых правил

  1. Итак, UTF-8 — это «обертка» для Unicode. Это не отдельная кодировка символов, это «обертнутый» Unicode. Вы, наверное, знаете Base64 кодировку, или слышали о ней — она может обернуть бинарные данные в печатаемые символы. Дак вот, UTF-8 это такой же Base64 для Unicode, как Base64  для бинарных данных. Это раз. Если вы это поймете, то уже многое станет ясно. И она также, как Base64, признана решить проблему совместимости в символах (Base64 была придумана для email, чтобы передавать файлы почтой, в которой все символы — печатаемые)
  2. Далее, если код работает с UTF-8, то внутри он все равно работает с Unicode кодировками, то есть, где-то глубоко внутри есть таблицы символов именно Unicode символов. Правда, можно не иметь таблиц символов Unicode, если надо просто посчитать, сколько символов в строке, например (см. ниже)
  3. UTF-8 сделан с той целью, чтобы старые программы и сегодняшние компьютеры могли работать нормально с Unicode символами, как со старыми кодировками, типа KOI8, Windows-1251 и т.п.. В UTF-8 нет байтов с нулями, все байты — они либо от 0x01 — 0x7F, как обычный ASCII, либо 0x80 — 0xFF, что также работает под программами, написанными на Си, как и работало бы не с ASCII символами. Правда, для корректной работы с символами программа должна знать Unicode таблицы.
  4. Все, что имеет старший 7-ой бит в байте (если считать биты с нулевого) UTF-8 — часть кодированного потока Unicode.

UTF-8 изнутри

Если вы знаете битовую систему, то вот вам краткая памятка, как кодируется UTF-8:

Первый байт Unicode символа в UTF-8 начинается с байта, где 7-ой бит всегда единица, и 6-ой бит всегда также единица. При этом в первом байте, если смотреть на биты слева направо (7-ой, 6-ой и так до нулевого), идет столько единиц, сколько байтов, включая первый, идет на кодирование одного Unicode символа. Заканчивается последовательность единиц нулем. А после этого идут биты самого Unicode символа. Остальные биты Unicode символа попадают во второй, или даже в третий байты (максимум три, почему — смотрите чуть ниже). Остальные байты, кроме первого, всегда идут с началом ’10’ и потом 6 битов следующей части Unicode символа.

Пример

Например:  есть байты 11010000 и второй 10011110. Первый — начинается с ‘110’ — это значит, что раз две единицы — будет два байта UTF-8 потока, и второй байт, как и все остальные, начинается с ’10’. А кодируют эти два байта символ Unicode, который состоит из 10100 битов от первого куска + 101101 от второго, получается [10000][011110] ->  10000011110 -> 41E в 16-ричной системе, или U+041E в написании Unicode обозначений. Это символ большая русская О.

Сколько максимум байт на символ?

Также, давайте посмотрим, сколько максимум байт уходит в UTF-8, чтобы закодировать 16 бит кодировки Unicode. Вторые и далее байты всегда максимум могут вместить 6 бит. Значит, если начать с конечных байтов, то два байта уйдут точно (2-ой и третий), а первый должен начинаться с ‘1110’, чтобы закодировать три. Значит первый байт максимум в таком варианте может закодировать первые 4 бита символа Unicode. Получается 4 + 6 + 6 = 16 байт. Выходит, что UTF-8 может иметь либо 2, либо 3 байта на символ Unicode (один не может, так как нет надобности кодировать 6 бит (8 — 2 бита ’10’) — они будут ASCII символом. Именно поэтому первый байт UTF-8 никогда не может начинаться с ’10’).

Заключение

Кстати, благодаря такой кодировке, можно взять любой байт в потоке, и определить: является ли байт Unicode символом (если 7-ой бит — значит не ASCII), если да, то первый ли он в потоке UTF-8 или не первый (если ’10’, значит не первый), если не первый, то мы можем переместиться назад побайтово, чтобы найти первый код UTF-8 (у которого 6-ой бит будет 1), либо переместится вправо и пропустить все ’10’ байты, чтобы найти следующий символ. Благодаря такой кодировке, программы также могут, не зная Unicode, считать, сколько символов в строке (на основании первого байта UTF-8 вычислить длину символа в байтах). Вообщем, если подумать, кодировка UTF-8 придумана очень грамотно, и в то же время очень эффективно.