Sanyanggae LogoSanyanggae

💻 Веб и фронтенд

Чем Unicode отличается от UTF-8? Искажённый корейский текст, байты, длина строк и нормализация

Сравним кодовые точки корейских букв и эмодзи, байты UTF-8 и длину строк JavaScript; отделим ошибки декодирования от проблем шрифтов, нормализацию от сохранения оригинала.

사냥개 (Sanyanggae)Опубликовано Обновлено 16 мин. чтения

Это перевод корейского оригинала. Корейский оригинал · Проверка перевода: Проверка с помощью ИИ

Поясняющая схема из трёх блоков: символы и кодовые точки, байты UTF-8, отображение и нормализация
Собственная поясняющая схема различий между номерами символов Unicode, байтами хранения UTF-8 и правилами отображения и сравнения; это не снимок сбоя и не результат измерения.

1. Номер символа и сохраняемые байты — разные вещи

Представьте, что после экспорта корейского документа и повторного открытия его заголовок превратился в странные символы. Язык автора не изменился: возможно, не совпали правила чтения сохранённых чисел. Unicode — стандарт, назначающий символам и знакам номера, называемые кодовыми точками, а UTF-8 — кодировка, представляющая соответствующие значения последовательностями байтов. Например, корейский слог «가» имеет кодовую точку U+AC00. Запись после U+ шестнадцатеричная; это не позиция в файле и не адрес памяти. Номер не задаёт форму шрифта или координаты пикселей. Одна кодовая точка может выглядеть по-разному в разных шрифтах и системах набора. Поддержка Unicode, сохранение в UTF-8 и установка корейского шрифта связаны, но проверять их нужно отдельно. Объяснение UTF от Unicode помогает отделить символы от способов хранения.

В UTF-8 слог «가» записывается тремя байтами EA B0 80 в шестнадцатеричном виде. Латинская A имеет точку U+0041 и один байт UTF-8 — 41. Байт состоит из восьми битов, а число байтов на символ может различаться даже в одном документе. Расширения .txt или .csv сами по себе не сообщают фактическую кодировку. UTF-8 представляет допустимые скалярные значения Unicode последовательностями от одного до четырёх байтов, поэтому точный размер файла нельзя получить, умножив количество видимых знаков на одну константу. Пример отдельного корейского слога нельзя распространять на все корейские знаки или языки. Кодирование переводит сведения о символах в байты; оно не является сжатием или шифрованием. Общий размер одинаковой фразы меняется из-за служебных данных формата, переводов строк и сжатия при передаче. Отделяя описанное в WHATWG Encoding Standard преобразование байтов и символов от полного размера файла и конфиденциальности, легче сузить причину проблемы.

2. Байты, кодовые единицы и видимые символы

Свойство length строки JavaScript считает кодовые единицы UTF-16. Это не число байтов файла UTF-8 и не число символов в восприятии человека. Например, лицо «😀» — одна кодовая точка U+1F600, но две единицы UTF-16, поэтому '😀'.length равно 2. В UTF-8 это четыре байта F0 9F 98 80. Десятичное 128512, полученное через codePointAt(0), обозначает тот же номер, что U+1F600, а не количество символов или размер файла. У «가» одна кодовая точка, одна единица UTF-16 и три байта UTF-8. Разные числа для одной строки объясняются разными единицами измерения. Описание длины строк MDN прямо указывает единицу length. По ней также нельзя точно определить, сколько байтов памяти реально выделил движок JavaScript.

Даже подсчёт отдельных кодовых точек не всегда совпадает с числом видимых пользователю символов. К базовому знаку могут присоединяться комбинируемые отметки; один эмодзи может содержать несколько точек. Правила сегментации Unicode описывают границы расширенных графемных кластеров. Перебор по кодовым точкам, например Array.from(text), не начинает автоматически считать такие многоточечные группы. Для браузера можно проверить поддержку среды и рассмотреть Intl.Segmenter с granularity, установленной в 'grapheme'. Однако кластер не обязательно соответствует одному рисунку шрифта, а ожидания редактирования различаются между языками. Для поля описания нужен понятный пользователю счётчик символов, а для хранения и передачи — отдельный байтовый предел. Если написать «20 символов», но обрезать только единицы UTF-16, можно разорвать эмодзи или комбинируемую последовательность посередине.

3. При искажении текста разделяйте хранение, передачу и показ

Искажение может появиться, если кодировка при записи байтов отличается от декодирования при чтении. Файл UTF-8, прочитанный другой кодировкой, способен показать неверные символы, хотя сам файл ещё не изменён. В HTML одновременно проверяют реальный формат хранения, charset в HTTP-заголовке Content-Type и объявление meta charset в начале документа. Атрибут lang='ko' сообщает язык содержимого, а не выбирает декодирование корейских байтов, как charset. Руководство W3C по объявлению кодировки требует не просто поменять объявление на UTF-8, но и действительно сохранить файл в UTF-8, согласовав объявления документа и сервера. Наличие сигнатуры кодировки влияет на приоритет определения; meta не обязательно перекрывает любую другую информацию. Если файл выглядит по-разному при прямом открытии и получении с сервера, настройки передачи становятся важной подсказкой.

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

4. Проверка тех же значений через TextEncoder и TextDecoder

