The Absolute Minimum Every Software Developer Must Know About Unicode

Вдруг кому? Вряд-ли все читали, да и мне например было полезно освежить память и перечитать.
23.09.2015 в 11:11
Пишет iCindy:

The Absolute Minimum Every Software Developer Must Know About Unicode
Давненько я не предавалась благородному переводческому ремеслу, надо бы освежить навыки. Наткнулась тут на днях на отличную статью про Unicode и решила перевести. На самом деле перевод этой статьи уже был выложен на Хабре, но он мне совсем не понравился.
Статья древняя, аж 2003-го года, но некоторые вещи никогда не стареют.
Не знаю, кому она может здесь пригодиться, но пусть лежит. Как говорит моя бабушка, "не пропадать же добру."

оригинал на сайте joelonsoftware.com

by Joel Spolsky
Ever wonder about that mysterious Content-Type tag? You know, the one you're supposed to put in HTML and you never quite know what it should be?

Did you ever get an email from your friends in Bulgaria with the subject line "???? ?????? ??? ????"?

I've been dismayed to discover just how many software developers aren't really completely up to speed on the mysterious world of character sets, encodings, Unicode, all that stuff. A couple of years ago, a beta tester for FogBUGZ was wondering whether it could handle incoming email in Japanese. Japanese? They have email in Japanese? I had no idea. When I looked closely at the commercial ActiveX control we were using to parse MIME email messages, we discovered it was doing exactly the wrong thing with character sets, so we actually had to write heroic code to undo the wrong conversion it had done and redo it correctly. When I looked into another commercial library, it, too, had a completely broken character code implementation. I corresponded with the developer of that package and he sort of thought they "couldn't do anything about it." Like many programmers, he just wished it would all blow over somehow.

But it won't. When I discovered that the popular web development tool PHP has almost complete ignorance of character encoding issues, blithely using 8 bits for characters, making it darn near impossible to develop good international web applications, I thought, enough is enough.

So I have an announcement to make: if you are a programmer working in 2003 and you don't know the basics of characters, character sets, encodings, and Unicode, and I catch you, I'm going to punish you by making you peel onions for 6 months in a submarine. I swear I will.

And one more thing:


In this article I'll fill you in on exactly what every working programmer should know. All that stuff about "plain text = ascii = characters are 8 bits" is not only wrong, it's hopelessly wrong, and if you're still programming that way, you're not much better than a medical doctor who doesn't believe in germs. Please do not write another line of code until you finish reading this article.

Before I get started, I should warn you that if you are one of those rare people who knows about internationalization, you are going to find my entire discussion a little bit oversimplified. I'm really just trying to set a minimum bar here so that everyone can understand what's going on and can write code that has a hope of working with text in any language other than the subset of English that doesn't include words with accents. And I should warn you that character handling is only a tiny portion of what it takes to create software that works internationally, but I can only write about one thing at a time so today it's character sets.

A Historical Perspective

The easiest way to understand this stuff is to go chronologically.

You probably think I'm going to talk about very old character sets like EBCDIC here. Well, I won't. EBCDIC is not relevant to your life. We don't have to go that far back in time.

ASCII tableBack in the semi-olden days, when Unix was being invented and K&R were writing The C Programming Language, everything was very simple. EBCDIC was on its way out. The only characters that mattered were good old unaccented English letters, and we had a code for them called ASCII which was able to represent every character using a number between 32 and 127. Space was 32, the letter "A" was 65, etc. This could conveniently be stored in 7 bits. Most computers in those days were using 8-bit bytes, so not only could you store every possible ASCII character, but you had a whole bit to spare, which, if you were wicked, you could use for your own devious purposes: the dim bulbs at WordStar actually turned on the high bit to indicate the last letter in a word, condemning WordStar to English text only. Codes below 32 were called unprintable and were used for cussing. Just kidding. They were used for control characters, like 7 which made your computer beep and 12 which caused the current page of paper to go flying out of the printer and a new one to be fed in.

And all was good, assuming you were an English speaker.

