Инспектор символов 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 его нет, и он вынужден писать эмодзи двумя суррогатными половинами.

Скрытые символы и текст, вышедший из модели ИИ

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

То, что доказывает скрытый символ, уже, чем предполагает почти всё написанное на эту тему. Внутри кодовой точки не записано, откуда она взялась: та, что пришла при копировании из системы управления содержимым, и та, что пришла со сгенерированным ответом, — одно и то же значение. Невидимые символы действительно отмечали в выводе больших языковых моделей, и в случае, привлёкшем больше всего внимания — узкий неразрывный пробел там, где полагался обычный, — производитель модели ответил, что это побочный продукт обучения, а не что-то помещённое туда намеренно, и сообщения прекратились за считаные дни. Так что находка здесь говорит, что удалить. Она не говорит, что написало фразу.

  • Пробелы, которые не являются обычным пробелом. Неразрывный пробел (U+00A0) и узкий неразрывный пробел (U+202F) называют чаще прочих; каждый примерно шириной с пробел, каждый — другой символ, и инструмент помечает оба.
  • Символы вовсе без ширины. Пробел нулевой ширины (U+200B), соединитель слов (U+2060) и метка порядка байтов (U+FEFF) не рисуют ничего, переживают проверку и ломают первое же сравнение, которого коснутся. Они помечаются как невидимые.
  • Блок Tags (от U+E0000 до U+E007F). В нём по одной невидимой кодовой точке на каждый печатный символ ASCII — U+E0041 называется TAG LATIN CAPITAL LETTER A — так что целая фраза может быть записана внутри текста, который не показывает ровно ничего. Каждая из них здесь названа и помечена как невидимая.
  • Пунктуация, на которую валят вину и которая ничего не скрывает. Длинное тире (U+2014) и типографский апостроф (U+2019) рисуют видимый знак, поэтому здесь их ничто не помечает; это обычная типографика, чем бы она ни была произведена.

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

Частые вопросы

Почему моя строка длиннее, чем число символов, которые я вижу?
Либо она содержит невидимые символы, либо содержит графемы, построенные из нескольких кодовых точек. И то и другое показано здесь: невидимые символы помечаются, а графемы из нескольких кодовых точек группируются, чтобы вы видели один символ и те несколько значений, из которых он составлен.
Две строки выглядят одинаково и не равны. Почему?
Обычно из-за нормализации. Буква с диакритикой может быть одной кодовой точкой или буквой плюс комбинирующий знак, и обе рисуются одинаково. Вставьте сюда каждую строку и сравните списки кодовых точек или посмотрите, какие формы нормализации отличаются от ввода.
Что такое пробел нулевой ширины и как он попал в мой текст?
Это символ без ширины и без следа, служащий подсказкой о возможности переноса строки. Обычно он приходит при копировании текста с веб-страницы, из редактора документов или из чат-клиента, и, поскольку ничего не отображается, переживает любую проверку, пока что-нибудь не сравнит строки и не сломается.
В чём разница между кодовой точкой и символом?
Кодовая точка — это одно значение Unicode. Символ в обиходном смысле — это графема, то, что читатель считает за один, и графема может состоять из нескольких кодовых точек. Эмодзи семьи — это пять кодовых точек и одна графема.
Почему инструмент говорит, что у некоторых символов нет имени?
По четырём разным причинам, и он говорит, по какой именно. У суррогатов и кодовых точек частного использования нет имени в стандарте, у неназначенных нечего называть, а у символа из редко просматриваемого исторического блока имя есть, но эта сборка его не несёт: таблица имён хранит те блоки, из которых люди действительно вставляют, вместо того чтобы отправлять все.
Является ли предупреждение о смешении письменностей доказательством атаки?
Нет. Оно означает, что одно слово берёт свои буквы более чем из одной системы письма, — это форма поддельного имени и вместе с тем нечто, что обычный текст иногда делает. Это повод посмотреть, а не приговор.
Отправляется ли что-нибудь из вставленного на сервер?
Нет. База данных символов загружается вместе со страницей, и всё работает в вашем браузере; ничего из набранного не выгружается и не записывается, и это работает без сетевого соединения.
Содержит ли текст из модели ИИ невидимые символы?
Иногда, и никогда так, чтобы это что-то опознавало. Больше всего внимания привлёк узкий неразрывный пробел (U+202F), появлявшийся в длинных ответах там, где полагался обычный; производитель модели ответил, что это побочный продукт обучения, а не что-то помещённое туда намеренно, и за считаные дни его перестали видеть. Вставьте свой текст сюда — и увидите, что в нём есть. Чего вы не увидите, так это откуда он взялся, потому что кодовая точка этого не записывает.
Как найти и удалить скрытые символы в тексте?
Вставьте его сюда, чтобы найти: каждая кодовая точка получает строку, со своим именем, если оно у неё есть, а те, что заслуживают второго взгляда, посчитаны над списком. Удаление — дело вашего редактора, здесь ничто не переписывает ваш текст, но, узнав, за какой кодовой точкой вы гонитесь, вы найдёте её в каждой строке как экранирование в шести форматах, а это и нужно полю поиска, которое их понимает.
Длинное тире или типографская кавычка — это скрытый символ?
Нет. Длинное тире (U+2014) и типографский апостроф (U+2019) оба рисуют знак на экране, поэтому здесь их ничто не помечает. На них часто указывают как на признак сгенерированного письма, а это вопрос стиля, на который инспектор символов ответить не может. Знать о них всё же стоит по другой причине: ни один из них не ASCII, так что оба способны помешать тому, что ждёт ASCII, и строка, написанная ими, не окажется равной той же строке, набранной дефисом и прямым апострофом.

Похожие инструменты