Нормализатор Unicode

В Unicode четыре формы нормализации, и они не взаимозаменяемы. NFC и NFD переупорядочивают один и тот же символ между одной кодовой точкой и базовой буквой плюс комбинирующим знаком; NFKC и NFKD идут дальше и переписывают символы, которые лишь похожи друг на друга — лигатуру fi, полноширинную A, обведённую ①, римскую цифру Ⅻ. Эта страница пропускает один и тот же текст через все четыре, выводит строку на каждый символ с описанием, что изменилось и почему, считает графемы относительно кодовых точек и байтов UTF-8 и сравнивает две строки в выбранной вами форме. Вторая половина — о символах, которые вообще не видны: нулевые пробелы, соединители, двунаправленные управляющие, селекторы вариантов, управляющие символы C0 и C1, кодовые точки приватного использования и сломанные половинки суррогатов, каждая с позицией и именем, и опция удалить их из копии текста.
Вся обработка выполняется локально в вашем браузере — ваши данные никогда не загружаются ни на один сервер.

Не уверены, что такое нормализация? Нормализация Unicode простыми словами — почему é может быть одним символом или двумя и почему из-за этого ломается вход.
Форма для отчёта и сравнения:
Сравнить два поля после:
NFC (пока ничего)
NFD (пока ничего)
NFKC (пока ничего)
NFKD (пока ничего)
Пока ничего не сравнено. Вставьте две строки и нажмите Сравнить две строки.
первая строка, нормализованная (пока ничего)
вторая строка, нормализованная (пока ничего)
Пока ничего не проверено. Нажмите Проверить на невидимые и опасные символы, затем Удалить, если хотите текст без этих символов.
текст без выбранных классов (пока ничего)
Ctrl+Enter в первом поле запускает активный режим.
Выберите режим и нажмите его кнопку действия. Нормализация выводит все четыре формы вашего текста и строку на каждый символ; Сравнение говорит, одинаковы ли две строки после нормализации; Проверка перечисляет символы, которых не видно, и может их удалить. Кнопка «Самопроверка» повторно прогоняет каждый задокументированный случай в файле данных этой страницы, каждый символ из таблиц невидимых и управляющих и выводит здесь число проверок.
Каждый символ, по одной строке:что это за символ, что делают с ним четыре формы и почему. Затенённая строка — символ, который меняет хотя бы одна форма; колонка NFD показывает, на что символ раскладывается, если вообще раскладывается.
Сколько здесь символов?Один и тот же текст, посчитанный четырьмя способами: графемные кластеры (что считает читатель), кодовые точки (что присваивает Unicode), единицы UTF-16 (что индексирует JavaScript) и байты UTF-8 (что передаётся). Нормализация меняет числа между второй и третьей колонками, не меняя первую.
Что найдено, в порядке появления:позиция считает кодовые точки, класс — тот, к которому принадлежит символ, а примечание говорит, что этот символ делает с тем, что читает текст дальше.
Классы, которые эта страница считает подозрительными:таблица выводится из самого сканера, поэтому слова на этой странице и поведение кнопок не могут разойтись. Серьёзность — о влиянии на сравнение, а не о том, насколько вероятно, что символ невинен.
Символы приватного использования перечислены как подозрительные по другой причине, чем остальные: они отображаются, но у них нет согласованного значения, поэтому одна и та же кодовая точка — разная картинка в каждом шрифте, который её определяет.
Какой размер входа принимает эта страница и почему:нормализация — это пересборка всей строки, а эта страница делает несколько таких пересборок плюс сканирование и подсчёт графем, поэтому стоимость растёт прямолинейно с длиной входа. Ограничения ниже существуют потому, что таблица по символам выводится в документ, а таблица с миллионом строк — это то, что на самом деле останавливает браузер.
Что такое нормализация Unicode?

Unicode даёт больше одного способа записать некоторые символы. Буква é может быть одной кодовой точкой, U+00E9, или обычной e, за которой идёт комбинирующий акут, U+0065 U+0301. Они выглядят одинаково на экране, печатаются одинаково, а как строки различны: разная длина, разные байты UTF-8, и 'é' === 'e\u0301' — это false.