Because bytes have room for up to eight bits, lots of people got to thinking, "gosh, we can use the codes 128-255 for our own purposes." The trouble was, lots of people had this idea at the same time, and they had their own ideas of what should go where in the space from 128 to 255. The IBM-PC had something that came to be known as the OEM character set which provided some accented characters for European languages and a bunch of line drawing characters… horizontal bars, vertical bars, horizontal bars with little dingle-dangles dangling off the right side, etc., and you could use these line drawing characters to make spiffy boxes and lines on the screen, which you can still see running on the 8088 computer at your dry cleaners'. In fact as soon as people started buying PCs outside of America all kinds of different OEM character sets were dreamed up, which all used the top 128 characters for their own purposes. For example on some PCs the character code 130 would display as é, but on computers sold in Israel it was the Hebrew letter Gimel (ג ), so when Americans would send their résumés to Israel they would arrive as rגsumגs. In many cases, such as Russian, there were lots of different ideas of what to do with the upper-128 characters, so you couldn't even reliably interchange Russian documents.

Eventually this OEM free-for-all got codified in the ANSI standard. In the ANSI standard, everybody agreed on what to do below 128, which was pretty much the same as ASCII, but there were lots of different ways to handle the characters from 128 and on up, depending on where you lived. These different systems were called code pages. So for example in Israel DOS used a code page called 862, while Greek users used 737. They were the same below 128 but different from 128 up, where all the funny letters resided. The national versions of MS-DOS had dozens of these code pages, handling everything from English to Icelandic and they even had a few "multilingual" code pages that could do Esperanto and Galician on the same computer! Wow! But getting, say, Hebrew and Greek on the same computer was a complete impossibility unless you wrote your own custom program that displayed everything using bitmapped graphics, because Hebrew and Greek required different code pages with different interpretations of the high numbers.

Meanwhile, in Asia, even more crazy things were going on to take into account the fact that Asian alphabets have thousands of letters, which were never going to fit into 8 bits. This was usually solved by the messy system called DBCS, the "double byte character set" in which some letters were stored in one byte and others took two. It was easy to move forward in a string, but dang near impossible to move backwards. Programmers were encouraged not to use s++ and s-- to move backwards and forwards, but instead to call functions such as Windows' AnsiNext and AnsiPrev which knew how to deal with the whole mess.

But still, most people just pretended that a byte was a character and a character was 8 bits and as long as you never moved a string from one computer to another, or spoke more than one language, it would sort of always work. But of course, as soon as the Internet happened, it became quite commonplace to move strings from one computer to another, and the whole mess came tumbling down. Luckily, Unicode had been invented.


Unicode was a brave effort to create a single character set that included every reasonable writing system on the planet and some make-believe ones like Klingon, too. Some people are under the misconception that Unicode is simply a 16-bit code where each character takes 16 bits and therefore there are 65,536 possible characters. This is not, actually, correct. It is the single most common myth about Unicode, so if you thought that, don't feel bad.

In fact, Unicode has a different way of thinking about characters, and you have to understand the Unicode way of thinking of things or nothing will make sense.

Until now, we've assumed that a letter maps to some bits which you can store on disk or in memory:

A -> 0100 0001

In Unicode, a letter maps to something called a code point which is still just a theoretical concept. How that code point is represented in memory or on disk is a whole nuther story.

In Unicode, the letter A is a platonic ideal. It's just floating in heaven:


This platonic A is different than B, and different from a, but the same as A and A and A. The idea that A in a Times New Roman font is the same character as the A in a Helvetica font, but different from "a" in lower case, does not seem very controversial, but in some languages just figuring out what a letter is can cause controversy. Is the German letter ß a real letter or just a fancy way of writing ss? If a letter's shape changes at the end of the word, is that a different letter? Hebrew says yes, Arabic says no. Anyway, the smart people at the Unicode consortium have been figuring this out for the last decade or so, accompanied by a great deal of highly political debate, and you don't have to worry about it. They've figured it all out already.

Every platonic letter in every alphabet is assigned a magic number by the Unicode consortium which is written like this: U+0639. This magic number is called a code point. The U+ means "Unicode" and the numbers are hexadecimal. U+0639 is the Arabic letter Ain. The English letter A would be U+0041. You can find them all using the charmap utility on Windows 2000/XP or visiting the Unicode web site.

There is no real limit on the number of letters that Unicode can define and in fact they have gone beyond 65,536 so not every unicode letter can really be squeezed into two bytes, but that was a myth anyway.

OK, so say we have a string:


which, in Unicode, corresponds to these five code points:

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

Just a bunch of code points. Numbers, really. We haven't yet said anything about how to store this in memory or represent it in an email message.


That's where encodings come in.

The earliest idea for Unicode encoding, which led to the myth about the two bytes, was, hey, let's just store those numbers in two bytes each. So Hello becomes

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

Right? Not so fast! Couldn't it also be:

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

