Абсолютный Минимум, который Каждый Разработчик Программного Обеспечения Обязательно Должен Знать о Unicode и Наборах Символов

From The Joel on Software Translation Project

Jump to: navigation, search

Автор: Джоэл Сполски
Переводчик: Илья Болодурин
В английском оригинале статья называется The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) и была написана в среду, 8 октября 2003 г.

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

Вы когда-нибудь получали электронную почту от ваших друзей из Болгарии с темой "?????? ?????? ??? ????"?


Ibm.jpg

Я был встревожен, обнаружив, как много разработчиков программного обеспечения в действительности плохо ориентируются В таинственном мире наборов символов, кодировок, Unicode, и подобного материала. Несколько лет назад один из бета-тестеров задавал вопрос, может ли FogBUGZ работать с поступающей электронной почтой на японском языке. Японский язык? Может ли он работать с электронной почтой на японском языке? Я не имел об этом ни малейшего представления. Пристально изучив коммерческий управляющий элемент ActiveX, который мы использовали для разбора сообщений электронной почты MIME, мы обнаружили, что он совершенно точно неправильно работает с наборами символов, и из-за этого мы должны были срочно написать код, который бы отменял сделанное неправильное преобразование и затем заново делал правильное. Я изучил также и другую коммерческую библиотеку, и она также полностью ломала преобразование кодов символов. Я написал разработчику этого пакета, и он ответил чем-то типа "мы ничего не можем с этим сделать". Как и многие программисты, он только хотел, чтобы все это так или иначе обошло его стороной.

Но этого не будет. Когда я обнаружил, что популярный инструмент разработки - PHP - почти полностью игнорирует встречающиеся преобразования символов, беспечно используя для символов 8 битов, что, чёрт возьми, делает невозможным разработку хороших многоязычных web-приложений, я подумал, что это уже слишком.

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

И ещё одна вещь:

ЭТО НЕ ТАК ТРУДНО.

В этой статье я дам вам абсолютно всё, что должен знать каждый работающий программист. Весь тот материал, который говорит о том, что "простой текст = ascii = символы из 8 бит", не только неправилен, он безнадежно неправилен, и если вы всё ещё программируете по этим принципам, вы не намного лучше, чем доктор, который не верит в микробов. Пожалуйста, не пишите ни одной строки кода, пока вы не закончите читать эту статью.