Это не курьёз, а класс ошибок. Какой из двух вариантов вы получите, зависит от раскладки клавиатуры, операционной системы, метода ввода и от того, не копировался ли текст через текстовый процессор. Проверка пароля, уникальный индекс базы данных, чёрный список или ключ кэша, которые сравнивают строки побайтно, будут считать два написания разными значениями — поэтому пользователь может задать пароль на одном устройстве и не войти с другого, а одно и то же слово может появиться дважды в списке «уже занятых» имён.

Версия на 30 секунд: NFC и NFD — это один и тот же текст, записанный двумя способами: NFC композирует (e + акцент становится одним символом), NFD раскладывает (один символ становится e + акцент). Они задуманы без потерь и являются двумя формами для сравнения и хранения. NFKC и NFKD делают всё, что делают первые две, а затем ещё заменяют символы совместимости: лигатура fi становится двумя буквами, ½ становится 1⁄2, римская цифра ⅲ становится XII, полноширинная A становится обычной A. Именно это нужно индексу поиска или фильтру ключевых слов, и именно это нельзя применять к паролю никогда.
Почему четыре формы, а не одна

Разделение существует, потому что служат две разные цели. Композиция и разложение текста — это каноническое изменение: результат означает то же самое, и для первых двух форм ничего не теряется. Устранение лигатур, надстрочных знаков, обведённых букв и полноширинных форм — совместимостное изменение: его делают ради удобства сопоставления, и оно действительно разрушает различие между двумя строками, которые Unicode считает разными. Четыре формы — это каждая комбинация двух решений.

Таблица выше — это список случаев из файла данных этой страницы, проверенный по собственному String.prototype.normalize платформы, и самопроверка перепроверяет каждую её строку у вас на глазах. Читайте первые две строки вместе: это один и тот же текст, записанный двумя способами, и после нормализации это одна и та же строка.

Разобранные примеры

Это строки, по которым проходит кнопка «Пример»; страница нормализует их живьём, а не вводит как ответы. Каждая выбрана потому, что какая-то форма делает с ней что-то видимое.

Символы совместимости: во что обходится NFKC

NFKC и NFKD необратимы, и потеря намеренная. Они отвечают на вопрос «это одно и то же слово?», а не на вопрос «это одна и та же строка?». Из этого следуют три следствия, и все три легко получить неверно в продакшн-коде:

  • Никогда не храните пароль, токен или подпись в форме совместимости. Два разных входа свернутся в одно и то же сохранённое значение, и проверка длины или символов станет слабее. Нормализуйте пароли через NFC (или не нормализуйте вовсе) и сравнивайте их как байты, сделав в точности то же самое с обеих сторон.
  • Свёртка совместимости может превратить один символ в несколько. Лигатура fi — это одна кодовая точка, которая становится двумя буквами, поэтому поле, проверенное по длине до нормализации, может стать длиннее после. Сначала нормализуйте, потом проверяйте, потом сохраняйте.
  • NFKC не удаляет всё невидимое. Селектор варианта переживает NFKC и NFKD, и соединитель нулевой ширины тоже. Поэтому у этой страницы есть режим сканирования: нормализация отвечает на вопрос о форме и идентичности символа, а сканер отвечает на вопрос, есть ли вообще что-то.
Графемы, кодовые точки и байты

Нормализация меняет второй из этих счётчиков и обычно оставляет первый в покое. Все три числа — «длина текста», и все они различны, поэтому поле формы с ограничением 20 символов может соответствовать строке из 20 кодовых точек, 30 единиц UTF-16 и 47 байт.

Число в колонке графем — это то, что считает человек. Разрыв между ним и числом кодовых точек — ровно то, чем занимается сканер: имя из 12 кодовых точек может отображаться как 12 букв, как 3 буквы с 9 невидимыми знаками между ними или как один эмодзи.

Символы, которых вы не видите

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

Сканер в третьем режиме перечисляет каждый из них с позицией: символы нулевой ширины и соединители, двунаправленные управляющие, селекторы вариантов, необычные пробелы, управляющие символы C0 и C1, кодовые точки приватного использования и суррогат, который не является половинкой корректной пары, а потому вообще не символ. Таблица классов выше на этой странице объясняет, что делает каждый класс, а кнопка удаления убирает копию этих символов, не трогая ваш вход.