Well, technically, yes, I do believe it could, and, in fact, early implementors wanted to be able to store their Unicode code points in high-endian or low-endian mode, whichever their particular CPU was fastest at, and lo, it was evening and it was morning and there were already two ways to store Unicode. So the people were forced to come up with the bizarre convention of storing a FE FF at the beginning of every Unicode string; this is called a Unicode Byte Order Mark and if you are swapping your high and low bytes it will look like a FF FE and the person reading your string will know that they have to swap every other byte. Phew. Not every Unicode string in the wild has a byte order mark at the beginning.

For a while it seemed like that might be good enough, but programmers were complaining. "Look at all those zeros!" they said, since they were Americans and they were looking at English text which rarely used code points above U+00FF. Also they were liberal hippies in California who wanted to conserve (sneer). If they were Texans they wouldn't have minded guzzling twice the number of bytes. But those Californian wimps couldn't bear the idea of doubling the amount of storage it took for strings, and anyway, there were already all these doggone documents out there using various ANSI and DBCS character sets and who's going to convert them all? Moi? For this reason alone most people decided to ignore Unicode for several years and in the meantime things got worse.

Thus was invented the brilliant concept of UTF-8. UTF-8 was another system for storing your string of Unicode code points, those magic U+ numbers, in memory using 8 bit bytes. In UTF-8, every code point from 0-127 is stored in a single byte. Only code points 128 and above are stored using 2, 3, in fact, up to 6 bytes.

This has the neat side effect that English text looks exactly the same in UTF-8 as it did in ASCII, so Americans don't even notice anything wrong. Only the rest of the world has to jump through hoops. Specifically, Hello, which was U+0048 U+0065 U+006C U+006C U+006F, will be stored as 48 65 6C 6C 6F, which, behold! is the same as it was stored in ASCII, and ANSI, and every OEM character set on the planet. Now, if you are so bold as to use accented letters or Greek letters or Klingon letters, you'll have to use several bytes to store a single code point, but the Americans will never notice. (UTF-8 also has the nice property that ignorant old string-processing code that wants to use a single 0 byte as the null-terminator will not truncate strings).

So far I've told you three ways of encoding Unicode. The traditional store-it-in-two-byte methods are called UCS-2 (because it has two bytes) or UTF-16 (because it has 16 bits), and you still have to figure out if it's high-endian UCS-2 or low-endian UCS-2. And there's the popular new UTF-8 standard which has the nice property of also working respectably if you have the happy coincidence of English text and braindead programs that are completely unaware that there is anything other than ASCII.

There are actually a bunch of other ways of encoding Unicode. There's something called UTF-7, which is a lot like UTF-8 but guarantees that the high bit will always be zero, so that if you have to pass Unicode through some kind of draconian police-state email system that thinks 7 bits are quite enough, thank you it can still squeeze through unscathed. There's UCS-4, which stores each code point in 4 bytes, which has the nice property that every single code point can be stored in the same number of bytes, but, golly, even the Texans wouldn't be so bold as to waste that much memory.

And in fact now that you're thinking of things in terms of platonic ideal letters which are represented by Unicode code points, those unicode code points can be encoded in any old-school encoding scheme, too! For example, you could encode the Unicode string for Hello (U+0048 U+0065 U+006C U+006C U+006F) in ASCII, or the old OEM Greek Encoding, or the Hebrew ANSI Encoding, or any of several hundred encodings that have been invented so far, with one catch: some of the letters might not show up! If there's no equivalent for the Unicode code point you're trying to represent in the encoding you're trying to represent it in, you usually get a little question mark: ? or, if you're really good, a box. Which did you get? -> �

There are hundreds of traditional encodings which can only store some code points correctly and change all the other code points into question marks. Some popular encodings of English text are Windows-1252 (the Windows 9x standard for Western European languages) and ISO-8859-1, aka Latin-1 (also useful for any Western European language). But try to store Russian or Hebrew letters in these encodings and you get a bunch of question marks. UTF 7, 8, 16, and 32 all have the nice property of being able to store any code point correctly.

The Single Most Important Fact About Encodings

If you completely forget everything I just explained, please remember one extremely important fact. It does not make sense to have a string without knowing what encoding it uses. You can no longer stick your head in the sand and pretend that "plain" text is ASCII.

There Ain't No Such Thing As Plain Text.

If you have a string, in memory, in a file, or in an email message, you have to know what encoding it is in or you cannot interpret it or display it to users correctly.

