Джоэл Спольски. Что нужно знать о Юникод

Обязательный минимум который должен знать каждый разработчик ПО о Юникод и о Таблицах Символов.
(Оправдания не допускаются) Joel Spolsky.



Статья довольно старая, но вместе с тем не потеряла актуальности. Не забывайте, что статья датируется 2003 годом.

Вы никогда не удивлялись, что за такой мистический тег Content-Type? Вы знаете, что его нужно добавить в вашу html страничку, но зачем это надо делать и для чего он вообще там не имеете ни малейшего понятия?

Вы наверняка получали почтовое сообщение от своего друга из Болгарии (имейте ввиду автор оригинала статьи из USA) и тема письма выглядела так "?????? ?????? ???????? ??????".

Меня удивило открытие того, как много разработчиков ПО не полностью понимают разницу между кодировками, таблицами символов, юникодом и т.п. Пару лет назад, бета тестер из FogBUGZ был озадачен возможностью принимать почтовые сообщения из Японии. Япония подумал я? У них почта на японском? Я был без понятия. Тогда я познакомился поближе с коммерческим элементом ActiveX, который мы использовали для парсинга MIME почтовых сообщений, и мы поняли что он неправильно работал с набором символов, и нам пришлось переписать кучу кода чтобы исправить ошибки конвертации из одной кодировки в другую. Затем я познакомился с другой коммерческой библиотекой и там тоже реализация таблицы символов была полностью реализована неправильно. Я связался с разработчиком этого ПО и он выдал мне что-то вроде «Мы ничего не можем с этим поделать». Как и большинство программистов он хотел чтобы его не беспокоили по таким мелочам.

Затем я открыл что популярный инструмент разработки для веб PHP почти полностью игнорировал особенности кодировки символов, он просто использует 8 бит на символ, что делает разработку веб приложений с поддержкой нескольких языков практически невозможной. И тут моя чаша терпения переполнилась.

Так что у меня есть что объявить: если вы программист работающий в 2003 и вы не знаете базовых концепций кодировок, таблиц символов, юникод, я поймаю вас и накажу — заставлю чистить лук на подводной лодке 6 месяцев. Клянусь я это сделаю.

Напоследок еще одна вещь.

Это не так трудно как кажется

В этой статье я дам вам то, что должен знать каждый программист. Думать что 'plain text = ascii = символы по 8 бит каждый' не только неправильно, более того это безнадежно неправильно, и если вы все еще программируете таким образом, вы не лучше доктора который не верит в микробов. Пожалуйста отложите редактор в сторону и не пишите код до тех пор пока не закончите прочтение этой статьи.

Перед тем как я начну, я должен предупредить вас, если вы один из тех кто знает что такое интернационализация приложений, вы найдете мое повествование немного упрощенным. Я просто пытаюсь установить здесь минимальную планку, чтобы каждый смог понять что происходит, и мог писать код с надеждой что он будет работать, даже если он содержит текст на разных языках. Тут я снова должен предупредить вас, что работа с символами только малая порция того, чтобы создавать ПО способное работать в разных языковых средах, но так как я могу писать только об одной вещи в отдельно взятый момент времени то блюдо на сегодня таблица символов.

Историческая перспектива

Простейший способ понять все — это пройтись по истории развития в хронологическом порядке.

Вы возможно думаете я буду говорить о каждом старом наборе символов например EBCDIC? Что ж, вы не угадали, EBCDIC не относится никак к вашей жизни. Так глубоко назад мы не будем возвращаться. В те дни когда Юникс только появился, а K&R писали свое «Язык программирования С» все было просто. EBCDIC было единственным решением. Только символы старого доброго алфавита английской языка имели значение, и у нас было название для этого ASCII, которая могла отобразить все символы используя число между 32 и 127. Пробел был 32, буква «А», была 65 и т.д. Все это легко помещалось в 7 бит. Большинство компьютеров в эти дни были 8 битными, так что вы могли не только хранить символы ASCII, но и у вас оставался 1 бит в запасе, который вы, в случае чего, могли использовать для своих целей. Например знак тусклой лампы в текстовом процессоре WordStar включался этим 8 битом отображая последнюю букву в слове, тем самым ограничивая использование WordStar английским языком. Коды менее 32 назывались непечатаемыми и были предназначены для мата. Шучу. Они использовались для контрольных символов, например символ '7' заставлял ваш компьютер издавать сигнал 'биип', или символ '12' который заставлял ваш принтер промотать до конца страницу, чтобы начать печать с новой.

