Pull to refresh

Comments 52

BTW, "Windows uses UTF-16 internally"


Можно дополнить к Java, ибо распространенная платформа. Речь о ядре Win32/Win64. про Win16 не помню уже.

Всё верно. Обязательно добавлю.

Windows, к примеру, позволяет создавать файлы, которые могут содержать не корректную UTF-16 последовательность.
Из-за этого, в частности, родилось надмножество над UTF-8 для кодирования таких последовательностей с замечательным названием WTF-8: https://simonsapin.github.io/wtf-8/

Интересно, не знал раньше об этом.
Есть ещё такая полезная вещь в юникоде — это группы. Они классифицируют символы по значению. Проверив вхождение в группу можно узнать, является ли символ буквой какого-нибудь алфавита или знаком препинания, например. Бывают случаи, когда символы из разных групп выглядят похоже — но при этом принадлежат разным группам, а значит между ними есть большая семантическая разница. Иногда это разница существенна — например, когда мы хотим распарсить текст программы, для чего нужно разделять символы и операторы между ними.

Важная особенность UTF-8 и UTF-16LE: при побайтовом сравнении Unicode-строки не меняют свой порядок.

Если честно то не распарсил:) Порядок чего? И что вообще может меняться при сравнении, которое как известно не модифицирующая операция?

При использовании UTF-8 и UTF-16BE (здесь, кажется, Bozaro немного ошибся) последовательности code unit'ов, если их представить в виде чисел, возрастают согласно unicode scalar value'ам, которые они представляют. В UTF-16LE же это не так.


К примеру, возьмём символ U+10FF. В UTF-16BE он будет представлен как число 10FF, или как два байта со значениями 16 и 255. В UTF-16LE он будет представлен как FF10, или два байта со значениями 255 и 16. А теперь возьмём символ U+1100, следующий по номеру за U+10FF. В UTF-16BE он будет представлен как 1100, или 17 и 0. А в UTF-16LE он будет представлен как 0011, или 0 и 17. Получается такое:


       | BE     | LE     |
U+10FF | 16 255 | 255 16 |
U+1100 | 17 0   | 0   17 |

Если сравнивать эти последовательности побайтно, то получается, что в UTF16-LE символ с кодом U+1100 будет "меньше" символа с кодом U+10FF, что противоречит порядку возрастания номеров символов в юникоде. Из-за этого наивная побайтовая, и даже пословная (по 2 байтам) сортировка будет давать весьма странные результаты при использовании UTF-16LE. Представления символов в UTF-8 я писать выше не стал, но там ситуация аналогична UTF-16BE — десятичные представления code point'ов возрастают согласно таблице юникода.