Almost every stupid "my website looks like gibberish" or "she can't read my emails when I use accents" problem comes down to one naive programmer who didn't understand the simple fact that if you don't tell me whether a particular string is encoded using UTF-8 or ASCII or ISO 8859-1 (Latin 1) or Windows 1252 (Western European), you simply cannot display it correctly or even figure out where it ends. There are over a hundred encodings and above code point 127, all bets are off.

How do we preserve this information about what encoding a string uses? Well, there are standard ways to do this. For an email message, you are expected to have a string in the header of the form

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

For a web page, the original idea was that the web server would return a similar Content-Type http header along with the web page itself -- not in the HTML itself, but as one of the response headers that are sent before the HTML page.

This causes problems. Suppose you have a big web server with lots of sites and hundreds of pages contributed by lots of people in lots of different languages and all using whatever encoding their copy of Microsoft FrontPage saw fit to generate. The web server itself wouldn't really know what encoding each file was written in, so it couldn't send the Content-Type header.

It would be convenient if you could put the Content-Type of the HTML file right in the HTML file itself, using some kind of special tag. Of course this drove purists crazy… how can you read the HTML file until you know what encoding it's in?! Luckily, almost every encoding in common use does the same thing with characters between 32 and 127, so you can always get this far on the HTML page without starting to use funny letters:

But that meta tag really has to be the very first thing in the section because as soon as the web browser sees this tag it's going to stop parsing the page and start over after reinterpreting the whole page using the encoding you specified.

What do web browsers do if they don't find any Content-Type, either in the http headers or the meta tag? Internet Explorer actually does something quite interesting: it tries to guess, based on the frequency in which various bytes appear in typical text in typical encodings of various languages, what language and encoding was used. Because the various old 8 bit code pages tended to put their national letters in different ranges between 128 and 255, and because every human language has a different characteristic histogram of letter usage, this actually has a chance of working. It's truly weird, but it does seem to work often enough that naïve web-page writers who never knew they needed a Content-Type header look at their page in a web browser and it looks ok, until one day, they write something that doesn't exactly conform to the letter-frequency-distribution of their native language, and Internet Explorer decides it's Korean and displays it thusly, proving, I think, the point that Postel's Law about being "conservative in what you emit and liberal in what you accept" is quite frankly not a good engineering principle. Anyway, what does the poor reader of this website, which was written in Bulgarian but appears to be Korean (and not even cohesive Korean), do? He uses the View | Encoding menu and tries a bunch of different encodings (there are at least a dozen for Eastern European languages) until the picture comes in clearer. If he knew to do that, which most people don't.

For the latest version of CityDesk, the web site management software published by my company, we decided to do everything internally in UCS-2 (two byte) Unicode, which is what Visual Basic, COM, and Windows NT/2000/XP use as their native string type. In C++ code we just declare strings as wchar_t ("wide char" ) instead of char and use the wcs functions instead of the str functions (for example wcscat and wcslen instead of strcat and strlen). To create a literal UCS-2 string in C code you just put an L before it as so: L"Hello".

When CityDesk publishes the web page, it converts it to UTF-8 encoding, which has been well supported by web browsers for many years. That's the way all 29 language versions of Joel on Software are encoded and I have not yet heard a single person who has had any trouble viewing them.

This article is getting rather long, and I can't possibly cover everything there is to know about character encodings and Unicode, but I hope that if you've read this far, you know enough to go back to programming, using antibiotics instead of leeches and spells, a task to which I will leave you now.

Абсолютный минимум того, что каждый разработчик ПО абсолютно, несомненно должен знать о Unicode и кодировках (никаких оправданий!)

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

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

Я был в смятении, узнав, как много разработчиков не в курсе того, что творится в загадочном мире кодировок, символов, Юникода и прочего. Пару лет назад бета-тестировщик FogBUGZ поинтересовался, сможет ли программа работать с емэйлами на японском языке. А то он без понятия. Проверив как следует, мы выяснили, что в программе творилось черти что. Я связался с разработчиком, который с сожалением сообщил, что они не могут ничего с этим поделать. Как и большинство программистов, они надеялись, что как-нибудь да пронесет.

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

Хочу сделать небольшое объявление: если вы считаете себя программистом и не знаете основ кодировки символов и Юникода, то я найду вас. Найду и покараю, заставив шесть месяцев кряду чистить лук в субмарине. Честное слово.

И еще кое-что:

Это совсем не сложно!

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

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

Самый просто способ разобраться в этом материале - обратиться к истории.

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

