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.
т.е. для любых 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 зависит от локали; одна и та же пара букв у разных народов может считаться упорядоченной по-разному. Если же мы упорядочиваем с техническими целями (например, для помещения в бинарное дерево), то нам абсолютно не важно, соответствует ли упорядочивание цепочек символов упорядочиванию цепочек байт.
Строго говоря, в 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-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 использовать, проблем ещё меньше будет!
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?
Да что тут думать, мутации в генетическом алгоритме. Взять половину символов.Какую практическую задачу вы решаете? И почему вы используете строки, а не какие-нибудь trie деревья?
А то так можно договориться до того, что обращение по индексу отлично решает задачу обращения по индексу.
Важно знать об этом и помнить; думаю Googolplex именно на это хотел обратить внимание всех читателей.
Да, всё так и есть. По моему мнению, нельзя говорить что "в таком-то языке строки в UTF-16", если этот язык позволяет легко и походя сконструировать строку, которая валидной UTF-16-последовательностью не является. Даже если этот язык и предоставляет какие-то методы для работы с суррогатными парами и даже если, скажем, методы типа reverse() умеют работать с суррогатными парами.
Проблема здесь в том, что в джаве работа с "символами" предоставляется таким образом, как будто строки это просто массивы char'ов (собственно, внутри так и есть), что с точки зрения UTF-16 некорректно. Пример того, как аналогичная проблема решается правильно — в Rust, где используется UTF-8 (переменной длины), и при этом гарантируется корректность внутреннего представления строки.
При этом в Java, в силу исторических причин, мы работаем не с юникод-символами, а напрямую с кодовыми квантами UTF-16. Поэтому, как вы верно заметили, при неаккуратной работе со строками можно получить некорректную с точки зрения UTF-16 последовательность таких квантов.
То, что в джаве есть методы, которые позволяют "исследовать" code point'ы UTF-16, не значит, что сами строки в ней представлены в UTF-16. Если бы в Java гарантировалась корректность того, что строка всегда валидная с точки зрения UTF-16, то тогда бы я согласился, но к сожалению это не так. Например, какой-нибудь substring() совершенно замечательно позволит распилить суррогатную пару пополам. А в Rust, например, аналогичная операция над UTF-8 строками невозможна, просто нет соответствующих методов. Если есть нужда в подобных операциях, строку всегда можно сконвертировать в срез байтов и работать с ним.
Во-первых, есть 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, кроме того, что при наборе «й» на клавиатуре вы получите один символ (и, соответственно, у вас будут проблемы с набором имён файлов).
Единица, воспринимаемая пользователем как единый символ, называется графемным кластером.
Как по мне, так это полный бред. Во-первых, нужно отделять мух от котлет, и не засорять пространство юникода де-факто картинками. А во-вторых, непонятно как их отображать — стандарта нет, никаких правил нет. Если для обычного текста существуют хоть какие-то правила, сформулированные в современной калиграфии и типографике (высота строки, выносные элементы, моноширинность итд + правила начертания для каждого отдельного языка), и этим правилам подчиняются создатели шрифтов, то что делать с эмодзи — вообще непонятно.
В результате имеем что имеем — в очередной раз, каждый производитель лепит свой собственный набор эмодзи, стандартизированного набора шрифтов или начертаний нет, в результате вы на андроиде имели ввиду одно, а ваша бабушка с айфоном приняла это за другое. Приехали.
:), :-), ^-^, O_O — одинаково распознаются в любой программе и шрифте. В отличие от миллиарда эмодзи, неизвестно присутствующих в очередной программе\шрифте или нет. Вопрос автозамены — вопрос к мессенджерам (у известных мне была опция отключения)
1. Вам никогда не приходили %), %|, :-*, :-P и др.?
2. Даже если брать стандартный :), что это: насмешка, смущённая улыбка или радость?
3. По факту, автозамена происходит. Отключить — иногда даже хуже, не отключив, ты хоть знаешь, когда собеседнику пришло покорёженное, а так полная угадайка. Особый шик, когда люди пользуются разными клиентами, и тебе приходит что-то типа «*STOP*» (
Я совершенно не фанат эмодзи. Но смайлики — это совсем не универсально и совсем не стандарт. И стандартизировать, по-моему, их бы ой как не мешало.
2. Это улыбка. Трактуется, как и обычная улыбка в контексте. Беседы и собеседника. Может быть всеми тремя и еще пачкой других.
3. Отключить — это если у вас есть регулярное перекидывание исходников, где оно напрягает. Да и то, обычно копипаст из мессенджера в любой блокнот спасает. Не знаю. У меня года полтора было отключено автораспознавание — остальное время как-то справлялся.
Эмодзи… это унылая попытка стандартизировать мимику и жесты. Безумно раздувающая размер шрифтов (мне где-то попадалась жалоба, что если включить шрифт с полным набором этой дряни, то программа автоматически тяжелеет едва ли не на пару десятков мегабайт).
Те же смайлы за все время их существования успешно развивались в рамках того небольшого набора символов который уже существовал. Сначала пришло редуцирование носа :-) => :), потом появились горизонтальные смайлы О_О и т.п. Эмодзи — это путь в никуда. Вы не отрисуете все эмоции, вы не отрисуете все флаги, вы не отрисуете все-что-только-можно придумать. Это по сути попытка засунуть в шрифт все клипарты на все темы — идея заведомо не имеющая смысла.
Как отличить, и вообще что делать с «ñ» и «ñ» (это совершенно разные последовательности символов, в них даже количество использованных байт отличается)? Как использовать в регулярках? Что делать, если в каком-нибудь прикладном языке мы получаем ошибку кодирования в utf-8 (вот это — абсолютный чемпион по количеству вопросов на SO)? Как правильно сравнивать строки? Реализация в разных языках? Почему умер UCS2 и чем плох UTF-16? Может быть, хоть что-то практическое?
> Приведённой выше информации вполне достаточно, чтобы не путаться
> в основных принципах и работать с текстом в большинстве повседневных задач
Да, если повседневные задачи у вас сводятся к чтению текста с экрана монитора. На гиктаймс вас что, не пускают?
Спасибо за подсказку по вопросам, о которых стоит рассказать.
И ваше «спасибо» блестяще смотрится на фоне того, что вы поленились вывести из «засеренного» мой комментарий, хотя в нем практической информации больше, чем во всех 100500 знаках вашей записи.
Если в мире в 2016 существует программист, который не в курсе, чем UTF-8 отличается от UTF-16, и что не так с однобайтными кодировками, то он и так неплохо зарабатывает на допиливании COBOL-монстров в Кремниевой долине.
Я, конечно, статью Спольски читал и вдохновился её простой. Но хорошего материала по Unicode на русском языке объективно мало, а поговорить есть о чём. Так что, уверен, моя статья не будет лишней, а цель на будущее — сделать качественный материал по всем основным аспектам Юникода.
Юникод: необходимый практический минимум для каждого разработчика