Да, я постоянно путаю LE и BE :(
Порядок сортировки сохраняется для UTF-8 и UTF-16BE.

И опять неверно! Для UTF-8 и UTF-32BE. Для UTF-16BE — нет.
т.е. имеется ввиду, что такая кодировка сохраняет «отношение упорядоченности»?

т.е. для любых A и B таких что A < B

выполняется F(A) < F(B)
Как бы, да.

Если Unicode-строки (т.е. последовательности code point'ов — номеров символов, например: U+434, U+31, U+10024 (>2байт), U+11003 (>2байт), U+2019) A и B таковы, что лексигографически A<B, то…

… и их UTF-8- и U̶T̶F̶-̶1̶6̶B̶E̶UTF-32BE-представления (в виде последовательностей байт, например: 0xD0, 0xB4, 0x31, 0xF0, 0x90, 0x80, 0xA4, 0xF0, 0x91, 0x80, 0x83, 0xE2, 0x80, 0x99) f(A) и f(B) таковы, что лексигографически f(A)<f(B).

Однако:
1. Во-первых, это не до конца верно для UTF-16BE. Символы U+E000..U+FFFF очевидно имеют бо́льшие номера, чем коды, используемые в UTF-16 для суррогатных пар — таким образом, U+FB20<U+10024, но (0xFB, 0x20)>(0xD8, 0x00, 0xDC). Естественно, это совершенно неверно и для любых UTF-*LE-представлений (UTF-16LE, UTF-32LE).
2. Во-вторых, при сравнении строк операцией меньше-больше правильно использовать collations. Collation зависит от локали; одна и та же пара букв у разных народов может считаться упорядоченной по-разному. Если же мы упорядочиваем с техническими целями (например, для помещения в бинарное дерево), то нам абсолютно не важно, соответствует ли упорядочивание цепочек символов упорядочиванию цепочек байт.
А U+E000..U+FFFF — это не только Private Use Area.

Строго говоря, в Java и в винде используется UCS-2, а не UTF-16. В частности, в Java char — это 16-битное число, которого, очевидно, недостаточно для представления всех символов юникода. В UTF-16, чтобы обойти эту проблему с недостаточным размером code unit'а, вводятся так называемые суррогатные пары, к которым в общем случае в Java/WinAPI доступ предоставляется раздельно. Ну то есть, нет ограничителей, которые не позволяли бы работать с отдельными code unit'ами. Из-за этого, если писать программы неаккуратно, можно получить проблемы с символами вне BMP.


Вот ещё очень хороший и правильный сайт, который объясняет почему UTF-8 это лучшее из представлений юникода: http://utf8everywhere.org/

Подождите-подождите. В Java полный UTF-16, 1 code point = 1 или 2 char. Для получения честных code point в String есть нужные методы. То есть Java и не пытается говорить, что char – это представление любого символа, это ваша личная придумка.

То есть Java и не пытается говорить, что char – это представление любого символа, это ваша личная придумка.

Я бы не был так категоричен. Ява появилась за несколько лет до появления суррогатных пар и на первых порах использовала UCS-2. В то время char действительно позволял представить любой существующий символ.

Потом появилась необходимость кодировать символы вне МЯП/BMP и, начиная с версии 1.5, Ява стала поддерживать UTF-16 и суррогатные пары. Тем не менее, в силу исторических причин, мы имеем возможность оперировать с внутренностями UTF-16 и создавать некорректные с её точки зрения последовательности кодовых квантов. Важно знать об этом и помнить; думаю Googolplex именно на это хотел обратить внимание всех читателей.
Собственно эти «исторические прчиины» — это и есть единственный смысл существования UTF-16. Если вы задаёте себе вопрос: «а какую кодировку использовать», то ответ однозначен — UTF-8. В редких случаях — UCS-4. Использовать же UTF-16 нужно только и исключительно тогда, когда у вас нет выбора.

Ну ответ все-таки не абсолютно однозначен. У UTF-8 есть недостаток: доступ по индексу за O(N). Иногда это бывает важно, и на помощь приходит UTF-32.

Технологическому миру ещё предстоит пройти через переосмысление подхода к строкам и Юникоду, возможно даже не один раз.

Сейчас в мире Java существует негласный статус-кво, что большинство разработчиков существуют в рамках БМЯП/BMP и скорее всего если и слышали о существовании суррогатных пар, то не вдавались в подробности. И, по большому счёту, огромная масса кода, использующая стандартные методы для работы со строками в Java — корявая и дырявая. За примером далеко ходить не надо — буквально недавно использование Emoji для телеграм-бота разломало парсер в JetBrains PhpStorm и я в настоящий момент жду фикса.

В мире Java есть робкие попытки сдвинуть всё с мёртвой точки, например — JEP 254: Compact Strings, но это по-прежнему очень осторожное подлечивание определённых симптомов проблемы.

В мире Python, начиная с версии 3.3 и реализации PEP 393: Flexible String Representation, дела обстоят намного лучше. Например, такой код:

emojiStr = u"" # в кавычках эмоджа, Хабр режет

print (str(len(emojiStr)) + ": " + str(bytes(emojiStr, 'utf-8')))

выдаст в ответ единицу и

1: b'\xf0\x9f\x98\x80'

Так что в каком-то смысле питонщикам везёт больше.

Как обстоят дела в мирах .NET и C/C++ я, к сожалению, ничего не знаю и буду рад, если кто-нибудь поделится информацией в комментариях.
У UTF-8 есть недостаток: доступ по индексу за O(N). Иногда это бывает важно, и на помощь приходит UTF-32.
Я в это, теоретически, готов поверить, а практически — никогда не сталкивался.

Я слышал эту отговорку много раз, но ещё ни разу не сталкивался с тем, чтобы какая-либо задача требовала этого. Ещё раз: ни разу.

Вариантов, когда использование UTF-32 и обращение по индексу за O(1) позволяют красиво и неправильно решить задачу — видел много, да. Работающих вариантов — не видел.

Дело в том, что «один символ» в Unicode абсолютно бессмысленен. К нему могут быть добавлены разные умляуты и цедилы, он может быть развёрнут в другую cторону («a > b», но "א > ב") и т.д. и т.п.

А если вам нужно «просто распарсить разметку» (XML, HTML, etc) — так она обычно из ASCII приходит и побайтовый доступ отлично работает и в UTF-8.

Пример задачи, которая хорошо ложится на UTF-32 и плохо на UTF-8 — был бы хорош. Потому что всё, что я пока что видел сводилось примерно к подходу «это отлично работает для русского и английского, а арабы, евреи и монголы со своими заморочками — пусть идут куда хотят». Ну так в этом случае можно и windows-1251 или koi8-r использовать, проблем ещё меньше будет!
В Rust так и сделано: строки в UTF-8, доступ по итератору. А поэлементный доступ… это что отдавать: графемный кластер? юникод-символ? байт?

Indexing is intended to be a constant-time operation, but UTF-8 encoding does not allow us to do this. Furtheremore, it's not clear what sort of thing the index should return: a byte, a codepoint, or a grapheme cluster. The as_bytes() and chars() methods return iterators over the first two, respectively.

Отсюда: https://doc.rust-lang.org/std/string/struct.String.html#utf-8

Советую почитать сайт, на который я уже давал ссылку выше: http://utf8everywhere.org/. Вопреки распространённому представлению, случаи, когда доступ по индексу (что, вообще говоря, требует отдельного определения — доступ по индексу чего?) важен, на практике исчезающе редки, и, как правило, встречаются в коде, автор которого работает с юникодом неправильно.

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

А зачем там текст а не byte/short/int/long array?

Ну да. Я напишу обёртку и назову её, внезапно, String. Вот и будет ответ — чем плох юникод.
Не понял ответ. Зачем Вам делать обёртку над массивом/вектором/списком? И, тем более, зачем называть её String?
Да что тут думать, мутации в генетическом алгоритме. Взять половину символов.
Какую практическую задачу вы решаете? И почему вы используете строки, а не какие-нибудь trie деревья?

А то так можно договориться до того, что обращение по индексу отлично решает задачу обращения по индексу.
Важно знать об этом и помнить; думаю Googolplex именно на это хотел обратить внимание всех читателей.

Да, всё так и есть. По моему мнению, нельзя говорить что "в таком-то языке строки в UTF-16", если этот язык позволяет легко и походя сконструировать строку, которая валидной UTF-16-последовательностью не является. Даже если этот язык и предоставляет какие-то методы для работы с суррогатными парами и даже если, скажем, методы типа reverse() умеют работать с суррогатными парами.

Проблема здесь в том, что в джаве работа с "символами" предоставляется таким образом, как будто строки это просто массивы char'ов (собственно, внутри так и есть), что с точки зрения UTF-16 некорректно. Пример того, как аналогичная проблема решается правильно — в Rust, где используется UTF-8 (переменной длины), и при этом гарантируется корректность внутреннего представления строки.

В Java и в самом деле есть полная поддержка UTF-16 ровно в том смысле, что при работе со строками учитывается существование символов мне МЯП/BMP и наличие суррогатных пар. Но так было не всегда и до пятой версии Java использовала UCS-2.

image

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

То, что в джаве есть методы, которые позволяют "исследовать" code point'ы UTF-16, не значит, что сами строки в ней представлены в UTF-16. Если бы в Java гарантировалась корректность того, что строка всегда валидная с точки зрения UTF-16, то тогда бы я согласился, но к сожалению это не так. Например, какой-нибудь substring() совершенно замечательно позволит распилить суррогатную пару пополам. А в Rust, например, аналогичная операция над UTF-8 строками невозможна, просто нет соответствующих методов. Если есть нужда в подобных операциях, строку всегда можно сконвертировать в срез байтов и работать с ним.

Вернее, в Rust методы есть (например, слайсинг строк типа s[1..3]), но эти методы будут паниковать, если смещения указывают в середину code point'ов.

В Windows уже с Win2000 используется UTF-16. До этого, действительно, только UCS-2.
Важно ещё следующее: один символ Юникода — не значит визуально один элемент.

Во-первых, есть combining character'ы. Это символы, которые визуально меняют предшествующие символы (например, добавляют какую-то диактрику). Т.е. строка Юникода состоит из т.н. base character'ов («обычные» символы, например «е») и т.н. combining character sequence'ов, каждый из последних (в норме, иначе это invalid combining character sequence) состоит из base character'а и одного-или-более combining character'а (например, «е»+« ̈»+« ̸»=«ё̸»). Не путать это с суррогатными парами (суррогатная пара — это представление ОДНОГО символа несколькими двухбайтными словами в UTF-16).

Во-вторых, даже base character'ы (которые формально не относятся к combining character'ам) могут лепиться несколько штук в один визуальный элемент. Сюда относятся, например, корейские согласные/гласные/завершающие. Например, «ᄒ»+«ᅡ»+«ᇶ»=«하ᇶ» (но «ᄒя» или «яᅡ» или «ᅡᄒ» не слепляются). Это Hangul (корейский алфавит), возможно, существуют и другие письменности, которые лепят символы в один визуальный элемент.

Для визуального элемента существует отдельный термин (grapheme, что ли?).

P.S.: Рекомендую сервис http://qaz.wtf/u/show.cgi, чтобы увидеть из чего состоит строка. Например, вставляете «ё̸», а оно выдаёт.
При этом некоторые визуальные элементы можно представить несколькими способами.

Например, «е»+« ̈»+« ̸»=«ё̸» и «е»+« ̸»+« ̈»=«ё̸».

Кроме того, есть символы, выглядящие аналогично какой-то последовательности символов (например, «ё» можно написать отдельным символом, а можно двумя: «е»+« ̈»=«ё»).

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

И вот тут, если вы пользуетесь и Mac OS X и Linux (или разрабатываете программу, которая должна пересылать файлы с одного на другое), то вы должны знать о существовании композиции и декомпозиции, потому что файл, названный по русски (по крайней мере с буквами Ё и Й в названии) нельзя просто так взять и перенести с одной системы в другую. Его имя нужно перекодировать. Из UTF-8 в UTF-8. Да, из одной и той же кодировки в ту же самую. Только OS X хранит имя файла в декомпозированном виде (UTF-8 NFD), т.е. Й хранится как И и следом за ней комбинирующий символ, а Linux — в композированном (UTF-8 NFC).


Поэтому на OS X нужно ставить свежий rsync из brew (потому что Apple традиционно кладёт в дистрибутив тухлые версии системного софта) и использовать ключик --iconv=UTF-8-MAC,UTF-8 всегда. Такие дела.


См. http://serverfault.com/a/627567/135595 и http://askubuntu.com/q/533690/167201

В linux большинство ФС не требует хранения UTF-8 NFC, а просто хранят последовательность байт без учёта кодировки вообще, за исключением работы с ФС, которым есть до этого какое‐то дело (т.е. в случае с ФС, используемыми в Windows и Mac OS). Нет никаких проблем с тем, чтобы использовать UTF-8 NFD на linux, кроме того, что при наборе «й» на клавиатуре вы получите один символ (и, соответственно, у вас будут проблемы с набором имён файлов).

Абсолютно верное замечание. Я намерено не стал включать такие подробности в статью, т.к. тут есть о чём поговорить на отдельный материал.

Единица, воспринимаемая пользователем как единый символ, называется графемным кластером.
Единственное, чего я не могу понять — зачем нужно было добавлять в юникод Эмодзи.

Как по мне, так это полный бред. Во-первых, нужно отделять мух от котлет, и не засорять пространство юникода де-факто картинками. А во-вторых, непонятно как их отображать — стандарта нет, никаких правил нет. Если для обычного текста существуют хоть какие-то правила, сформулированные в современной калиграфии и типографике (высота строки, выносные элементы, моноширинность итд + правила начертания для каждого отдельного языка), и этим правилам подчиняются создатели шрифтов, то что делать с эмодзи — вообще непонятно.

В результате имеем что имеем — в очередной раз, каждый производитель лепит свой собственный набор эмодзи, стандартизированного набора шрифтов или начертаний нет, в результате вы на андроиде имели ввиду одно, а ваша бабушка с айфоном приняла это за другое. Приехали.
Хотя я тоже не люблю эти эмодзи, но причину их включения в юникод в принципе понимаю. Это универсальный стандарт для их передачи. Раньше лепили кто во что горазд, типа :-). Из-за этого, кстати, возникает сильно бесящая проблема со всякими скайпами, которые преобразуют все похожие последовательности в эти дебильные смайлики, особенно куски кода (скажем, присылают тебе стектрейс, где есть что-то типа «Class::Property», а там все :P заменены на смайлик с высунутым языком. Хочется взять и… сделать что-нибудь нехорошее. К счатью, это отключается). С эмодзи в юникоде такой проблемы нет.
В скайпе можно сделать форматирование кода, добавив "!!! " перед кодом.
На мой вкус, смайлы куда более универсальный стандарт.
:), :-), ^-^, O_O — одинаково распознаются в любой программе и шрифте. В отличие от миллиарда эмодзи, неизвестно присутствующих в очередной программе\шрифте или нет. Вопрос автозамены — вопрос к мессенджерам (у известных мне была опция отключения)
Ой не универсальный.

