суббота, 24 августа 2013 г.

О кодировках, кодовых страницах и проблемах вывода на консоль

  Вряд ли это сейчас сильно актуально, но может кому-то покажется интересным (или просто вспомнит былые годы).

Начну с небольшого экскурса в историю компьютера. Поскольку компьютер использовался для обработки информации, то он просто обязан представлять эту информацию в «человеческом» виде. Компьютер хранит информацию в виде чисел (байтов), а человек воспринимает символы (буквы, цифры, различные знаки). Значит, надо сделать сопоставление число <-> символ и задача будет решена. Сначала посчитаем, сколько символов нам надо (не забудем, что "мы" - американцы, использующие латинский алфавит). Нам надо 10 цифр+ 26 заглавных букв английского алфавита + 26 строчных букв + математические знаки (хотя бы +-/*=><%) + знаки препинания (.,!?:;’” ) + различные скобки + служебные символы (_^%$@|) +32 непечатных управляющих символов для работы с устройствами (в первую очередь, с телетайпом). В общем, 128 символов хватает «впритык» и этот стандартный набор символов "мы" назвали ASCII, т.е. «American Standard Code for Information Interchange»

Отлично, для 128 символов достаточно 7 бит. С другой стороны, в байте 8 бит и каналы связи 8-битные (забудем про «доисторические» времена, когда в байте и каналах бит было меньше). По 8-ми битному каналу будем передавать 7 бит кода символа и 1 бит контрольный (для повышения надежности и распознавания ошибок). И все было замечательно, пока компьютеры не стали использоваться в других странах (где латиница содержит больше 26 символов или вообще используется не латинский алфавит). Вместо того, чтобы всем поголовно освоить английский, жители СССР, Франции, Германии, Грузии и десятков других стран захотели, чтобы компьютер общался с ними на их родном языке. Пути были разные (в зависимости от остроты проблемы): одно дело, если к 26 символам латиницы надо добавить 2-3-10 национальных символа (можно пожертвовать какими-то специальными) и другое дело, когда надо «вклинить» кириллицу. Теперь "мы" - русские, стремящиеся "русифицировать" технику. Первыми были решения на основе замены строчных английских букв прописными русскими. Однако проблема в том, что русских букв (33) и они не влезают на 26 мест. Надо "уплотнить" и первой жертвой этого уплотнения пала буква Ё (еe просто повсеместно заменили на Е). Другой прием – вместо «русских» A,E,K,M,H,O,P,C,T стали использовать похожие английские (таких букв даже больше чем надо, но в некоторых парах прописные похожие, а строчные - не очень: Hh Tt Bb Kk Mm). Но все же "вклинили " и в результате весь вывод шел ПРОПИСНЫМИ БУКВАМИ, что неудобно и некрасиво, однако со временем привыкли. Второй прием – «переключение языка». Код русского символа совпадал с кодом английского символа, но устройство помнило, что сейчас оно в русском режиме и выводило символ кириллицы (а в английском режиме – латиницы). Режим переключался двумя служебными символами: Shift Out (SO, код 14) на русский и Shift IN (SI, код 15) на английский (интересно, что когда-то в печатных машинках использовалась двухцветная лента и SO приводил к физическому подъему ленты и в результате печать шла красным, а SI ставил ленту на место и печать снова шла черным).  Текст с большими и маленькими буквами стал выглядеть вполне прилично. Все эти варианты более-менее работали на больших компьютерах, но после выпуска IBM PC началось массовое распространение персональных компьютеров по всему миру и надо было что-то решать централизовано.

Решением стала разработанная фирмой IBM технология кодовых страниц. К этому времени «контрольный символ» при передаче потерял свою актуальность и все 8-бит можно было использовать для кода символа. Вместо диапазона кодов 0-127 стал доступен диапазон 0-255. Кодовая страница (или кодировка)– это сопоставление кода из диапазона 0-255 некоему графическому образу (например, букве «Я» кириллицы или букве «омега» греческого). Нельзя сказать "символ с кодом 211 выглядит так", но можно сказать "символ с кодом 211 в кодовой странице CP1251 выглядит так: У, а в CP1253(греческая) выглядит так: Σ ".Во всех (или почти всех) кодовых таблица первые 128 кодов соответствуют таблице ASCII, только для первых 32 непечатных кодов IBM «назначила» свои картинки (которые показывается при выводе на экран монитора). В верхней части IBM разместила символы псевдографики (для рисования различных рамок), дополнительные символы латиницы, используемые в странах Западной Европы, некоторые математические символы и отдельные символы греческого алфавита. Эта кодовая страница получила название CP437 (IBM разработала и множество других кодовых страниц) и по умолчанию использовалась в видеоадаптерах. Кроме того, различные центры стандартизации (мировые и национальные) создали кодовые страницы для отображения национальных символов.

Наши компьютерные "умы" предложили 2 варианта: основная кодировка ДОС и альтернативная кодировка ДОС.  Основная предназначалась для работы везде, а альтернативная - в особых случаях, когда использование основной неудобно. Оказалось, что таких особых случаев большинство и основной (не по названию, а по использованию) стала именно "альтернативная" кодировка.  Думаю, такой исход был ясен с самого начала для большинства специалистов (кроме "ученых мужей", оторванных от жизни). Дело в том, что в большинстве случаев использовались английские программы, которые "для красоты" активно использовали псевдографику для рисования различных рамок и тп. Типичные пример - суперпопулярный Нортон коммандер, стоящий тогда на большинстве компьютеров. Основная кодировка на местах псевдографики разместила русские символы и панели нортона выглядели просто ужасно (равно как и любой другой псевдографический вывод). А альтернативная кодировка бережно сохранила символы пседографики, использую для русских букв другие места. В результате и с Нортон коммандером и с другими программами вполне можно было работать. Андрей Чернов (широко известная личность в то время)  разработал кодировку KOI8-R (КОИ8), пришедшую с "больших" компьютеров, где господствовал UNIX. Ее особенностью было то, что если у русского символа пропадал 8й бит, то получившийся в результате "обрезания" английский символ будет созвучен исходному русскому. И вместо "Привет" получался "pRIVET", что не совсем то, но хотя бы читаемо.

Отступление для самых любознательных, которым интересно, почему получалось "pRIVET", а не "Privet". В абсолютном большинстве кодировок коды ЗАГЛАВНЫХ букв меньше, чем коды строчных. В большинстве, но не в КОИ, причем сделано это специально и продуманно. Корни этого решения зарыты глубоко-глубоко, а точнее в 7-битных кодировках, когда национальные символы занимали место английских строчных. Для примера рассмотрим английскую "p", ее код 11210 или 7016  и на ее место в КОИ7 поместим русскую П. В результате в 128 кодах разместились и русские и английские символы, но только прописные, текст получился РУССКО-АНГЛИЙСКИЙ. Если программа или устройство не знает про нашу КОИ7, то они выведут строчную английскую букву и вместо ПРИВЕТ получится privet. Когда в ход пошли 8-битные кодировки, для кодов прописных букв в КОИ8 использовали тот же код, то и в КОИ7, только увеличенный на 128 (его как бы подняли вверх таблицы, установив 8 бит). А для строчных установили 8 бит у соответствующих прописных английских. В результате получилось соответствие: p<->П, а P<->п.  Русский текст, набранный прописными буквами в КОИ8 так же по-русски и так же прописными печатался и  в КОИ7 (при "обрезании 8-го бита) и таким образом обеспечивалась совместимость сверху-вниз (буквально!) 8-ми и 7-ми битных кодировок, особенно актуальная при передаче текстовых данных по каналам связи, когда одни узлы работают с 8-ми битными символами, а другие с 7-ми битными. Только "сладкая парочка" КОИ7-КОИ8 если не решала проблему "кракозябр", то в какой-то мере снимала ее остроту и поэтому долгое время центры коммуникаций (большинство) использовали ее.

Переключимся на основную тему: итак, в СССР на компьютерах использовали 3 различных кодовых страницы (основную, альтернативную и  KOI8). И это не считая различных "вариаций", когда в альтернативной кодировке, скажем, отдельные символы (а то и строки) изменялись. От KOI8 тоже "отпочковывались" варианты - украинский, белорусский, таджикский, кавказский и др. Оборудование (принтеры, видеоадаптеры) тоже надо было настраивать (или "прошивать") для работы со своими кодировками. Коммерсанты могли привезти дешевую партию принтеров (из эмиратов, например, по бартеру) а они не работали с русскими кодировками.

 Тем не менее в целом кодовые страницы позволили решить проблему вывода национальных символов (устройство просто должно уметь работать с соответствующей кодовой страницей), но породили проблему множественности кодировок, когда почтовая программа отправляет данные в одной кодировке, а принимающая программа показывает их в другой. В результате пользователь видит так называемые "кракозябры" (вместо "привет" написано "ЏаЁўҐв" или "оПХБЕР"). Потребовались программы-перекодировщики, переводящие данные из одной кодировки в другую. Увы, порой письма при прохождении через почтовые серверы неоднократно автоматически перекодировались (или даже "обрезался" 8-й бит) и нужно было найти и выполнить всю цепочку обратных преобразований.

После массового перехода на Windows к трем кодовым страницам добавилась четвертая (Windows-1251 она же CP1251 она же ANSI ) и пятая (CP866 она же OEM или DOS). Не удивляйтесь - Windows для работы с кириллицей в консоли по-умолчанию использует кодировку CP866 (русские символы такие же как в "альтернативной кодировке", только некоторые спецсимволы отличаются), для других целей - кодировку CP1251. Почему Windows понадобилось две кодировки, неужели нельзя было обойтись одной? Увы, не получается: DOS-кодировка используется в именах файлов (тяжелое наследие DOS) и консольные команды типа dir, copy должны правильно показывать и правильно обрабатывать досовские имена файлов. С другой стороны, в этой кодировке много кодов отведено символам псевдографики (различным рамкам и т.п.), а Windows работает в графическом режиме и ей (а точнее, windows-приложениям) не нужны символы псевдографики (но нужны занятые ими коды, которые в CP1251 использованы для других полезных символов). Пять кириллических кодировок поначалу еще больше усугубили ситуацию, но со временем наиболее популярными стали Windows-1251 и KOI8, а досовскими просто стали меньше пользоваться. Еще при использовании Windows стало неважно, какая кодировка в видеоадаптере (только изредка, до загрузки Windows в диагностических сообщениях можно видеть "кракозябры").

Решение проблемы кодировок пришло, когда повсеместно стала внедряться система Unicode (и для персональных ОС и для серверов). Unicode каждому национальному символу ставит в соответствие раз и навсегда закрепленное за ним 20-ти битовое число ("точку" в кодовом пространстве Unicode, причем чаще всего хватает 16 бит, поскольку 20-битные коды используются для редких символов и иероглифов), поэтому нет необходимости перекодировать (подробнее об Unicode см следующую запись в журнале). Теперь для любой пары <код байта>+<кодовая страница> можно определить соответствующий ей код в Unicode (сейчас в кодовых страницах для каждого 8-битного кода показывается 16-битный код Unicode) и потом при необходимости вывести этот символ для любой кодовой страницы, где он присутствует. В настоящее время проблема кодировок и перекодировок для пользователей практически исчезла, но все же изредка приходят письма, где либо тема письма либо содержание "не в той" кодировке.

Интересно, что примерно год назад проблема кодировок ненадолго всплыла при "наезде" ФАС на сотовых операторов: http://briansk.ru/world/rossiyane-pereplachivaut-za-sms.201185.256492.html , де мол те дискриминируют русскоязычных пользователей, поскольку за передачу кириллицы берут больше. Это объясняется техническим решением, выбранным разработчиком протокола SMS  связи. Если бы его россияне разработали, они бы, возможно, отдали приоритет кириллице. В указанной статье "начальник управления контроля транспорта и связи Дмитрий Рутенберг отметил, что существуют и восьмибитные кодировки для кириллицы, которые могли бы использовать операторы." Во как - на улице 21-й век, Unicode шагает по миру, а господин Рутенберг тянет нас в начало 90-х, когда шла "война кодировок" и проблема перекодировок стояла во весь рост. Интересно, в какой кодировке должен получить СМС Вася Пупкин, пользующийся финским телефоном, находящийся в Турции на отдыхе, от жены с корейским телефоном, отправляющей СМС из Казахстана? А от своего французского компаньона (с японским телефоном), находящегося в Испании? Думаю, никакой начальник ответа на этот вопрос дать не сможет. К счастью, это "экономное" предложение не воплотилось в жизнь.

Юный читатель может спросить - а что помешало сразу использовать Unicode, зачем были придуманы эти заморочки с кодовыми страницами? Думаю, дело в финансовой стороне проблемы. Unicode требует в 2 раза больше памяти, а память стоит денег (и дисковая и ОЗУ). Стал бы американец покупать компьютер на 1-2 тыс дороже из-за того, что "теперь новая ОС требует больше памяти, но позволяет без проблем работать с русским, европейскими, арабскими языками"? Боюсь, простой англоязычный покупатель воспринял бы такой аргумент "неадекватно" (и обратился бы к другим производителям).

Небольшой бонус: программка для показа различных кодовых страниц: http://rusfolder.com/control/?file_id=37594813


Консольный вывод в программах (Windows)


От "кодировочных проблем" пользователей перейдем к проблемам программистов. Какие сложности для разрабочиков программ порождает наличие множества кодовых страниц, какие варианты решений применяются и как эти решения зависят от языков программирования. Самое первое ( и пожелуй, самое важное), что должее понимать разработчик - это то, что небоходим комплексный подход к проблеме в целом. Какая информация и как буде вводиться, как она будет храниться в ОЗУ и обрабратываться и, наконец, как она будет передаваться, выводиться и сохраняться на в файлах. Какая среда программирования используется и какие средства для работы с кодировками она предоставляет. Бесполезно (ну, почти бесполезно) пытаться русифицировать ввод/вывод "этой строки, этих данных" без понимания того, что будет с ними происходить в дальнейшем. Это приведет к наложению заплат, потом наложению "заплат на заплаты"  и разобраться в этой каше даже автору будет весьма трудно...

C (Си)
Начнем с языка "си": давайте напишем очень простую программу prim1 для вывода “Hello, world!” – это классика из книги «Язык программирования Си» Брайана Кернигана и Денниса Ритчи, опубликованной в 1978 году. Написали, скомпилировали, запустили и видим на экране: “Hello, world!”. В чем проблемы?  Проблем нет, но при замене “Hello, world!” на “Всем привет!” проблемы скорей всего появятся... С этими проблемами сталкиваются многие начинающие программисты, с удивлением обнаруживающие, что выводимый на экран русский текст совсем не тот, что они писали в программе. Причина такого несоответствия в длинной цепочке: текст_программы->программа->исполняющая_система->экран и опасность подстерегает на каждом шагу.
Кодовые страницы используются при наборе текста программы, при хранении текста в запускаемой программе, при выводе текста на экран исполняющей системой. И все эти кодовые страницы могут быть разными! При изучении "Си" начинающий программист пишет простенькие программы и выводит текст на экран. Формы он пока не знает и использует консоль для вывода, а текст набирает в текстовом редакторе. «Си» про двухбайтовые символы ничего не знает, тип char (символ) у него занимает 1 байт, этот байт он хранит как есть и вместо “Всем привет!” выводит на экран: ёхь яЁштх!. Оказывается, текстовый редактор использует кодовую страницу 1251 и при построении программы текст в ней будет именно в этой кодировке, а консоль использует кодовую страницу 866, поэтому на экране не то, что хотел программист. Чтобы вывести текст по-русски, программисту надо набрать исходный текст в кодировке 866 и построить программу prim2, тогда все получится как надо. Самые грамотные пользователи могут сказать: зачем набирать программу в 866 кодировке, можно переключить кодовую страницу командой chcp и все! Давайте переключим “chcp 1251” и запустим prim1 - все равно на экране мусор… Чтобы после переключения на страницу 1251 символы выводились правильно, надо шрифт консоли переключить на Lucida Console и только тогда prim1 выведет на экран “Всем привет!”. Еще один вариант работы со строками 1251 – это перекодировать их «на лету» перед выводом на экран (своей функцией или какой-либо готовой, типа CharToOem).

C++
Более продвинутый по сравнению с обычным “Си”, но настроить программу для работы с консолью не просто, причем в разных системах разработки это делается по-разному. Например, в VisualStudio вызывают процедуру setlocale( LC_ALL,"Russian" )  и после этого cout<<”Всем привет!” выводится правильно.  Вуаля!

К сожалению, это только решение проблемы вывода, а когда начнем вводить, тут нас ждет засада. Напишем (в VS) программу, которая вводит текст с консоли в буфер и тут же выводит его обратно для проверки. Вот ключевой фрагмент программы:
char buf[256];
setlocale( LC_ALL,"Russian" ); // можно закомментировать для изучения
// SetConsoleCP(1251);
// SetConsoleOutputCP(1251);
cout<<"Введите текст по-русски (абвг): ";
// SetConsoleCP(1251);
cin >> buf;
// SetConsoleCP(866);
// OemToCharA((LPCSTR)buf,(LPSTR)buf); // перекодировка DOS->WIN
cout << buf;

В зависимости от [рас]комментированных функций результат разный:
рсту(рсту) ->[224,225,226,227]->абвг   // консоль выводит в кодировке1251 (WIN)
рсту(Ёё..) ->[224,225,226,227]->абвг    // консоль выводит 1251
абвг(абвг) ->[224,225,226,227]->рсту   // консоль выводит 866 (DOS)
абвг(рсту) ->[224,225,226,227]->рсту   // консоль выводит 866
абвг(рсту) ->[160,161,162,163]->рсту   // консоль выводит 866, 160-это 'а' в 866 кодировке.

Первый вариант означает, что мы набираем "рсту", при этом на экране видим "рсту", в буфер попадают коды 224,225,226,227 и на экран обратно выводится "абвг". Разберемся в этой каше. Символы с кодами 224,225,226,227 (их удобно использовать для анализа проблем кодировок) в кодировке 1251(Windows) выводятся как "абвг", а в кодировке 866(DOS) выводятся как "рсту". Консоль выводит символы на экран в указанной в конце примера кодировке. В первой строке программа поместила введенные символы в DOS-кодировке, а потом их вывела, но уже в Win-кодировке.

Увы, среди этого "зоопарка" нет варианта, чтобы :
а) консоль выводила в 1251,
б) вводимые символы правильно показывались на экране,
в) в буфер попадали символы в кодировке 1251.
Что делать?
Вариант1: действовать как в программе, только после ввода символов с клавиатуры раскомментировать строку с перекодировкой.
Вариант2: раскомментировать строку перед вводом и строку после ввода. Строка "до" временно изменит режим работы консоли и символы попадут в буфер в Win-кодировке. А строка "после" вернет все обратно, чтобы вывод шел правильно. Мне трудно понять, почему SetConsoleCP(1251); приводит к тому, что вывод идет в DOS кодировке, а SetConsoleCP(866) возвращает все на место. Может, это конфликт с setlocale()?
В общем, "это невозможно понять - это надо запомнить: сол и фасол пишется с мягким знаком, а вилька и тарелька без мягкого знака".

Есть и Вариант3: использовать специальный режим работы консоли и специальный шрифт "LucidaConsole" (выше он упоминался). Если пишете программу "для себя, для изучения программирования - это можно использовать". Но предлагать кому-то: вот моя программа, она делает то-то, только перед ее использованием надо сделать такие шаманские процедуры. Имхо, это просто "не Ubuntu", т.е не гуманно по отношению к пользователям.

Java
Теперь возьмем современный язык – java и напишем в нем программку prim1.java:
public class Test
{
   public static void main(String [] args)
   {
           System.out.println("Всем  привет!”);
   }
}

Компилируем, запускаем и видим на экране: “ёхь яЁштх!”. О-о-о - знакомый набор значков! Все ясно, мы написали программу в кодировке 1251, а консоль выводит в 866. Надо просто набрать текст в 866-й кодировке. Пишем, компилируем, запускам и видим: “Всем привет!”. Все – проблема решена? На первый взгляд – да, но хорошо бы  посмотреть, как выводятся все русские символы. Выводим весь алфавит и видим, что вместо «Ш» на экране «?», а остальные символы выведены правильно. Непонятно – чем буква “Ш” хуже других, почему java ее не любит? Дело в том, что мы совершили две ошибки, но они скомпенсировали друг друга для всех символов, кроме «Ш»... Вот как бывает – большая проблема порой выглядит, как маленький «косячок». Чтобы понять, что это за ошибки и почему они так «удачно» компенсировали друг друга, надо вспомнить (хотя бы в общих чертах), как компилируются и исполняются java-программы. Сначала компилятор javac.exe из теста java-программы prim.java создает файл-программу prim.class, а затем виртуальная машина  java.exe (ВМЯ) выполняет ее.  Java – язык современный и все текстовые строки из prim.java хранятся в скомпилированном файле prim.class в utf-8.  ВМЯ при загрузке файла prim.class преобразует текстовые строки из utf-8 в utf-16 (для персоналок utf-16LE), а при выводе в консоль делает преобразование из utf-16 в системную кодировку Windows (в России это 1251). Проследим теперь за судьбой символа ‘р’ (русская)  из слова "привет".  В кодировке 866 его код 224, а кодировке  1251 этот код у символа ‘а’ (русская). Компилятор javac видит ‘a’ (компилятор считает, что текст набран в кодировке 1251, поскольку windows русский (и нет признака BOM) и записывает в prim.class U+0430. При выводе на экран ВМЯ делает обратное преобразование U+0430 в код 224 (‘а’ в кодировке 1251), но поскольку консоль использует кодировку 866, то на экране вместо ‘а’ мы видим ‘р’ (именно тот символ, что набрали в программе). Трансформация, подобная ‘р’ (866) ->’a’ (1251)->’a’ (utf-8) ->’a’ (utf-16) ->’a’ (1251) ->’р’ (866) происходит со всеми символами, кроме ‘Ш’, поскольку в кодировке 886 его код 152 (9816) , а в кодировке 1251 это код не задействован и компилятор в class-файл  пишет символ ‘?’, который потом и выводится на экран.  Чтобы все символы правильно выводились, надо набирать текст программы в кодировке 1251 (или utf-8), а при запуске ВМЯ указать ей ключ “-Dfile.encoding=CP866”, тогда вывод будет делаться в кодировке 866 и преобразование пойдет так: ‘р’ (1251) ->’р’ (utf-8) ->’р’ (utf-16) ->’р’ (866). Рекомендация: используйте кодировку utf-8 (или хотя бы 1251) при наборе текста java-программ,  а при запуске задавайте ключ “-Dfile.encoding=CP866” для правильного вывода на консоль и проблемы не будет.

C#
хотя и похож на java (и у него тоже есть виртуальная машина), но проблем с выводом в консоль не имеет. Чтобы в этом убедиться, наберём программу:
using System;
namespace ConsoleTest
{
class Program
    {
    static void Main(string[] args)
        {
            Console.Out.WriteLine("Всем привет!");
        }
    }
}
Компилируем, запускам и видим на экране: “Всем привет!”. Впрочем, это неудивительно, поскольку и C# и Windows – детища Microsoft (и это она сделала так, что кодовая страница консоли отличается от кодовой страницы Windows). Сама "поломала" - сама "починила".


"Меня это не касается, я не пишу консольные программы" - так может сказать начинающий программист. Однако проблемы с кодировками, подобные рассмотренным выше, встречаются и при чтении/записи файлов и при обмене по каналам связи.

И снова о Юникоде (Unicode)

    Конечно, о юникоде (Unicode) в интернете море информации (см, например http://ru.wikipedia.org/wiki/Юникод, некоторые фрагменты текста я взял оттуда), я ни в коей мере не претендую на полное и исчерпывающее описание. Это просто некоторая дополнительная «информация к размышлению». В предыдущей заметке я писал о проблемах показа однобайтовых символов. Суть в том, что хотя для показа 26 символов латинского алфавита достаточно 52 кодов (прописные+строчные), для показа национальных символов даже 256 кодов (это байт, единица хранения и адресации) оказывается недостаточно. Для решения проблемы отображения национальных символов использовались (да и сейчас кое-где используются) кодовые страницы. Хочешь выводить национальные символы – используй соответствующую кодовую страницу. Но что, если в тексте нужны символы из многих кодовых страниц? Для набора математического текста могут понадобиться английские, русские, математические, греческие, типографские символы, причем одновременно. Кроме того, для национальных символов используется несколько конкурирующих кодировок (т.е. кодовых страниц), какую выбрать? Решить эти проблемы должен был созданный в 1991 году стандарт Unicode (по-русски Юникод или Уникод).

    Юникод решительно порвал с однобайтовым прошлым и предложил стандарт UCS-2 (universal character set), где каждый символ кодируется раз и навсегда закрепленным за ним 16-ти битовым числом, состоящим из старшего байта и младшего байта (естественно, для прописных и строчных символов – разные числа). Кроме кода, за каждым символом закреплено название (на английском языке). Скажем, за русской прописной “A” закреплен код  104010, а за строчной “я” закреплен код 110310 и название "CYRILLIC SMALL LETTER YA". Здесь коды чисел приведены в 10-ричной системе счисления, но часто используют 16-ричной коды: 104010 соответствует 41016, а коду 110310 соответствует 44F16.  Дабы не гадать, в какой системе счисления записан код и чтобы сразу было ясно, что речь идет не об абстрактном числе, а о коде символа юникода, используется особая запись: U+0410 для “А” и U+044F для “я”. Видим в тесте U+FEFF  и сразу понимаем, что речь идет о символе юникода FEFF16, он же 6527910 (по-русски называется «неразрывный пробел нулевой ширины»). А коды U+0401 и U+0451 назначены символам Ё "CYRILLIC CAPITAL LETTER IO" и ё соотвественно (букву Ё опять "обидели",  присвоив ей коды вне диапазона остальных букв русского алфавита). Все символы кириллицы (да и многие другие вместе с кодами) очень удобно смотреть на http://unicode-table.com/ru/sections/cyrillic/.

    Итак, Unicode перешел от однобайтовой кодировке к 2-байтовой, кардинально расширил количество описываемых символов – можно почивать на лаврах? Отнюдь, с новыми возможностями пришли новые проблемы. Проблема первая – как хранить эти 2 байта символа в памяти и в файлах? Запишем код U+0410 в виде двух байтов: 04(старший) и 10(младший). В памяти, как мы знаем, каждый байт имеет свой адрес и можно записать наш код двумя способами: либо 04 по адресу X, а 10 по адресу X+1, либо 10 по адресу X, а 04 по адресу X+1. Соответственно, в файле 10 будет записано либо после 04 либо перед ним. На первый взгляд проблема высосана из пальца: раз код юникода – это 16-битовое число, давайте и хранить его так, как компьютер хранит 16-битовые числа. А-а-а,  вот тут и попались – компьютеры различной  архитектуры хранят 16-ти битовые числа по-разному: одни хранят старший байт числа перед младшим, а другие - после! Персоналки хранят старший байт после младшего, а многие «большие компьютеры» хранят старший байт перед младшим. Архитектура многих смартфонов такая, что данные хранятся, «как в больших компьютерах», так что для архитектур «размер не имеет значения».  Когда данные пишутся из памяти в файл или читаются из файла в память (напрямую, без участия процессора), байты передаются в порядке возрастания адресов.  С другой стороны, операции сравнения и сортировки требуют работы с кодами как с числами. В общем – либо проблемы с порядком байтов в файлах, либо проблемы с алгоритмами обработки. Победили алгоритмы и 2 байта символа хранятся в памяти так же, как  данная архитектура хранит целые числа (т.е. код «в смысле юникода» и код как число в памяти – это одно и то же 16-ти битовое число, независимо от архитектуры). Соответственно, в файле первым идет либо старший байт числа (стандарт UCS-2BE – “Big Endian” или "прямой порядок"), либо младший байт (стандарт UCS-2LE – “Little Endian” или "обратный порядок").

    Ну хорошо, все же 2 варианта хранения – это не несколько десятков кодовых страниц. Подождите – еще не вечер, обсудили только первую проблему. По мере роста популярности юникода возникло масса желающих добавить в него свои символы. Ладно бы хотели добавить символы типа церковнославянских «ять», «ижица», но появилось желание добавить тысячи иероглифов, знаков древних письменностей и т.п. В общем, 65536 символов не хватило (а ведь когда-то казалось «всем-всем хватит», были даже незанятые области, «про запас»). В результате в 1996 году появился второй стандарт юникода, расширяющий количество доступных символов с 65536 до 1’112’064, так что код символа стал от U+0000 до U+10FFFF.  Расширили «с большим запасом» и даже спустя 16 лет, в стандарте юникода 6.2 от 2012г описано ~110000 символов (плюс ~137000 зарезервировано). Расширить расширили, но как их хранить в памяти, в файлах? Для такого количество символов нужны числа с 21 битом, т.е. 3 байта. И что делать с морем программ, файлов которые хранят и обрабатывают 2-х байтовые символы юникода стандарта USC-2 ?

    В результате нашли компромиссное решение, снижающее сложности перехода. Поступили так: те 65536 (216) кодов из UCS-2 – они наиболее употребительные и  образуют нулевую или базовую плоскость (если представить, что вся область кодов разрезана на слои по 65536 символов). Остальные коды образуют плоскости с 1-й по 16-ю. Символы нулевой плоскости U+0000 – U+FFFF храним как раньше – в виде одного 16-битового числа, а символы с кодами U+010000 - U+10FFFF (таких кодов 220) храним в виде пары 16-ти битных чисел (так называемые «суррогатные пары»), первое число пары из диапазона U+D800 - U+DBFF, а второе - из диапазона U+DС00 - U+DFFF. Легко увидеть, что в каждом диапазоне 10 бит произвольны, в паре это дает произвольное 20-ти битовое число. Но как отличить, представляет ли код U+DA15 соответствующий ему символ из UCS-2 или это первый код суррогатной пары? А не надо отличать – в UCS-2 коды из диапазона D800 - DFFF (2048 символов) были зарезервированы, поэтому им не соответствовали никакие символы. Стандарты представления символов с «суррогатными парами» называются utf-16BE и utf-16LE. Пришлось, конечно, переписывать программы и библиотеки для работы с новым стандартом, но если не было нужды в использовании символов из плоскостей 1-16, то и старые программы отлично работали. Java (которая сразу использовала стандарт UCS-2) включила поддержку суррогатных пар только в версии J2SE 5.0, а до этого как-то обходилась без них… Почему в суррогатных парах диапазоны первого и второго символа различаются? Не знаю… Если бы был один общий диапазон, то тогда бы можно было закодировать не 20 бит, а 22 бита и вместо миллиона доступных символов получить 4 миллиона. Но миллион тоже много, когда еще его «истратят», а раздельные диапазоны дают дополнительный контроль (вдруг файл не юникодовский).

    Резюмирую: для символов U+0000-U+FFFF в utf-16 используются 16-битовое представление, как в UCS-2, а для символов U+10000-U+10FFFF в  используются «суррогатная пара» из кодов в области U+D800-U+DFFF (перед переводом числа U+10000 и выше его уменьшают на 1000016 и полученное 20-битное число кодируют суррогатной парой). Итого, в utf-16 можно представить 220+216-2048 = 1’112’064 символов. В интернете встречается и другое число для количества доступных символов Unicode: 1'114'112 (оно просто включает и суррогатные пары), какое из них правильное - "дело вкуса". Другая тонкость -  то, что символы из области 220  не включают область 216, а дополняют ее и в UTF16 для представления символов из дополнительной области нужно два 16-ти битных числа (суррогатная пара).

Что дало расширение Unicode? Те, кому нужны были новые символы, получили возможность их использовать. Те, кому не нужны редко используемые иероглифы, кто не работает с устаревшей письменностью расширения возможностей [почти] не заметили. А вот программистам мороки добавилось. То ли дело в UCS-16: хочешь получить n-й символ строки, берешь n-й символ массива и все. А с суррогатными парами это не проходит. Пишешь программу, работающую с текстом – не забывай про эти «пары» (даже если тебе они тебе вроде как и не нужны,  все равно используй другие объекты, другие функции).

    Есть ли другие способы представления (хранения) символов юникода, не utf-16? Есть – это utf-32, где каждый символ представлен 32-битным числом. Разумеется, старший байт такого числа всегда 0, а байт перед ним почти всегда 0, но столь неэкономное расходование памяти компенсируется удобством обработки (не нужны суррогатные пары , нет проблемы «взять n-й символ в строке). Естественно, в зависимости от архитектуры, используют вариант либо utf-32BE либо utf-32LE).

    А теперь сюрприз - кроме utf-16/32 есть еще одно замечательное представление символов юникода в виде последовательности байтов (от 1 до 4). Это представление придумали в 1992 году Кен Томпсон и Роб Пайк и назвали его utf-8. В этом представлении символам ASCII (первые 128 символов 0-й плоскости) соответствует сам код символа, т.е. текст из символов с номером меньше 128 в utf-8 состоит из тех же самых байтов, поэтому любую строку в ASCII автоматически можно считать строкой в utf-8.

Символы utf-8 получаются из Unicode следующим образом (из Википедии):
0x00000000 — 0x0000007F: 0xxxxxxx (т.о. символы 0-127 не меняются)
0x00000080 — 0x000007FF: 110xxxxx 10xxxxxx
0x00000800 — 0x0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
0x00010000 — 0x001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

    Чем больше код символа, тем больше надо байтов для его преставления в utf-8. Счастливым пользователям «чисто латинского» алфавита достаточно 1 байта (так что выигрыш по сравнению с utf-16 очевиден) , европейцам только иногда требуется два байта. А для кириллицы всегда надо два байта (как в utf-16), но для смешанного русско-английского текста какой-то выигрыш получается. А вот сирийским, грузинским символам надо три байта... Еще один плюс utf-8 - в компьютерах с любой архитектурой байты хранятся по возрастанию адресов и поэтому не нужны BE/LE (суррогатные пары, кстати, тоже не нужны). Наконец, такая фишка utf-8, как «самосинхронизация». Предположим, программа принимает поток символов в формате utf-8 и выводит их на экран. Пусть по каким-то причинам (сбой, например) программа неверно определила номер байта для принимаемого символа. Естественно, на экран вместо правильного символа будет выведен совсем другой символ. Но очень быстро программа исправится и начнет выводить корректные символы (это не свойство программы – это свойство самой utf-8). А если бы принимали utf-16/32 и потеряли байт (или вместо ожидаемого utf-16LE поступает utf-16BE)? Все, приехали – вместо нужной информации на экране будут «не наши» символы и для правильного показа программа должна сделать какой-то интеллектуальный анализ.
    Кстати, раз уж пошла речь о сбоях и чужих форматах, как программа определит, в каком именно utf записан файл (даже если она точно знает, что имеет дело с юникодом)? Вариантов (минимум) три:
1. В самое начало файла записан специальный символ с кодом U+FEFF. Позвольте, ведь это «неразрывный пробел нулевой ширины».  Да, это он, но теперь как пробел он не используется и у него есть второе имя –“byte order mark” (BOM, маркер порядка байтов). В файл utf-16LE он запишется как FF FE, а в файл utf-16BE как FE FF, поэтому, прочитав пару (тройку) первых байт файла, программа может определить, в каком формате записан файл. А вдруг в файл записан символ с кодом U+FFFE и программа собьется, приняв его за BOM? Не беспокойтесь – символа с кодом U+FFFE не существует, т.е. такой код никому не назначен (и не будет). Для файлов в формате utf-8 первым в файле тоже может быть записан символ U+FEFF (он выглядит как последовательность EF BB BF), хотя здесь он маркирует не порядок байтов, а сам формат utf-8. А если программа старенькая и про новую роль BOM ничего не знает, а принимает его за «неразрывный пробел нулевой ширины»? Ничего страшного – он «пробел» и поэтому пустой, а из-за «нулевой ширины» места на экране не занимает, т.е. на экране его не видно (код специально так подобрали, чтобы не имел пары с обратным порядком байтов и не «мозолил» глаза при выводе)! А его «типографскую роль» сначала взял на себя символ “word joiner” U+2060, а потом и другие (U+200b “zero width space”, U+200d “zero width joiner”)

2. Есть соглашение, что этот файл всегда пишется в определенном формате или есть общесистемное соглашение (к примеру, в персоналках и windows – LE, в «больших» машинах и unix - BE)

3. Если нет ни BOM ни соглашения, то для межплатформенного обмена считается, что файл в стандарте utf-16BE (тут unix и большие машины «победили» windows и персоналки).

Об успешной борьбе LiveJournal с начинающими авторами.

Не буду рассуждать о причинах, по которым начинающий решил создать свой персональный блог. Причин много, они  разные, но есть и общая, а именно: абсолютное большинство создает блог для того, чтобы его читали. Конечно, есть ненулевая вероятность, что некто создает блог только для себя любимого (как говорится в анекдоте «чукча не читатель – чукча писатель»), но большинство авторов надеется и хочет найти своих читателей.
Итак желание есть, теперь возникает вопрос – где его создавать?   Да где угодно – хоть в LiveJournal, например. Страница создана, начинающий автор с энтузиазмом делится своими знаниями, наблюдениями, фотографиями и т.д. Конечно, он не рассчитывает, что его страничка будет пользоваться огромным успехом (во всяком случае сразу), но на некоторый интерес со стороны интернет-сообщества он надеется… Однако проходят месяцы, год – и НИЧЕГО, пусто! Автор беспокоится, задает себе вопросы – в чем причина отсутствия интереса, может пишу скучно или темы неактуальные и никому ненужные…?
Бедный автор, он и не догадывается, что причина не в нем. Когда автор излагал свои мысли в виде книги, брошюры   он, во-первых должен, был издать эту книгу, брошюру и, во-вторых распространить напечатанный тираж (продать, подарить наконец). Тираж, который лежит в углу на складе никто не читает.
Сейчас, в эпоху интернета,   публикация статьи и не означает, что она дойдет до читателя. Проблем с электронными статьями нет – зарегистрировался и опубликовал. Но чтобы это публикация сдала достояние широких народных масс, она должна попасть в поисковики (Google, Яндекс и др). В мире интернета действует принцип: тебя «нет», если тебя нет в поисковиках!  На первый взгляд это небольшая проблема: поисковики постоянно «рыщут по интернету» и если не просканирует через час, так просканирует через день. И все – потенциальные читатели получат ссылки на твой материал и кто-то зайдет глянуть.
Увы, с ЖЖ это не получится – ЖЖ запрещает поисковикам сканировать страницы начинающих авторов, причем большинство авторов об этом не догадывается. Более того, при написании статьи авторам предлагается внести набор ключевых слов, характеризующих материал и это создает иллюзию продвижения материала в поисковиках (поскольку ключевые слова нужны именно для этого).
Я задал вопрос службе поддержки ЖЖ – «почему поисковикам запрещают сканировать страницы начинающих авторов?». Получив ответ просто не поверил своим глазам – оказывается, «В рамках программы по борьбе со спамом в LiveJournal введен запрет на индексирование журналов с малым весом.  Запрет снимается автоматически по мере увеличения интереса к журналу со стороны других пользователей». На мой второй вопрос – как запрет на сканирование страниц поисковиками помогает бороться со спамерами и почему меня априори заблокировали как потенциального спамера внятного ответа я не получил.
Возникла удивительная ситуация:
1.    Наказание без преступления: раз пишите, значит можете спамить, значит мы Вас заблокируем
2.    Чтобы снять блокировку, страница должна стать популярной и возникает элементарный порочность круг: для разблокировки нужна популярность,  для популярности нужны читатели, для потока читателей нужна разблокировка.
3.    В правилах, на которые ссылается служба поддержки, есть описание того, чего нельзя делать (в т.ч. спамить), но ни слова не сказано про то, что страницы начинающих авторов будут  заблокированы для поисковиков. Имхо, борьба со спамом – это дело поисковиков и специальных органов.
4.     В СССР было правило: запрещалось прописывать граждан не имеющих работы и запрещалось принимать на работу граждан без прописки - и у многих людей были проблемы…
5.    Представим бытовую аналогию:  Вы берете товар с полки, а Вас как потенциального вора (раз взяли товар, значит мог пронести через кассу без оплаты) запирают в комнате, пока не докажете, что добропорядочный покупатель.  А доказать можете, предъявив купленные ранее товары вместе с чеками…


Вот ссылка на переписку со службой поддержки (дабы не возникла мысль, что я все это придумал или сгущаю краски) http://www.livejournal.com/support/see_request.bml?id=1664550