Все было прекрасно если предположить что вы англоязычный пользователь.

Поскольку в байте содержится 8 бит, много людей подумало, «Черт мы можем использовать коды с 128 по 255 с пользой для себя.» Проблема была в том, что большинство людей подумало об этом одновременно, и каждый решил по своему какому коду назначить какой символ. IBM-PC например придумал свое, более известное как OEM набор символов, который предложил некоторые символы для Европейских языков, и целый набор символов для рисования: горизонтальные и вертикальные линии, горизонтальные линии с маленькими вертикальными черточками по правой стороне и т.д., для того, чтобы рисовать на экране таблицы и линии, которые вы все еще можете увидеть на запущенном 8088 компьютере в вашей химчистке по соседству. В действительности как только компьютеры стали продавать за пределами USA, все виды OEM наборов символов ушли в небытие. Например на некотором компьютере код символа '130' отобразил бы é, а на компьютере проданном в Израиле это была бы буква ג, и если бы американец отправил свое резюме в Израиль, то оно отобразилось бы как rגsumגs. Во многих случаях, например в русском, было множество разных идей что делать с символами коды которых свыше 128, и вы не смогли бы даже достоверно обмениваться документами на русском. (имеется ввиду что было несколько вариантов кодировок русской кириллицы)

В итоге OEM бесплатно для всех «исправил» ANSI стандарт. В ANSI, все согласились что делать с кодами до 128, которые были такие же как и в ASCII, но было много различных способов работать с символами от 128 и выше, в зависимости от того где вы проживаете. Эти разные системы назвали кодовыми страницами (code pages). Например израильская DOS использовала кодовую страницу 862, тогда как греки использовали 737. Они были одинаковыми для символов ниже 128, но разными для символов чей код выше 128. Национальные версии MSDOS имели десятки этих кодовых страниц, работая со всеми от Англии до Исландии, и они имели несколько «мультиязычных» кодовых страниц, которые отображали Эсперанто и Галисийский на том же компьютере. Но работа на иврите и греческом на одном и том же компьютере была невозможной, по крайней мере если только вы не напишите свою программу которая отображает все используя растровую графику, так как иврит и греческий требуют разных кодовых страниц с различной интерпретацией кодов свыше 128.

В то же время в Азии, пришлось принимать во внимание более сумасшедшие идеи. Фактически азиатский алфавит имеет тысячи букв, которые не уместятся в 8 бит. Это было решено системой названной DBCS, ‘двухбайтовый набор символов’, в которой одни символы определялись одним байтом другие двумя. Было просто двигаться вперед по строке, но почти невозможно двигаться в обратную сторону. Программистам теперь надо было не использовать простую конструкцию s++ или s--, чтобы двигаться по строке, а вызывать функцию (В Windows например AnsiNext и AnsiPrev), которые знали как разрулить эту мешанину.

Но все еще большинство исходило из того что байт это символ, а символ это 8 бит, и до тех пор пока ваша строка не переносится из одного компьютера в другой, или скажем не содержит символов более одного языка все будет нормально работать и так. Но тут «внезапно» появился Интернет, и стало общей практикой перемещать строки из одного компьютера в другой, и вся эта конструкция обвалилась. К счастью в этом момент и был изобретен Юникод.

Юникод

Юникод был попыткой создать единый набор символов который будет содержать в себе все символы всех языков на планете, и некоторые верят, что даже символы Клингона тоже(вымышленный язык). Некоторые люди имеют неправильное представление, что Юникод — это просто 16 битный код, где каждому символу соответствует 16 битный код и таким образом существует 65536 возможных символов. Это не соответствует действительности. Это наиболее общий миф о Юникод, и если вы тоже так думали не переживайте.