1. Вам никогда не приходили %), %|, :-*, :-P и др.?

2. Даже если брать стандартный :), что это: насмешка, смущённая улыбка или радость?

3. По факту, автозамена происходит. Отключить — иногда даже хуже, не отключив, ты хоть знаешь, когда собеседнику пришло покорёженное, а так полная угадайка. Особый шик, когда люди пользуются разными клиентами, и тебе приходит что-то типа «*STOP*» (
угадайте что это
Это девушка прощалась. Смайл *STOP* выглядел в её клиенте как поднятая вверх ладонь руки. Она интерпретировала этот жест как помахать на прощание.
).

Я совершенно не фанат эмодзи. Но смайлики — это совсем не универсально и совсем не стандарт. И стандартизировать, по-моему, их бы ой как не мешало.
1. Два крейзи-смайла, крайне редко встречались вне иконизированных мессенджеров. :-* — поцелуй, :-P — высунутый язык. Где сложность?
2. Это улыбка. Трактуется, как и обычная улыбка в контексте. Беседы и собеседника. Может быть всеми тремя и еще пачкой других.
3. Отключить — это если у вас есть регулярное перекидывание исходников, где оно напрягает. Да и то, обычно копипаст из мессенджера в любой блокнот спасает. Не знаю. У меня года полтора было отключено автораспознавание — остальное время как-то справлялся.