Простой опыт начинайте с собственной короткой строки. TextEncoder преобразует строку JavaScript в массив байтов UTF-8. У new TextEncoder().encode('가') шестнадцатеричный результат EA B0 80, а length массива равен 3. Инструменты разработчика могут показать те же значения десятичными числами 234, 176, 128: это другая система счисления, а не другие данные. Сравнение с обычным length строки, равным 1, выявляет различие единиц. Чтение массива через new TextDecoder('utf-8').decode(bytes) возвращает «가»; bytes здесь означает созданный выше массив. Этот кодировщик выдаёт UTF-8, а не сохраняет произвольно выбранную старую кодировку как универсальный конвертер. При исследовании реального файла сначала нужны его исходные байты. Повторное кодирование уже неверно прочитанного экранного текста в UTF-8 не доказывает, какими были байты первоначального файла.

Если конструктору TextDecoder передать 'utf-8' и { fatal: true }, недопустимый ввод можно обрабатывать ошибкой вместо молчаливой замены. Например, одиночный байт FF недопустим в UTF-8, поэтому декодирование в этом режиме завершится неудачей. В обычном режиме замены повреждённый участок может стать символом U+FFFD. Но отсутствие ошибки не означает, что определена исходная кодировка или проверена правильность содержания. Некоторые байты допустимы в нескольких кодировках; результат прежнего ошибочного чтения тоже мог быть заново сохранён как допустимый UTF-8. Такая проверка устанавливает, принимает ли ввод выбранный декодер. Она автоматически не удостоверяет происхождение документа, совпадение с оригиналом или отсутствие промежуточной подмены файла.

5. Нормализация отдельно согласует представления

Две правильно прочитанные строки могут иметь разную последовательность кодовых точек. «가» представляется одной U+AC00 либо последовательностью начального согласного U+1100 и гласного U+1161, которые соединяются. Эти два значения отличаются от простого перечисления совместимых корейских букв «ㄱ» и «ㅏ», поэтому важны их номера. Обычно оба представления рисуются одним слогом, но при сыром сравнении строк JavaScript могут не совпадать. По правилам нормализации Unicode, применение NFC к обеим сторонам приводит этот канонически эквивалентный пример к одному представлению. NFD выполняет каноническое разложение. Здесь UTF-8 для NFC занимает три байта, а два знака разложенной NFD — шесть; одинаковая картинка не гарантирует одинакового размера хранения. Если запрос и сохранённое имя выглядят одинаково, но не совпадают, стоит проверить эту разницу. Это другой случай, чем искажение из-за неверного чтения байтов.

Форму нормализации выбирают по задаче. NFC работает с канонической эквивалентностью, а NFKC включает совместимое разложение и может шире менять отдельно введённые варианты, например полноширинные цифры и цифры в кружках. Для поисковой копии сравнения это бывает полезно, но в математической записи и исходных материалах различия могут быть значимы. Поэтому полезно сохранить оригинал и определить форму для нужной сравнительной копии. Нормализация также не заменяет сравнение регистра, языковую сортировку или поиск одинаково звучащих слов. Массовое преобразование имён файлов или идентификаторов учётных записей может объединить разные записи в одно значение сравнения: сначала изучите правила именования и обработку коллизий. Хранение всех внешне похожих знаков как одного значения не является общей целью нормализации.

6. Сохраняйте оригинал и отмечайте границы преобразований

Прежде чем перезаписывать первоначальный файл в поиске причины повреждения, сохраните исходные байты и путь получения. Читайте копию предполагаемой кодировкой и проверяйте, сохраняется ли то же содержимое после повторного сохранения внешне верного результата. Описанная в материале W3C о BOM начальная сигнатура UTF-8 — EF BB BF; она необязательна. UTF-8 не требует выбора порядка байтов, поэтому нельзя приписывать этой сигнатуре ту же роль, что выбор порядка в UTF-16. Добавление или удаление BOM также отличается от перекодирования основных байтов в другую кодировку. Если нечитаемые значения уже сохранены как символ замены или вопросительный знак, несколько исходных значений могли слиться, и автоматическое восстановление не гарантировано. Оценивать обратимость можно только при известных исходной кодировке и истории преобразований; нормализация не восстанавливает утраченные байты.

Многоязычную функцию нельзя проверять только одной английской строкой. В короткий набор включите корейский слог, латинскую букву с комбинируемым знаком, эмодзи дополнительной плоскости и текст справа налево, например арабский или иврит; сравните ввод, сохранение, повторное чтение и экспорт. На каждом этапе нужны видимая форма, кодовые точки, число байтов UTF-8 и принятая продуктом единица подсчёта символов. Направление чтения не исправляется заменой UTF-8 другой кодировкой; настройки языка и направления интерфейса проверяют отдельно. Меняя ограничения ввода или поисковое сравнение, тоже сохраняйте оригиналы и объясняйте новые правила. Проверьте, используют ли браузерный счётчик и серверный предел одну единицу; если ограничения различаются, покажите каждое. Числа и кодовые примеры здесь описывают короткие учебные строки, а не измеренный сбой конкретного сервиса. Последовательная проверка номеров символов, байтов хранения, единиц строк, отображения и сравнения помогает воспроизвести изменение на каждой границе.

Источники и материалы

Статья объясняет сведения из открытых источников. Упоминание организации не означает её проверки или одобрения.

Об издателе и контакты

Другие статьи по теме