Sanyanggae LogoSanyanggae

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

Чим Unicode відрізняється від UTF-8? Спотворений корейський текст, байти, довжина рядків і нормалізація

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

사냥개 (Sanyanggae)Опубліковано Оновлено 15 хв

Це переклад корейського оригіналу. Корейський оригінал · Перевірка перекладу: Перевірено ШІ

Пояснювальна схема з трьох блоків: символи й кодові точки, байти 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 виконує канонічне розкладання. Тут NFC у UTF-8 займає три байти, а два знаки NFD — шість, тож однаковий вигляд не гарантує однакового обсягу. Якщо запит і збережене ім'я однакові на вигляд, але не збігаються, дослідіть цю різницю. Це не те саме, що спотворення від неправильного читання байтів.

Форму нормалізації добирають відповідно до мети. NFC працює з канонічною еквівалентністю, а NFKC містить сумісне розкладання та ширше змінює введені відмінності, наприклад повноширинні цифри чи цифри в колах. Для порівняльної пошукової копії це може бути корисно, але в математичному записі чи первинному матеріалі різниця буває значущою. Тому варто зберегти оригінал і визначити форму лише для потрібної копії порівняння. Нормалізація не замінює порівняння регістру, мовного сортування чи пошуку однаково вимовлюваних слів. Масова зміна імен файлів або ідентифікаторів облікових записів може звести різні елементи до однакового значення; спершу перевірте правила іменування та колізій. Зберігати всі зовнішньо схожі знаки як одне значення — не загальна мета нормалізації.

6. Зберігайте оригінал і межі перетворень

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

Багатомовну функцію не перевіряють лише одним англійським рядком. Додайте до короткого набору корейський склад, латинську літеру з комбінувальним знаком, емодзі додаткової площини та текст справа наліво, як арабська або іврит; порівняйте введення, зберігання, повторне читання й експорт. На кожному етапі перевіряють вигляд, точки, число байтів UTF-8 і визначену продуктом одиницю кількості символів. Напрям читання не виправляється заміною UTF-8 іншим кодуванням; мову й напрям інтерфейсу налаштовують окремо. Змінюючи межі вводу чи пошукове порівняння, теж зберігайте оригінали й пояснюйте нові правила. Перевірте, чи браузерний лічильник і серверний ліміт мають одну одиницю; за різних обмежень покажіть кожне. Числа й код тут стосуються коротких навчальних рядків, а не виміряного збою конкретного сервісу. Послідовно перевіряючи номери символів, байти, одиниці рядків, показ і порівняння, можна відтворити, що змінилося на кожній межі.

Докази й джерела

Стаття пояснює інформацію з відкритих джерел. Посилання не означає перевірки чи схвалення відповідною організацією.

Про видавця та контакти

Інші статті на цю тему