Эмодзи… это унылая попытка стандартизировать мимику и жесты. Безумно раздувающая размер шрифтов (мне где-то попадалась жалоба, что если включить шрифт с полным набором этой дряни, то программа автоматически тяжелеет едва ли не на пару десятков мегабайт).
Те же смайлы за все время их существования успешно развивались в рамках того небольшого набора символов который уже существовал. Сначала пришло редуцирование носа :-) => :), потом появились горизонтальные смайлы О_О и т.п. Эмодзи — это путь в никуда. Вы не отрисуете все эмоции, вы не отрисуете все флаги, вы не отрисуете все-что-только-можно придумать. Это по сути попытка засунуть в шрифт все клипарты на все темы — идея заведомо не имеющая смысла.
А что из всего этого входит в «необходимый практический минимум»?

Как отличить, и вообще что делать с «ñ» и «ñ» (это совершенно разные последовательности символов, в них даже количество использованных байт отличается)? Как использовать в регулярках? Что делать, если в каком-нибудь прикладном языке мы получаем ошибку кодирования в utf-8 (вот это — абсолютный чемпион по количеству вопросов на SO)? Как правильно сравнивать строки? Реализация в разных языках? Почему умер UCS2 и чем плох UTF-16? Может быть, хоть что-то практическое?

