Мне надакучыла гадаць. Я вырашыў паглядзець на гэта ў байтах. Надрукаваў Привет у UTF-8 і атрымаў дванаццаць байтаў на шэсць літар. Потым аддаў іх дэкодару, які ўпэўнены, што адзін байт роўны аднаму сімвалу:
$ printf 'Привет' | xxd
00000000: d09f d180 d0b8 d0b2 d0b5 d182 ............
$ printf 'Привет' | iconv -f CP1251 -t UTF-8
Привет
Звычайна я выпраўляю такое пераборам кадовак у выпадальным спісе, пакуль тэкст не вернецца. Спосаб працуе. Пры гэтым я ні разу не зазірнуў, што там пад капотам.
Шэсцьдзясят шэсць літар, два першыя байты
Руская кірыліца жыве ад 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 перад кожным другім сімвалам. З-за гэтага смецце можна чытаць у адваротны бок:
| дэкодар | d0 | d1 | Привет прыходзіць як |
|---|---|---|---|
| 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
Рэгістр перакульваецца, бо малая кірыліца займае дыяпазон вялікай лацінкі. Канал па дарозе з’еў восьмы біт, а тэкст усё яшчэ чытаецца. Пры такім адсотку трапленняў гэта наўрад ці выпадковасць. Тая самая аперацыя над байтамі 1251 дае Ophber.
Усе тры словы, якія я ўзяў, складаюцца з літар, якія маюць лацінскую пару. ъ ці щ усярэдзіне выйшлі б знакамі прыпынку.
Што не атрымалася
Я хацеў прагнаць праз усе дэкодары фразу даўжэйшую, Здравствуйте, коллеги. CP1252 адмовіўся:
CP1252 ЗдравÑ
iconv: (stdin):1:11: cannot convert
Ён стаў на адзінаццатым байце, а гэта 81. У CP1252 пяць байтавых значэнняў не вызначаныя. 81 адно з іх, так што канвертару, які ідзе строга па табліцы, вяртаць няма чаго. Той самы дыяпазон C1, з-за якога CP1252 і ISO-8859-1 друкуюць па-рознаму, аказаўся і тым, дзе адзін з іх падае зусім.
У гэтым дыяпазоне цесна. У трыццаці чатырох літар з шасцідзесяці шасці другі байт ляжыць паміж 80 і 9f. Гэта значыць, крыху больш за палову любога кірылічнага тэксту трапляе туды, дзе гэтыя дзве табліцы перастаюць супадаць. Што робяць з тымі ж байтамі CP1250 і CP1257, я пакуль не правяраў.