Фактически, Юникод это другой способ думать о символах, и вы должны понимать как устроен Юникод или дальнейшее потеряет всяких смысл.
До данного момента, мы полагали что каждому символу соответствует определенный набор битов которые вы можете хранить на диске или в памяти:

A-> 0100 0001

В Юникод, каждому символу ставится в соответствие кодовая точка (code point), которая все еще теоретическое понятие. То как эта кодовая точка представлена на диске или в памяти отдельная история.

В Юникод, буква A это теоретическое понятие. Это нечто плавающее в облаках:
A
Эта теоретическая A отличается от B, и отличается от а, но такая же как A или A и A. Это теоретическое понятие основывается на том, что A в шрифте Times New Roman, тот же самый символ, что и A в Helvetica, но отличается от 'a' в нижнем регистре. Это не выглядит противоречиво, но есть языки в которых буква может иметь противоречия или двусмысленность. В германском буква ß реальная буква, или особый стиль написания буквы S? Если фигура буквы изменяется в конце слова, это другая(новая) буква? Иврит скажет 'да' на этот вопрос, а арабский нет. Как бы там ни было, умные люди в консорциуме Юникод выяснили это в течении длительного времени и жарких дискуссий, и вам нет необходимости волноваться по этому поводу. Они уже все уладили.

Каждой теоретической букве в каждом алфавите назначено магическое число консорциумом Юникод, которое записывается следующим образом: U+0639. Это магическое число называется кодовой точкой. Знак U+ означает 'Unicode' и цифры шестнадцатеричные. U+0639 это арабская буква Ain. Английской букве A соответствует U+0041. Вы можете найти их используя утилиту charmap на winxp/win7 или посетив сайт Юникод.

Пределов для количества букв которые может определить Юникод нет, более того фактически они уже перешагнули отметку в 65536, так что не каждая буква в Юникод может быть определена в два байта, но это самый сильный миф о Юникод.

Ладно, давайте скажем у нас есть строка:
hello
которая в Юникод соответствует следующим пяти кодовым точкам:
U+0048 U+0065 U+006C U+006C U+006F.
Всего лишь набор кодовых точек. Только лишь цифры. Мы еще не сказали ничего о том как будем хранить все это в памяти или как представлять это в почтовом сообщении.

Кодировки


Вот где и появляются кодировки.

В ранние дни кодировки Юникод, которая и привела к мифу о двух байтах..., давайте вернемся к практике, сохраним эти числа, каждое два байта. Таким образом «hello» станет:
00 48 00 65 00 6С 00 6С 00 6А
Правильно? Не так быстро! Это можно также записать и по другому.
48 00 65 00 6С 00 6С 00 6А 00

У нас есть два способа записать строку в Юникод учитывая порядок байт. Более того, первые реализации Юникод были способны писать кодовые точки Юникод обоими способами(high-endian и low-endian), в зависимости от особенностей процессора. Так уж сложилось исторически что образовалось два способа для определения кодовой точки в Юникод. Таким образом люди были вынуждены прийти к соглашению о добавлении вначале каждой Юникод строки специального кода 'FE FF'; это так называемая марка порядка байт или BOM и если вы меняете старший и младший байт в вашей строке то FE FF прочитается как FF FE и тот кто читает вашу строку узнает что ему нужно поменять местами байты. Не все придерживаются этого и поэтому вы можете встретить Юникод строку без BOM.(Это не обязательное а рекомендательное правило в стандарте Юникод).

Юникод оказался хорошим решением всех проблем но программисты начали жаловаться. «Посмотрите на все эти нули!» говорили они, поскольку они были американцами и работали с текстами на английском то редко пользовались символами выше U+00FF. Также они были калифорнийскими хиппи и не могли вынести факта что для хранения строк им нужно в два раза больше места чем ранее, к тому же у них уже имелось огромное количество документов в ASCII и DBCS кодировке и они не желали конвертировать его в Юникод. Это была главная идея почему большинство игнорировало Юникод несколько лет и положение с течением времени только ухудшилось.