Ошибки, которые эта страница призвана ловить
  • Сравнение двух строк без их нормализации. 'é' === 'e\u0301' — это false. Нормализуйте обе стороны одной формой перед сравнением и используйте ту же форму в индексе, с которым сравниваете.
  • Нормализация только с одной стороны. Поиск, который нормализует запрос, но не сохранённое значение, ничего не находит и выглядит как проблема данных, а не нормализации.
  • Использование формы совместимости и называние её безопасной для NFC. NFKC, применённый к паролю, сокращает пространство возможных паролей и сливает входы, которые должны были различаться.
  • Предположение, что форма нормализации исправляет подделку. Она не удаляет соединитель нулевой ширины, селектор варианта или гомоглиф — кириллическая а и латинская a — разные кодовые точки, которые NFC не трогает.
  • Доверие к подсчёту символов. Счёт, который видит пользователь, счёт, который навязывает колонка базы данных, и счёт, который навязывает лимит байтов, — три разных числа, и строки выше показывают, насколько далеко они могут расходиться.
  • Предположение, что вы получите NFC. Текст, который приходит из метода ввода, файла или другой системы, находится в той форме, которую произвела эта система. Эта страница стартует с NFC, потому что это форма, которую ожидает веб, а не потому, что ваш вход именно такой.
Быстрые ответы

“Почему эти две строки не равны, если выглядят одинаково?”

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

“Какую форму использовать в приложении?”

NFC — для всего, что человек будет читать, отображать, хранить и искать; это форма, которую W3C рекомендует для веба. NFD — правильный выбор, когда код за вами ожидает базовые буквы и отдельные знаки, что обычно в обработке текста и в некоторых файловых системах macOS. Используйте NFKC или NFKD только там, где вы сопоставляете, а не храните: индексы поиска, фильтры ключевых слов, обнаружение дубликатов.

“Останется ли строка, которая уже нормализована, нормализованной после того, как я к ней что-то допишу?”

Не обязательно. Конкатенация не сохраняет нормализацию: склейка двух строк NFC может дать последовательность, которую NFC записал бы иначе, потому что комбинирующий знак в начале второй строки может присоединиться к символу в конце первой. Нормализуйте после сборки, а не только до неё.

“Удаляет ли нормализация невидимые символы?”

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

“Почему число байтов меняется, когда число символов не меняется?”

Потому что две формы построены из разных кодовых точек, которые в UTF-8 стоят разное число байтов. Предсоставленная é — это два байта; разложенная пара — три. Таблица счётчиков выше выводит все четыре числа рядом для входа и для каждой формы, так что разница видна, а не описана.

“Откуда я знаю, что результаты на этой странице верны?”

Нажмите Самопроверка. Она нормализует каждый случай из файла данных этой страницы, перепроверяет четыре формы друг против друга по задокументированным соотношениям (NFC идемпотентна, NFD строки NFC совпадает с NFD исходной и так далее), прогоняет через сканер каждый символ, названный в таблицах невидимых и управляющих, и сверяет счёт графемных случаев с Intl.Segmenter.

Стандарты и авторство

Четыре формы и их определения взяты из Unicode Standard, Annex 15 (Unicode Normalization Forms). Кодовые точки, использованные как примеры разложения, — это те, что назначил стандарт; эта страница не реализует таблицы разложения сама — она вызывает собственный String.prototype.normalize браузера, то есть ту же реализацию, которую платформа использует для ввода текста, сортировки и сравнения, так что сообщаемое здесь то же, что делает с этой строкой остальная система. Графемные кластеры берутся из Intl.Segmenter, если он есть в браузере, и из задокументированного приближения, если нет.

Текст страницы написан для этого сайта. Каждый рабочий пример на этой странице был получен собственным кодом страницы и затем сверен с записанным списком случаев файла данных, который в свою очередь сверен с данными Unicode NormalizationTest для тех же символов. Таблицы невидимых и опасных символов — курированное подмножество, написанное для предупреждений, понятных человеку, а не полный профиль безопасности Unicode (UTS #39), и страница говорит об этом, а не подразумевает полноту, которой у неё нет.