Абсолютный Минимум, который Каждый Разработчик Программного Обеспечения Обязательно Должен Знать о 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, но вы никогда точно не знаете, как это сделать?

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


Я был встревожен, обнаружив, как много разработчиков программного обеспечения в действительности плохо ориентируются В таинственном мире наборов символов, кодировок, 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 являлся символом конца страницы, заставляя принтер выплюнуть текущий лист бумаги и загрузить новый.

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

Поскольку байты имеют восемь битов, многие люди думали, "чёрт возьми, мы можем использовать коды 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 содержит необычный подход к пониманию понятия символ, и вы должны понять подход 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 имеет в начале метку порядка байтов.

Image: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, и так как все человеческие языки имеют разлиные частотные вероятности использования букв, такой подход часто неплохо срабатывает. Это весьма причудливо, но это, кажется, действительно cрабатывает достаточно часто, так что наивные авторы веб-страниц, которые никогда не знали, что они нуждались в указании тэга Content-Type в заголовке их страничек для того, чтобы странички правильно отображались, до того прекрасного дня, когда они напишут что-то, что точно не соответствует типичному частотно-вероятностному распределению букв их родного языка, и Internet Explorer решит, что это корейский язык и покажет ее соответствующим образом, доказывая, как я думаю, что принцип Закона Постэля о том, чтобы "быть консерватором в том, что вы выводите и либералом в том, что вы принимаете" является пожалуй не слишком хорошим техническим принципом. Так или иначе, что остается делать бедному читателю этого вебсайта, который был написан на болгарском языке, но отображается на корейском (и даже не на осмысленном корейском)? Он использует меню View | Encoding и пробует несколько разных кодировок (есть по крайней мере дюжина для восточноевропейских языков), пока картина не станет более ясной. Если, конечно, он знает, как это делать, ведь большинство людей этого не знает.

Для последней версии 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