И вот тут была открыта изумительная идея которая лежит в основе UTF-8. UTF-8 это другая система хранения строк с кодовыми точками Юникод в памяти, для тех самых магических чисел, используя 8 бит. В UTF-8, каждая кодовая точка от 0-127 хранится как один байт. Только символы от 128 и выше хранятся как 2, 3 или вплоть до 6 байт.

Это имело удобный посторонний эффект. Текст на английском выглядел совершенно одинаково как в UTF-8 так и в ASCII, так что американцы ничего и не заметили. Только остальная часть мира должна была приспособится к новой кодировке. Слово hello которое было U+0048 U+0065 U+006C U+006C U+006F, теперь сохранилось как 48 65 6C 6C 6F, и одинаково как если бы его сохранили в ASCII или каком либо из OEM наборов символов. Теперь если вы желаете то можете использовать подчеркнутые буквы, греческие или клингоновские, вы просто должны использовать несколько байт чтобы сохранить кодовую точку буквы, но американцы этого даже не заметят.

На данный момент я описал 3 способа кодировки Юникод. Традиционный способ хранения в 2 байтах называется UCS-2 (потому что использует 2 байта) или UTF-16 (2 байта = 16 бит), и вам еще требуется определить в high или low endian формате хранится строка. И UTF-8, новый популярный формат, который имеет положительные характеристики и удобен даже если по стечению обстоятельств вы работаете только с английским текстом или с программами которые не работают ни с чем другим кроме ASCII.

В действительности есть и другие способы кодировать Юникод. Есть так называемый UTF-7, которые в большинстве совпадает с UTF-8 но гарантирует что высший бит всегда будет ноль, так что если вам потребуется передать Юникод через особый космический почтовый сервер, который считает что 7 бит это достаточно, то вы сможете это сделать. Также есть UCS-4, которая хранит каждую кодовую точку в 4х байтах, что удобно так как всегда хранит в 4х байтах вне зависимости ни от чего.

Теперь когда вы фактически думаете о вещах в терминах теории кодовых точек Юникод, то понимаете как эти кодовые точки могут быть перекодированы в одну из олдскульных кодировок. Например, вы можете закодировать Юникод строку для hello (U+0048 U+0065 U+006C U+006C U+006F) в ASCII или старую OEM Греческую кодировку, или ANSI кодировку иврита, или в любую другую из тысяч кодировок которые были изобретены, с одной тонкостью: некоторые из букв возможно не появятся! Если не существует эквивалента для кодовой точки Юникод которую вы пытаетесь представить, в кодировке в которую вы хотите конвертировать, вы обычно увидите маленький знак вопроса:? или, если вам повезло, то и черный квадратик. Который из них вы уже видели? → �

Существует сотни национальных кодировок которые могут хранить правильно лишь некоторые кодовые точки, и заменяют остальные кодовые точки на знаки вопроса. Вот некоторые популярные кодировки английского текста Windows-1252 (стандарт Windows 9x для западных европейских языков) и ISO-8859-1, или Latin-1 по другому(также полезную для западных европейских языков). Но попробуйте сохранить русские или еврейские буквы в этих кодировках и вы получите вопросительные знаки. UTF 7, 8, 16, и 32 у всех имеется возможность сохранять правильно любые кодовые точки.

Один наиболее важный факт о кодировках

Если вы вдруг забыли все, что я сейчас вам объяснял, пожалуйста запомните один очень важный факт. Нет смысла в строке если вы не знаете её кодировки. Вам не нужно больше совать голову в песок и делать вид что 'простой' текст это ASCII.
Не существует такой вещи как простой текст.
Если у вас есть строка, в памяти или в файле, или в почтовом сообщении, вы должны знать в какой она кодировке иначе вы не сможете однозначно интерпретировать символы и правильно отобразить их на экране.

Почти каждый раз вопросы типа: 'на моей страничке странные значки вместо символов', или 'я не могу прочитать почтовое сообщение там одни знаки вопросов', возникают из-за программиста который не понимает того факта, что строка с данными закодирована в UTF-8 или ASCII или ISO 8859-1 (Latin 1) или Windows 1252 (Western European), и не может быть отображена правильно. Существует более сотни кодировок описывающих кодовые точки символов свыше 128.