Вернемся в прошлое не столь отдаленное, когда создавался UNIX, Керниган и Ритчи разрабатывали язык Си, и все было просто. EBCDIC постепенно отходил на второй план. Единственные символы, которые имели значение, были старые добрые английские буквы без каких-либо диакритических знаков, и для них была таблица кодировки ASCII, которая представляла каждую букву цифрой от 32 до 127. Пробел, к примеру, имел код 21, "A" - 65 и так далее. Каждый символ занимал 7 бит памяти. А поскольку большинство компьютеров в те дни уже имели байт, равный 8 битам, то оставался целый бит на то, чтобы всякие негодяи использовали его в своих сомнительных целях: например, идиоты из WordStar запихнули туда указатель на последнюю букву в слове, обрекая таким образом WordStar на работу лишь с англоязычным текстом. Коды до 32 обозначали так называемые непечатные символы, которые использовались для ругательств. Шутка. Они обозначали управляющие символы. Например, тот, что обозначался кодом 7, заставлял ваш компьютер пикнуть, а двенадцатый - отправить страничку в путешествие до принтера, открыв вам новую.

Все прекрасно, при условии, что вы англофон.

Но в байте по-прежнему было восемь бит, и многие люди думали "Божечки, у нас есть коды от 128 до 255, которые мы можем использовать как захотим!". Загвоздка была в том, что очень много людей одновременно подумали об этом, и более того, у каждого был свой вариант того, что должно быть закодировано от 128 до 255.
У компании IBM появилось то, что стало известно как кодировка OEM (прим.переводчика: она же DOS-866), поддерживающая некоторые европейские символы с диакритическими знаками и целый набор символов для рисования… горизонтальные полосы, вертикальные полосы, горизонтальные с маленькой загогулиной справа и т.д., и вы могли использовать эти линии для рисования красивеньких рамочек и линий на экране, которые все еще можно увидеть на компьютерах с процессором Intel 8088, ныне использующихся в автосушилках.
На самом деле, когда ПК стали продаваться за пределами Америки, то была выдумана куча разновидностей кодировки OEM, которая использовала последние 128 символов на все лады. Например, на некоторых ПК символ с кодом 38 отображался как é, но на других компьютерах, проданных в Израиле, он же был буквой Гимель ג. Так что когда американцы отправляли свои résumés в Израиль, работодатели получили rגsumגs. У русских, например, было такое количество идей того, что сотворить со 128 свободными символами, что они не могли адекватно обмениваться даже русскоязычными документами.

В конце концов, эта OEM-для-всех была подведена под стандарт ANSI. Здесь все были согласны с тем, что должны собой представлять первые 128 символов, а значение остальных зависело от того, где вы живете. Такая система называлась системой кодовых страниц. Таким образом, в израильских DOS использовалась страница 862, в то время как греки пользовались 737-й. MS-DOS для определенной страны могла иметь дюжины кодовых страниц, где до 128 все было одинаково, а после - все остальное. Таким образом на одном компьютере мог быть и английский язык, и Эсператно. Вау! Но, к слову, для отображения еврейских или греческих символов необходимо было написать специальную программу для их отображения.

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

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


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

На самом деле, Юникод представляет символы совсем по-иному, и вам необходимо понять этот принцип, иначе все остальное просто не имеет смысла.

До сих пор мы были уверены, что символ состоит из нескольких битов, хранящихся на диске или в памяти:

A -> 0100 0001

В Юникоде символ состоит из так называемых кодовых точек, которые сами по себе понятие чисто теоретическое. А то, как эта точка представлена в памяти или на диске, совсем другая история.

В Юникоде символ A - это отвлеченное понятие. Это нечто, витающее в воздухе:


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

За каждой номинальной буквой в алфавите закреплено волшебное число, которое консорциум Юникод напутствовал записывать следующим образом: U+0639. Это чудесное число и зовется кодовой точкой. U+ означает "Unicode", за чем следует число в шестнадцатеричной системе счисления. U+0639 - это арабская буква Айн. Английская A в Юникоде - это U+0041. Абсолютно все коды можно найти на сайте www.unicode.org.

Ограничений по количеству символов в Юникоде нет, их количество давно превысило 65 536, так что далеко не каждый символ Юникод можно запихнуть в два байта, а такое заблуждение тоже имело место быть.

Итак, у нас есть строка:


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

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

Просто ряд кодовых точек. Чисел, на самом деле. Мы еще не упомянули о том, как они хранятся в памяти и каким образом представляются в емэйлах.