> Приведённой выше информации вполне достаточно, чтобы не путаться
> в основных принципах и работать с текстом в большинстве повседневных задач

Да, если повседневные задачи у вас сводятся к чтению текста с экрана монитора. На гиктаймс вас что, не пускают?

Верный способ сделать плохую статью — напихать в неё всё и сразу. Это будет много воды для людей, хорошо разбирающихся в теме, и слишком много букв для только начинающих в ней осваиваться. Если пойти от частого к редкому, то больше шансов сделать полезный всем материал, что я и собираюсь сделать в будущих статьях.

Спасибо за подсказку по вопросам, о которых стоит рассказать.
Серьезно? Спасибо? Сиречь, вы начали «цикл статей» вот этой бессмысленной водой, даже не имея представления о том, о чем действительно нужно рассказать?

И ваше «спасибо» блестяще смотрится на фоне того, что вы поленились вывести из «засеренного» мой комментарий, хотя в нем практической информации больше, чем во всех 100500 знаках вашей записи.

Если в мире в 2016 существует программист, который не в курсе, чем UTF-8 отличается от UTF-16, и что не так с однобайтными кодировками, то он и так неплохо зарабатывает на допиливании COBOL-монстров в Кремниевой долине.

Перевод уже есть на Хабре: Что нужно знать каждому разработчику о кодировках и наборах символов для работы с текстом + часть 2

Я, конечно, статью Спольски читал и вдохновился её простой. Но хорошего материала по Unicode на русском языке объективно мало, а поговорить есть о чём. Так что, уверен, моя статья не будет лишней, а цель на будущее — сделать качественный материал по всем основным аспектам Юникода.
Все верно. Просто фраза «необходимый минимум о юникоде» сразу легла.
Спасибо, отличная статья! Я бы порекомендовал Вам сделать акцент на том, что Unicode решает проблемы отображения текста, но не решает проблемы при работе с ним. Например, без знания локали невозможно сделать Uppercase. Буква i может перейти как в I (английский) так и в İ (турецкий).
Там много чего есть. Начнём с того, что кроме uppercase и lowercase есть ещё и titlecase! Но один взгляд на три символа для которых titlecase != uppercase вам всё прояснит:
NJ
Nj
nj
А есть много заморочек где всё не так просто…
Sign up to leave a comment.

Articles