Как нам сохранить информацию о кодировке используемой строки? Что ж существует стандартный способ сделать это. Для почтовых сообщений, у вас есть специальная строка в заголовке сообщения.

Content-Type: text/plain; charset=«UTF-8»

Для веб странички, идея была в том что веб сервер вернет похожий http заголовок Content-Type вместе со страницей — но не в самой странице, а в заголовке ответа который посылается до страницы.

Это создает проблемы. Положим у вас есть большой веб сервер на котором огромное количество сайтов и сотни страниц выложенных разными людьми, на разных языках, и все использовали кодировки заданные в их любимом редакторе, например From Page. Веб сервер сам по себе не знает в действительности в какой кодировке записан каждый файл, так что он не может теперь отсылать заголовок Content-Type.

Удобнее если бы вы могли вложить Content-Type html файла прямо в сам файл, используя специальный тег. Это собьет с толку некоторых… как вы можете читать html файл если вы не знаете его кодировку? К счастью, почти каждая кодировка одинаково определяет символы между 32-127(англ.буквы), так что вы можете прочитать значительную часть html файла:

Но тег meta должен быть самым первым в секции поскольку как только веб браузер увидит этот тег то он прекратит парсировать страничку, и заново начнет парсирование странички с нужной кодировкой определенной в теге meta.
Что делают веб браузеры если они не найдут Content-Type, ни в http заголовке ни тег meta? IE сделает кое-что интересное: он попытается догадаться, основываясь на частоте появления байтов в обычном тексте в обычной кодировке для различных языков. Так как разные старые 8 битные кодовые страницы склонны располагать свои национальные символы в различных местах от 128 до 255, и поскольку каждый человеческих язык имеет различные особенности в частоте появления определенных букв, то возможно что ему удастся правильно определить кодировку. Это странно, но тем не менее это часто срабатывает и создатели веб страниц не утруждают себя указанием кодировки в content-type страницы пока в один прекрасный день они не напишут на страничке что то где частота букв не совпадает со среднестатистической и IE решит что это корейский язык и вместо текста посетитель увидит странные символы. Это доказывает полагаю точку зрения что закон Постела «Быть консервативным в том что вы выдаете и либеральным в том что принимаете» если быть честным, не слишком хороший инженерный принцип. Как бы там ни было наш бедный посетитель сайта увидит сайт который написан на болгарском но отрисован браузером как если бы он был на корейском хотя и не имеет ни какоко отношения к корейскому. Тут посетитель будет пользоваться меню View | Encoding и пробовать один за одним варианты кодировок коих там не одни десяток до тех пор пока не найдет правильную. И это при условии что он знает что так можно делать, о чем большинство пользователей не догадывается.

Для последней версии CityDesk, ПО для управления вебсайтом разработанного моей компанией, мы решили все делать в UCS-2(2-байтовой) Юникод, тот что родной для строкового типа в Visual Basic, COM, и Windows NT/2000/XP. В коде C++ мы объявляли строки как wchar_t(“wide char”) вместо char и использовали функцию wcs вместо str(например wcscat и wcslen вместо strcat и strlen). Чтобы создать произвольную UCS-2 строку в C коде вы просто предваряйте строку буквой L вот так L”hello”.

Когда CityDesk публикует вебстраницу, то конвертирует ее в UTF-8, хорошо поддерживаемую большинством браузеров. Вот таким образом все 29 языковых версий Joel on Software перекодированы и я не слышал еще ни одной жалобы на невозможность просмотра ни единой страницы страницы.

Эта статью становится очень длинной и хотя я не могу рассказать обо всех аспектах Юникод, тем не менее если вы дочитали до этого момента, то вы можете вернуться к программированию, используя антибиотики вместо пиявок и заклинаний. На сем позвольте откланяться.

Оригинал статьи можно найти здесь http://www.joelonsoftware.com/articles/Unicode.html.
Теги:
Юникод перевод joel spolsky

Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.