dimhold.by
← Артыкулы

Чытаем UTF-8 як 1251: "Привет"

Мне надакучыла гадаць. Я вырашыў паглядзець на гэта ў байтах. Надрукаваў Привет у UTF-8 і атрымаў дванаццаць байтаў на шэсць літар. Потым аддаў іх дэкодару, які ўпэўнены, што адзін байт роўны аднаму сімвалу:

$ printf 'Привет' | xxd
00000000: d09f d180 d0b8 d0b2 d0b5 d182            ............

$ printf 'Привет' | iconv -f CP1251 -t UTF-8
Привет
набрана П р и в е т байты utf-8 d0 9f d1 80 d0 b8 d0 b2 d0 b5 d1 82 чытаем як CP1251 Р џ С Ђ Р ё Р І Р µ С чытаем як CP1252 Ð Ÿ Ñ Ð ¸ Ð ² Ð µ Ñ чытаем як ISO-8859-1 Ð Ñ Ð ¸ Ð ² Ð µ Ñ чытаем як CP866 Я А В чытаем як KOI8-R п ÷ я п п п я
Дванаццаць байтаў не мяняюцца. Мяняецца толькі табліца, паводле якой іх чытаюць. У ISO-8859-1 на месцах 9f, 80 і 82 стаяць кіравальныя C1, таму тры ячэйкі пустуюць, а CP1252 іх запаўняе.

Звычайна я выпраўляю такое пераборам кадовак у выпадальным спісе, пакуль тэкст не вернецца. Спосаб працуе. Пры гэтым я ні разу не зазірнуў, што там пад капотам.

Шэсцьдзясят шэсць літар, два першыя байты

Руская кірыліца жыве ад U+0410 да U+044F, а Ё і ё стаяць асобна на U+0401 і U+0451. У UTF-8 усе яны займаюць па два байты. Я сабраў першы байт кожнай літары, вялікай і малой:

$ perl -CO -e 'print map { chr } (0x401, 0x451, 0x410..0x44f)' \
    | perl -ne 'for my $b (unpack "C*", $_) { $seen{$b} = 1 if $b >= 0xC0 }
        END { print join " ", map { sprintf "%02x", $_ } sort keys %seen }'
d0 d1

Два значэнні на шэсцьдзясят шэсць літар. Значыць, любая аднабайтавая кодавая старонка намалюе сваё ўяўленне пра d0 і d1 перад кожным другім сімвалам. З-за гэтага смецце можна чытаць у адваротны бок:

дэкодарd0d1Привет прыходзіць як
CP1251РСПривет
CP1252ÐÑПривет
ISO-8859-1ÐÑÐÑивеÑ
CP866╨Я╤А╨╕╨▓╨╡╤В
KOI8-Rпяп÷я─п╦п╡п╣я┌

Частакол з Р і С значыць, што файл прачыталі як 1251. На практыцы гэта амаль заўсёды мой выпадак. Астатнія чатыры трапляюцца радзей і гэтак жа чытаюцца па першай калонцы.

CP1252 і ISO-8859-1 згодныя наконт d0 і d1, таму адбітак у іх аднолькавы. А вось у дыяпазоне ад 80 да 9f яны разыходзяцца. У CP1252 там друкавальныя сімвалы. Ён выдае Привет: дванаццаць сімвалаў на дванаццаць байтаў. У ISO-8859-1 той самы дыяпазон заняты кіравальнымі C1. Байты 9f, 80 і 82 не друкуюцца наогул, таму той самы радок выходзіць як ÐÑивеÑ, дзевяць бачных сімвалаў. Я паўгадзіны звяраў колькасць сімвалаў, перш чым да гэтага дайшоў.

Найцікавей мне было праверыць сваю мову. Беларуская кірыліца таксама ўкладваецца ў d0 і d1, разам з ў. А ўкраінская ламае правіла: Ґ кадуецца як d2 90. У 1251 d2 малюецца як Т.

Назад ужо не сабраць

Прачытаць байты 1251 як UTF-8 значыць зладзіць тую самую аварыю наадварот. Паводзіць яна сябе зусім інакш:

$ printf 'Привет' | iconv -f UTF-8 -t CP1251 | xxd
00000000: cff0 e8e2 e5f2                           ......

$ printf 'Привет' | iconv -f UTF-8 -t CP1251 | iconv -f UTF-8 -t UTF-8
iconv: (stdin):1:0: cannot convert

