Инспектор символов Unicode
Разбирает текст на кодовые точки с именами, категориями, блоками и кодировками и отмечает невидимые, bidi- и похожие символы.
Что делает этот инструмент
Вставьте любой текст, и он вернётся разобранным на отдельные символы, из которых на самом деле состоит. Каждый получает своё имя Unicode, свой номер U+, к какому виду символов относится, к какой системе письма и к какому блоку принадлежит, сколько байтов занимает в UTF-8 и UTF-16 и какое экранирование требует в шести разных форматах.
Большинство приходит сюда потому, что что-то не сходится. Строка длиннее, чем выглядит; два значения, отображаемые одинаково, отказываются быть равными; имя пользователя отклонили из-за символов, похожих на обычные буквы; или строка кода читается не так, как ведёт себя. У всех четырёх одна и та же форма: символ делает то, чего экран не показывает.
Символы, кодовые точки и графемы
Слово «символ» означает по меньшей мере три разные вещи, и путаница между ними — источник почти всей путаницы вокруг Unicode. Кодовая точка — это одно значение стандарта Unicode, записываемое U+0041 или U+1F600. Графема — это то, что читатель считает одним символом. Часто это не одни и те же числа.
- Простая «a» — это 1 графема, 1 кодовая точка и 1 единица UTF-16: всё сходится.
- U+00E9, предсоставленная é, — это 1 графема, 1 кодовая точка и 1 единица UTF-16.
- U+0065 U+0301, та же буква, записанная как e плюс комбинирующий акут, — это 1 графема, но 2 кодовые точки и 2 единицы UTF-16.
- U+1F600, улыбающееся лицо, — это 1 графема и 1 кодовая точка, но 2 единицы UTF-16: она живёт выше BMP, поэтому JavaScript хранит её суррогатной парой.
- Эмодзи семьи — это 1 графема, 5 кодовых точек и 8 единиц UTF-16: три человека, соединённые двумя невидимыми соединителями.
Эмодзи семьи — тот случай, на котором люди спотыкаются. Выглядит как одна вещь, JavaScript сообщает длину 8, а столбец базы данных на 5 символов её не вместит. Этот инструмент группирует кодовые точки под графемой, которую они образуют, так что оба прочтения видны сразу, а не сводятся в уме.
Символы, которых не видно
Удивительно много символов Unicode не рисуют вообще ничего. Они существуют, чтобы управлять поведением текста вокруг них, и, не оставляя следа, переживают копирование, вставку и ревью так, что этого никто не замечает. Вот на какие инструмент указывает, а не просто перечисляет:
- Пробел, соединитель и разъединитель нулевой ширины (U+200B, U+200D, U+200C) — ни ширины, ни следа, и они ломают любое сравнение строк, которого касаются.
- Мягкий перенос (U+00AD) — подсказка, где слово можно разорвать, невидимая, пока это не произойдёт.
- Неразрывный пробел (U+00A0) — выглядит точно как пробел и является другим символом, поэтому так долго живёт в конфигурационных файлах.
- Метка порядка байтов (U+FEFF) — часто остаётся в начале файла, где становится невидимым первым символом первого значения.
- Управляющие символы и переопределения bidi (от U+202A до U+202E, от U+2066 до U+2069) — они переставляют текст вокруг себя.
Управляющие символы bidi заслуживают отдельного упоминания. Их добавили, чтобы иврит и арабский корректно смешивались с латинским текстом, и с этой работой они справляются хорошо. Но переопределение, помещённое внутрь комментария или строкового литерала, может заставить строку кода отображаться в порядке, совершенно отличном от того, который читает компилятор, — а значит, ревьюер может одобрить код, делающий совсем другое. Этот класс приёмов известен как Trojan Source, и именно поэтому инструмент помечает каждый управляющий символ bidi отдельно от прочих невидимых символов.
Похожие символы и смешение письменностей
В Unicode много символов, визуально неотличимых от букв ASCII и таковыми не являющихся. Кириллическая а, греческая ο и латинская a занимают разные кодовые точки и в большинстве шрифтов рисуются одинаково. Домен или имя пользователя, собранные из них, выглядят правильно и ведут в другое место.
Помечать каждого не-ASCII двойника было бы бесполезно: это выкрасило бы красным каждое русское, греческое или еврейское слово, а предупреждение, срабатывающее на обычном тексте, — это предупреждение, которое учатся пропускать. Поэтому правило здесь то же, что и в стандарте безопасности Unicode: слово подозрительно, когда буквы внутри него происходят более чем из одной системы письма:
- «paypal.com», написанное целиком латиницей: сказать нечего.
- То же слово с кириллическими эр и а перед латинским «ypal»: одно слово, две системы письма, помечено.
- Целое русское слово кириллицей: одна система письма, обычный текст, не помечено.
- Японская фраза, использующая хань, хирагану и катакану вместе: три письменности, одна система письма, не помечено.
Последний случай и есть причина, по которой правило требует аккуратности. Японский пишется тремя письменностями сразу, а корейский смешивает две, так что наивная проверка «больше одной письменности — подозрительно» объявила бы атакой каждую японскую фразу. Сочетания, которые действительно образуют одну систему письма, трактуются как одна, и это сохраняет предупреждению смысл.
Нормализация, или почему две одинаковые строки различаются
Некоторые символы можно записать более чем одним способом. Буква é — это либо единственная кодовая точка U+00E9, либо пара U+0065 U+0301: простая e, за которой следует комбинирующий акут. Обе отображаются одинаково. Ни одна не является ошибочной. Равными они не являются.
Нормализация — это выбор одного написания, и у неё четыре формы. NFC составляет там, где может, и именно её веб предполагает по умолчанию; NFD, наоборот, разлагает. Две формы K идут дальше и сворачивают также символы совместимости — полноширинная A становится обычной A, а лигатура fi становится fi, — что полезно для поиска и сопоставления и с потерями для хранения.
Панель здесь показывает все четыре формы того, что вы вставили, и отмечает, какие из них отличаются от ввода. Если ни одна не отличается, текст уже нормализован и расхождение, за которым вы гонитесь, находится в другом месте. Если некоторые отличаются, вы его нашли.
Имена, категории, письменности и блоки
Каждый назначенный символ несёт четыре элемента идентичности, и они отвечают на разные вопросы. Имя — это сам символ. Общая категория говорит, что это за вещь: строчная буква, комбинирующий знак, символ валюты, управляющий символ форматирования. Письменность — это система письма, к которой он относится. Блок — это диапазон кодовых точек, в котором он был назначен, и это факт организационный, а не лингвистический.
Блок и письменность легко перепутать, и они часто не совпадают. Латинские буквы живут по меньшей мере в полудюжине блоков, а один блок может содержать символы нескольких письменностей. Когда вы хотите знать «какой это алфавит», ответ — письменность; когда хотите знать «где это находится в стандарте» — блок.
Имена, письменности и блоки показаны здесь по-английски во всех языках, и это сделано намеренно. Это идентификаторы, определённые стандартом, а не проза: U+200D во всём мире называется ZERO WIDTH JOINER, и перевод сделал бы его невозможным для поиска.
Кодировки и экранирование
Кодовая точка — это число; байтами она становится только после выбора кодировки. UTF-8 использует от одного до четырёх байтов и оставляет ASCII нетронутым, поэтому он и победил. UTF-16 использует одну или две 16-битные единицы, и именно из них состоят строки JavaScript, — потому .length и сообщает 2 для одного эмодзи.
U+0041 41 # UTF-8: один байт, как в ASCII U+00E9 C3 A9 # UTF-8: два байта U+20AC E2 82 AC # UTF-8: три байта U+1F600 F0 9F 98 80 # UTF-8: четыре байта
Каждая строка здесь даёт также символ, записанный как экранирование для JavaScript, JSON, HTML, CSS, URL и Python, готовый к копированию. Они не все одинаковы, и выше BMP разница существенна: у JavaScript и Python есть экранирование по кодовой точке, и они им пользуются, а у JSON его нет, и он вынужден писать эмодзи двумя суррогатными половинами.
Частые вопросы
- Почему моя строка длиннее, чем число символов, которые я вижу?
- Либо она содержит невидимые символы, либо содержит графемы, построенные из нескольких кодовых точек. И то и другое показано здесь: невидимые символы помечаются, а графемы из нескольких кодовых точек группируются, чтобы вы видели один символ и те несколько значений, из которых он составлен.
- Две строки выглядят одинаково и не равны. Почему?
- Обычно из-за нормализации. Буква с диакритикой может быть одной кодовой точкой или буквой плюс комбинирующий знак, и обе рисуются одинаково. Вставьте сюда каждую строку и сравните списки кодовых точек или посмотрите, какие формы нормализации отличаются от ввода.
- Что такое пробел нулевой ширины и как он попал в мой текст?
- Это символ без ширины и без следа, служащий подсказкой о возможности переноса строки. Обычно он приходит при копировании текста с веб-страницы, из редактора документов или из чат-клиента, и, поскольку ничего не отображается, переживает любую проверку, пока что-нибудь не сравнит строки и не сломается.
- В чём разница между кодовой точкой и символом?
- Кодовая точка — это одно значение Unicode. Символ в обиходном смысле — это графема, то, что читатель считает за один, и графема может состоять из нескольких кодовых точек. Эмодзи семьи — это пять кодовых точек и одна графема.
- Почему инструмент говорит, что у некоторых символов нет имени?
- По четырём разным причинам, и он говорит, по какой именно. У суррогатов и кодовых точек частного использования нет имени в стандарте, у неназначенных нечего называть, а у символа из редко просматриваемого исторического блока имя есть, но эта сборка его не несёт: таблица имён хранит те блоки, из которых люди действительно вставляют, вместо того чтобы отправлять все.
- Является ли предупреждение о смешении письменностей доказательством атаки?
- Нет. Оно означает, что одно слово берёт свои буквы более чем из одной системы письма, — это форма поддельного имени и вместе с тем нечто, что обычный текст иногда делает. Это повод посмотреть, а не приговор.
- Отправляется ли что-нибудь из вставленного на сервер?
- Нет. База данных символов загружается вместе со страницей, и всё работает в вашем браузере; ничего из набранного не выгружается и не записывается, и это работает без сетевого соединения.