Прежде, чем начать, я должен предупредить вас, что если вы один из тех редких людей, которые знают об интернационализации, вы скорее всего найдете всё мое обсуждение немного упрощённым. Я в действительности пытаюсь установить здесь минимальный уровень, так, чтобы каждый мог понять то, как это работает, и мог писать код, который имеет шансы правильно работать с текстом на любом языке, а не только на подмножестве латиницы, не включающем диакритические знаки (прим. пер.: значки типа ' над Á или дужки над Й). И я должен предупредить вас, что обработка символов является только крошечной частью того, что потребуется для создания программного обеспечения, которое может работать на всех языках, но я могу писать только об одной вещи одновременно, и сегодня я напишу про наборы символов.

Исторический Обзор

Самый легкий способ понять этот материал состоит в том, чтобы пойти хронологически.

Вы вероятно думаете, что я собираюсь говорить здесь об очень старых наборах символов, таких как EBCDIC. Хорошо, не буду. EBCDIC не существенен в вашей жизни. Мы не должны идти назад во времени так далеко.

Таблица ASCII

Не в столь уж и древние времена, когда Unix ещё только изобретался и K&R писали Язык программирования C, всё было очень просто. Повсеместно был распространен EBCDIC (произносится эб-си-дик). Единственными символами, которые имели значение, были старые добрые английские буквы без диакритических знаков, и у нас был для них код, названный ASCII, который был в состоянии представить все эти символы, используя числа от 32 и до 127. Пробелу соответствовало число 32, букве "A" -- 65 и т.д. Это позволило использовать 7 бит для хранения символов. Большинство компьютеров в те дни использовало 8-битовые регистры, и таким образом вы не только могли хранить любой возможный символ ASCII, но ещё и имели целый бит экономии, который, если у вас была такая блажь, можно было использовать в собственных левых целях: программисты с кашей вместо мозгов из WordStar фактически использовали старший бит, чтобы указать последнюю букву в слове, приговорив тем самым WordStar работать только с английским текстом. Коды меньше 32 назывались непечатными и использовались для сквернословия. Шутка. Они использовались для управляющих символов, например, символ 7 заставлял ваш компьютер пискнуть, а символ 12 являлся символом конца страницы, заставляя принтер выплюнуть текущий лист бумаги и загрузить новый.

И все было прекрасно, правда, только если вы говорили на английском.

Oem.png

Поскольку байты имеют восемь битов, то многие люди думали: "Чёрт возьми, мы же можем использовать коды 128-255 в наших собственных целях!". Неприятность была в том, что в головы очень многих людей эта идея пришла почти одновременно, но у всех были свои собственные идеи относительно того, что должно размещаться на месте с кодами от 128 до 255. У IBM-PC было нечто, что стало известным как набор символов OEM, в котором было немного диакритических символов для европейских языков и набор символов для рисования линий... горизонтальные полоски, вертикальные полоски, уголки, крестики, и т.д., и вы могли использовать эти символы для того, чтобы делать элегантные кнопки и рисовать линии на экране, которые всё ещё можно увидеть на компьютерах с процессорами 8088, установленных в химчистках. Фактически, как только люди начали покупать PC вне Америки, было выдумано множество различных наборов символов OEM, и каждый из них использовал верхние 128 символов в своих собственных целях. Например, на некоторых PC символ с кодом 130 показывался как é, но на компьютерах, проданных в Израиле, это была еврейская буква Gimel (ג), и если бы американцы посылали свои résumé (резюме) в Израиль, они прибывали бы как rגsumג. Во многих случаях, как, например, в случае русского языка, было много различных идей относительно того, что делать с верхними 128 символами, и поэтому вы не могли даже надежно обмениваться русскоязычными документами.

В конечном счёте всё это разнообразие кодировок OEM было сведено в стандарт ANSI. Стандарт ANSI оговаривал, какие символы располагались ниже 128, эта область в основном оставалась той же, что и в ASCII, но было много различных способов обращаться с символами от 128 и выше в зависимости от того, где вы жили. Эти различные системы назвали кодовыми страницами. Так, например, в Израиле DOS использовал кодовую страницу с номером 862, в то время как греческие пользователи использовали страницу с номером 737. Они были одними и теми же ниже 128, но отличались выше 128, где и находились все эти забавные символы. Национальные версии MS-DOS поддерживали множество этих кодовых страниц, работая со всеми языками, начиная с английского и заканчивая исландским, и было даже несколько "многоязычных" кодовых страниц, которые позволяли использовать Эсперанто и Галисийский (прим. пер.:галисийский язык относится к романской группе языков, распространён в Испании, носителей 4 млн. чел) на одном и том же компьютере! Ничего себе! Но получить, скажем, иврит и греческий на одном и том же компьютере было абсолютно невозможно, если только вы не написали собственную программу, которая показывала всё, используя графику с побитовым отображением, потому что для еврейского и греческого требовались различные кодовые страницы с различными интерпретациями старших чисел.

Тем временем в Азии продолжали твориться ещё более безумные вещи, принимая во внимание тот факт, что азиатские алфавиты имеют тысячи букв, которые никогда бы не смогли уместиться в 8 битов. Эта проблема обычно решалась запутанной системой под названием DBCS, "двухбайтовый набор символов" (double byte character set), в котором некоторые символы сохранялись в одном байте, а другие занимали два. Было очень легко передвигаться по строке вперёд, но абсолютно невозможно передвигаться назад. Программисты не могли использовать для перемещения вперед и назад s++ и s--, а вместо этого должны были вызывать специальные функции, типа функций Windows AnsiNext и AnsiPrev, которые знали, как иметь дело с этим беспорядком.

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

Unicode

Unicode был храброй попыткой создать единственный набор символов, который включал бы все реальные системы письма, существующие на планете, а также заодно и некоторые выдуманные, такие как Klingon. Некоторые люди имеют неправильное представление, что Unicode -- это обычный 16-битовый код, где каждый символ занимает 16 битов и поэтому есть 65,536 возможных символов. На самом деле это не верно. Это самый распространенный миф о Unicode, и если вы думали точно так же - что ж, вы не одиноки.

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

До сих пор мы предполагали, что символы отображаются на набор каких-то битов, которые вы можете хранить на диске или в памяти:

A -> 0100 0001

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

В Unicode буква A -- это всего лишь платонова идея (прим. пер.: понятие философии Платона, идеи - это идеальные сущности, лишённые телесности и являющиеся подлинно объективной реальностью, находящиеся вне конкретных вещей и явлений). Это нечто, плавающее в небесах:

A


Эта платонова A отличается от B, и отличается от a, но это та же самая A, что и A и A. Идея, что А в шрифте Times New Roman является тем же самым, что и А в шрифте Helvetica, но отличается от строчной "a", не кажется слишком спорной, но в некоторых языках само понятие буква противоречиво. Немецкая буква ß -- это настоящая буква или всего лишь причудливый способ написать ss? Если написание буквы, стоящей в конце слова, изменяется, она становится другой буквой? Иврит говорит да, арабский говорит нет. Так или иначе, умные люди в консорциуме Unicode уже обсудили это в прошлом десятилетии в многочисленных очень политических дебатах, и вы не должны волноваться об этом. Они уже разобрались со всем этим.

Каждой платоновой букве в каждом алфавите консорциумом Unicode было назначено волшебное число, которое записывается так, как это: U+0645. Это волшебное число называют кодовой точкой. U+ означает "Unicode", а числа являются шестнадцатеричными. Число U+FEC9 является арабской буквой Аин (Ain). Английская буква A соответствует U+0041. Вы можете найти все буквы, воспользовавшись утилитой Таблица символов (charmap) в Windows 2000/XP или посетив вебсайт Unicode.

В действительности нет никакого предела для количества букв, которые могут определяться через Unicode, и на самом деле они уже перешагнули за пределы 65,536, так что не каждая буква из Unicode может действительно сжиматься в два байта, но даже без этого факта это всё равно является мифом.

Итак, представим, что мы имеем строку:

Hello

которая соответствует следующим пяти кодовым точкам в Unicode:

U+0048 U+0065 U+006C U+006C U+006F.

Всего лишь группа кодовых точек. Числа в действительности. Но пока мы ничего не сказали о том, как их хранить в памяти или как поместить в сообщение электронной почты.

Кодирование

Вот откуда взялись кодировки.

Самой первой идеей для кодирования Unicode, которая приводила к мифу о двух байтах, была: "Эй, давайте хранить эти числа в двух байтах на каждое!". Таким образом, Hello превратится в

00 48 00 65 00 6C 00 6C 00 6F

Правильно? Не так быстро! Не может ли это быть также:

48 00 65 00 6C 00 6C 00 6F 00 ?

Ну, технически -- да, я действительно полагаю, что и это могло быть, и, фактически, ранние реализации хотели хранить кодовые точки Unicode в формате с первым старшим байтом и первым младшим байтом (high-endian or low-endian) в зависимости от того, с каким форматом именно их процессор работал быстрее... И был вечер, и было утро, и было уже два способа хранить Unicode. Поэтому люди были вынуждены придумать причудливое соглашение о хранении FF FE в начале каждой строки Unicode; это называют Меткой Порядка Байтов Unicode, и если вы поменяете местами ваши старший и младший байты, то в начале будет стоять что-то типа FF FE, и человек, читающий вашу строку, будет знать, что он должен поменять байты в каждой паре местами. Уф. В жизни отнюдь не каждая строка Unicode имеет в начале метку порядка байтов.

Hummers.jpg

Некоторое время казалось, что все довольны, но программисты начали жаловаться: "Посмотрите на все эти ноли!" - говорили они, так как они были американцами, рассматривали английский текст, и редко использовали кодовые точки выше U+00FF. Ещё они в основном были либеральными хиппи из Калифорнии, которые хотели экономии (смешно) . Если бы они были техасцами, они бы гордились тем, что теперь символы стали в два раза больше. Но эти калифорнийские мещане не смогли перенести идею увеличить вдвое место, необходимое для строк, и в любом случае уже была чёртова пропасть документов, которые использовали различные виды наборов символов ANSI и DBCS, и кто же собирается их преобразовывать? Мы? По одной этой причине большинство людей решило игнорировать Unicode в течение нескольких лет, и за это время всё ухудшилось.

Поэтому была изобретена блестящая концепция UTF-8. UTF-8 был другой системой хранения вашей последовательности кодовых точек Unicode, тех самых волшебных U+ чисел, используя те же 8 битов в памяти. В UTF-8 каждая кодовая точка с номерами от 0 до 127 сохранялись в единственном байте. Как только номер кодовой точки станет равна 128 и выше, для хранения используется 2, 3, и, фактически, до 6 байтов.

Как работает UTF-8

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

Я уже описал вам три способа кодирования Unicode. Традиционные методы "хранить это в двух байтах" называются UCS-2 (потому что они используют два байта) или UTF-16 (потому что они используют 16 битов), и вы ещё должны выяснять, является ли это код UCS-2 со старшим байтом в начале или со старшим байтом в конце. И есть популярный новый стандарт UTF-8, строки на котором имеют приятную особенность также работать и в старых программах, работающих с английским текстом, и в новых умных программах, которые прекрасно знают о том, что есть и другие наборы символов кроме ASCII.

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

И теперь, когда вы способны думать в терминах платоновых идей символов, представленых кодовыми точками Unicode, эти кодовые точки Unicode могут быть закодированы любой кодовой схемой старой школы!

Например, вы можете закодировать Unicode-строку Hello (U+0048 U+0065 U+006C U+006C U+006F) в кодировке ASCII, или в старой греческой кодировке OEM, или в еврейской кодировке ANSI, или в любой из нескольких сотен кодировок, которые были изобретены до наших дней, с одной загвоздкой: некоторые из символов могут не отображаться! Если нет никакого эквивалента кодовой точки Unicode в кодовой таблице, которую вы пытаетесь использовать для преобразования, то обычно получается небольшой вопросительный знак: ? или, если вы действительно хороший программист, квадратик. Что у вас получилось? -> �

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

Самый Важный Факт О Кодировках

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

Нет Такой Вещи Как Простой Текст.

Если у вас есть строка -- в памяти, в файле, или в сообщении электронной почты -- вы должны знать, в какой она кодировке, иначе вы не сможете правильно её интерпретировать или показать пользователю.

Почти все глупые проблемы типа "мой вебсайт похож на тарабарщину" или "она не может читать мои электронные письма, если я использую символы с ударениями" сводятся к одному наивному программисту, который не понимает того простого факта, что если явно не говорится, находится ли определённая строка в кодировке UTF-8, или ASCII, или ISO 8859-1 (Латинский-1), или Windows 1252 (Западноевропейский), то просто невозможно показать её правильно или даже выяснить, где она заканчивается. Есть более ста кодировок символов выше кодовой точки 127 -- и нет никакой информации для того, чтобы выяснить, какую из них использовать.

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

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

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

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

Было бы очень удобно, если бы вы могли поместить Content-Type для файла HTML непосредственно в сам HTML-файл, используя некоторый специальный тег (tag). Конечно, это сводило бы пуристов с ума... как вы можете читать файл HTML, пока Вы не узнаете, какую кодировку он использует?! К счастью, почти все кодировки используют одну и ту же таблицу символов с кодами от 32 до 127 и поэтому вы всегда можете пробраться достаточно далеко по странице HTML, пока не понадобится использовать эти забавные слова:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

Но этот тег Meta действительно должен быть в самой первой строке в секции <head>, потому что как только веб-браузер увидит этот тег, он перестанет разбирать страницу и начнёт все заново, используя ту кодировку, которую вы задали.

Что происходит, если веб-браузер не находит никакого Content-Type ни в заголовке http, ни в теге Meta? Internet Explorer фактически делает кое-что весьма интересное: он пробует определить, основываясь на частоте, с которой различные байты появляются в типичном тексте в типичных кодировках различных языков, какой язык и кодировка используются. Так как разные старые 8-битовые кодовые страницы по-разному размещали национальные символы в диапазоне между 128 и 255, и так как все человеческие языки имеют различные частотные вероятности использования букв, то такой подход часто неплохо срабатывает. Это довольно странно, но это действительно срабатывает достаточно часто, так что для наивных авторы веб-страниц, которые никогда не знали о необходимости в указании тэга Content-Type, всё выглядит хорошо -- до того прекрасного дня, когда они напишут что-то, что точно не соответствует типичному частотно-вероятностному распределению букв их родного языка, и Internet Explorer решит, что это корейский язык и покажет её соответствующим образом, доказывая, по моему мнению, что принцип Закона Постэля -- "быть консерватором в том, что вы выводите и либералом в том, что вы принимаете" -- является, пожалуй, не слишком хорошим техническим принципом. Так или иначе, что остается делать бедному читателю этого веб-сайта, который был написан на болгарском языке, но отображается на корейском (и даже не на осмысленном корейском)? Он использует меню View | Encoding и пробует несколько разных кодировок (есть по крайней мере дюжина для восточноевропейских языков), пока картина не станет более ясной. Если, конечно, он знает, как это делать, ведь большинство людей этого не знает.

Rose.jpg

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

Когда CityDesk формирует веб-страницу, он конвертирует все строки в кодировку UTF-8, которая уже в течении многих лет прекрасно поддерживается веб-браузерами. Именно эта кодировка используется во всех 29 языковых версиях Джоэла о Программном обеспечении (сейчас версии более тридцати (и это статья - часть одной из них) и они располагаются здесь), и я ещё не слышал ни от одного человека, что он имел хоть какие-нибудь проблемы с их правильным отображением.

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

Personal tools