А вот откуда берутся кодировки.

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

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

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

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

В принципе, да. Я верю, что могло быть и так. На самом деле, ранее разработчики хотели иметь возможность хранить юникодовые точки в порядке от младшего байта к старшему или наоборот в зависимости от того, как будет удобнее процессору. И вечер был и утро было, и было два способа хранения Юникода. Так что людям пришлось смириться с необходимостью хранения кодового символа U+FEFF, который используется для индикации порядка байтов в строе Юникод, так называемого маркера последовательности байтов. И если вы меняете местами местами старший и младший байт, то маркер последовательности будет иметь вид U+FFFE, и человек, читающий строку, должен это учитывать. Казалось бы, разобрались! Но в реальной жизни отнюдь не каждая строка имеет этот маркер.

До поры до времени все шло хорошо, но программисты роптали. "Ну вы гляньте на все эти нули!", говорили они, будучи американцами, которые, в отличие от англичан, редко использовали коды старше U+00FF. Эти либеральные хиппи не хотели впустую расходовать ресурсы (хех). Будь они техасцами, то не были бы против слопать побольше байтов. Однако для калифорнийских хлюпиков удвоенное количество байтов, занимаемых строкой, оказалось неподъемным, но все эти дурацкие документы со всякими символами из ANSI и DBCS никуда не делись, а конвертировать их кто будет? А чего сразу мы-то? Только лишь поэтому многие отказались от использования Юникода. А потом все стало плохо.

И тогда появилась блестящая концепция UTF-8. Это была еще одна система хранения кодовых точек Юникода с волшебной U+ и числами после нее, которые занимали 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, ANSI или в любой из кодировок OEM. Так что если вы беретесь использовать символы с диакритическими знаками, греческие символы или, в конце концов, написать что-то на клингонском, то готовьте для хранения одной-единственной кодовой точки несколько байт. Счастливые американцы этого и не заметят. (Еще у UTF-8 есть хорошее свойство, благодаря которому старый код, обрабатывающий строки с нулевым байтом на конце, эти строки не обрубает).

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

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

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

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

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

Если вы успели забыть все, что я только что объяснял, то, пожалуйста, запомните один чрезвычайно важный факт. Бессмысленно работать со строкой, не зная ее кодировки. Вам не уйти от этого.

Нет такой вещи, как простой текст!

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

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

И как нам сохранить информацию о том, какая строка в какой кодировке? Есть ряд стандартных способов сделать это. В емэйлах предполагается наличие заголовка

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

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

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

Было бы круто, если бы можно было расположить Content-Type в HTML коде страницы с помощью специального тега. Хотя, казалось бы, идейка-то с запашком… как нам прочитать HTML-код до того, как мы узнаем, в какой он кодировке? К счастью, почти всякая кодировка использует общие символы от 32 до 127, так что в HTML не обязательно начинать каждую страницу словами:

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

А что же делать веб-браузерам, если он не нашли Content-Type ни в http заголовке, ни в мета-теге? IE делает кое-что весьма интересное: он пытается догадаться, рассчитывая частоту байтов, где появляются символы, свойственный определенной кодировке того или иного языка. Благодаря страницам, содержащим старый восьмибитный код, где между 128 и 255 размещались разные символы, имеющие в разных языках разную частоту употребления, это работало. И наивным создателям веб-страниц, в жизни нt задумывающимся о том, как им пригодился бы заголовок Content-Type, казалось, что этого достаточно. Они смотрели на страницу и думали "Ну все же ок!", пока однажды они не писали нечто, не поддающееся частотному анализу их родного языка, Internet Explorer не решал, что это, к примеру, корейский, ну и отображал все это соответствующим образом. Мне кажется, правило Джона Постела: "Будь либерален к тому, что принимаешь, и требователен к тому, что отсылаешь", откровенно говоря, не лучшее кредо для инженеров. Так или иначе, что же в этом случае делать бедному посетителю веб-сайта, который с утра писал на своем, на болгарском, а его вдруг заставляют читать по-корейски? А вот что: Вид -> Кодировка, и тыкаетесь по меню, пока картинка не станет удобочитаемой. Если, конечно, сообразит. Большинство пользователей обречены.

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

Когда CityDesk публикует страницу, то конвертирует ее в UTF-8, которую уже много лет очень любят браузеры. Пока я не слышал, чтобы у кого-то были с ней проблемы.

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

URL записи


Teach Coding