Ніводзін з гэтых шасці байтаў не пачынае карэктную UTF-8-паслядоўнасць, таму iconv спыняецца на першым жа. Рэдактар, у якім у мяне быў адкрыты той самы файл, павёў сябе менш сумленна. Ён падставіў U+FFFD на кожную пазіцыю, з-за чаго здаецца, быццам з тэкстам яшчэ можна працаваць.

Варыянт з Р і С захоўвае ўсе зыходныя байты. Калі прагнаць краказябры назад праз 1251, вяртаецца роўна тое, што было:

$ printf 'Привет' | iconv -f CP1251 -t UTF-8 | iconv -f UTF-8 -t CP1251 | xxd
00000000: d09f d180 d0b8 d0b2 d0b5 d182            ............

А калі замест адваротнай канверсіі смецце проста захаваць, файл акажацца закадаваны двойчы. Дванаццаць байтаў ператвараюцца ў дваццаць пяць.

$ printf 'Привет' | iconv -f CP1251 -t UTF-8 | wc -c
25

У KOI8-R алфавіт раскладзены па лацінцы

KOI8-R выглядае так, быццам алфавіт ператасавалі. ю ідзе раней за а, ц сядзіць паміж б і д. Вось верхняя палова табліцы, а побач з кожным байтам стаіць ASCII-сімвал, у які ён ператвараецца пасля скідання восьмага біта:

c0 ю @  c1 а A  c2 б B  c3 ц C  c4 д D  c5 е E  c6 ф F  c7 г G
c8 х H  c9 и I  ca й J  cb к K  cc л L  cd м M  ce н N  cf о O
d0 п P  d1 я Q  d2 р R  d3 с S  d4 т T  d5 у U  d6 ж V  d7 в W
d8 ь X  d9 ы Y  da з Z  db ш [  dc э \  dd щ ]  de ч ^  df ъ _

а стаіць на A, б на B, ц на C, ф на F. Большасць літар легла паверх сваёй лацінскай транслітарацыі. Тым, каму пары не знайшлося, дасталіся рэшткі: так ю апынулася на @, а ъ на _. Скіньце восьмы біт у радка ў KOI8-R. Транслітарацыя праявіцца сама:

$ printf 'Кодировка' | iconv -f UTF-8 -t KOI8-R | perl -pe 's/(.)/chr(ord($1) & 0x7f)/ge'
kODIROWKA

Здравствуйте   fa c4 d2 c1 d7 d3 d4 d7 d5 ca d4 c5   ->   zDRAWSTWUJTE
Кодировка      eb cf c4 c9 d2 cf d7 cb c1            ->   kODIROWKA
Спасибо        f3 d0 c1 d3 c9 c2 cf                  ->   sPASIBO
koi8-r К = eb = 1 1 1 0 1 0 1 1 bit 8 & 0x7F k = 6b = 0 1 1 0 1 0 1 1 Кодировка kODIROWKA
Байт eb у KOI8-R гэта К. Скіньце восьмы біт, атрымаецца 6b, гэта значыць k. Увесь алфавіт раскладзены паводле гэтага прынцыпу.

Рэгістр перакульваецца, бо малая кірыліца займае дыяпазон вялікай лацінкі. Канал па дарозе з’еў восьмы біт, а тэкст усё яшчэ чытаецца. Пры такім адсотку трапленняў гэта наўрад ці выпадковасць. Тая самая аперацыя над байтамі 1251 дае Ophber.

Усе тры словы, якія я ўзяў, складаюцца з літар, якія маюць лацінскую пару. ъ ці щ усярэдзіне выйшлі б знакамі прыпынку.

Што не атрымалася

Я хацеў прагнаць праз усе дэкодары фразу даўжэйшую, Здравствуйте, коллеги. CP1252 адмовіўся:

CP1252   ЗдравÑ
iconv: (stdin):1:11: cannot convert

Ён стаў на адзінаццатым байце, а гэта 81. У CP1252 пяць байтавых значэнняў не вызначаныя. 81 адно з іх, так што канвертару, які ідзе строга па табліцы, вяртаць няма чаго. Той самы дыяпазон C1, з-за якога CP1252 і ISO-8859-1 друкуюць па-рознаму, аказаўся і тым, дзе адзін з іх падае зусім.

У гэтым дыяпазоне цесна. У трыццаці чатырох літар з шасцідзесяці шасці другі байт ляжыць паміж 80 і 9f. Гэта значыць, крыху больш за палову любога кірылічнага тэксту трапляе туды, дзе гэтыя дзве табліцы перастаюць супадаць. Што робяць з тымі ж байтамі CP1250 і CP1257, я пакуль не правяраў.