Вступ Rubyist до кодування символів, Unicode та UTF-8

Дуже ймовірно, що ви бачили виняток Ruby, такий як UndefinedConversionError або IncompatibleCharacterEncodings. Менше ймовірно, що ви зрозуміли, що означає виняток. Ця стаття допоможе. Ви дізнаєтесь, як працює кодування символів і як вони реалізовані в Ruby. Зрештою, ви зможете набагато легше зрозуміти та виправити ці помилки.

рубіст

Отже, що таке "кодування символів" у будь-якому випадку?

У кожній мові програмування ви працюєте зі рядками. Іноді ви обробляєте їх як вхідні дані, іноді відображаєте як вихідні. Але ваш комп’ютер не розуміє «рядків». Він розуміє лише біти: 1 і 0. Процес перетворення рядків у біти називається кодуванням символів.

Але кодування символів належить не лише епосі комп’ютерів. Ми можемо навчитися на більш простому процесі, перш ніж у нас з’явилися комп’ютери: азбука Морзе.

Азбука Морзе

Азбука Морзе дуже проста у своєму визначенні. У вас є два символи або способи отримання сигналу (короткий і довгий). За допомогою цих двох символів ви представляєте простий англійський алфавіт. Наприклад:

  • A є .- (одна коротка позначка та одна довга позначка)
  • Е є. (одна коротка позначка)
  • O є - (три довгі позначки)

Ця система була винайдена приблизно в 1837 році, і вона дозволяла кодувати весь алфавіт лише за допомогою двох символів або сигналів.

На зображенні ви можете побачити "кодера", особу, відповідальну за кодування та декодування повідомлень. Це незабаром зміниться з появою комп’ютерів.

Від ручного до автоматичного кодування

Для кодування повідомлення потрібно, щоб людина вручну переклала символи в символи, дотримуючись алгоритму азбуки Морзе.

Подібно до азбуки Морзе, комп’ютери використовують лише два «символи»: 1 і 0. Ви можете зберігати в комп’ютері лише їх послідовність, і коли вони читаються, їх потрібно інтерпретувати так, щоб це було зрозуміло для користувача.

Процес працює так в обох випадках:

SOS в азбуці Морзе це буде:

Великою зміною в роботі з комп’ютерами та іншими технологіями стало те, що процес кодування та декодування був автоматизований, тому ми більше не потребували людей для перекладу інформації.

Коли були винайдені комп’ютери, одним із ранніх стандартів, створених для автоматичного перетворення символів в 1 і 0 (хоча і не перший), був ASCII.

ASCII означає американський стандартний код для обміну інформацією. "Американська" частина відіграла важливу роль у тому, як комп'ютери деякий час працювали з інформацією; чому ми побачимо у наступному розділі.

ASCII (1963)

На основі знання телеграфних кодів, таких як азбука Морзе та дуже ранні комп'ютери, стандарт кодування та декодування символів у комп'ютері був створений приблизно в 1963 році. Ця система була відносно простою, оскільки спочатку охоплювала лише 127 символів, англійський алфавіт плюс додаткові символи.

ASCII працював, пов'язуючи кожен символ з десятковим числом, яке можна було перевести в двійковий код. Подивимось приклад:

"A" - це 65 в ASCII, тому нам потрібно перевести 65 у двійковий код.

Якщо ви не знаєте, як це працює, ось швидкий спосіб: ми починаємо ділити 65 на 2 і продовжуємо, поки не отримаємо 0. Якщо ділення не є точним, додаємо 1 як залишок:

Тепер беремо залишки і ставимо їх у зворотному порядку:

Отже, ми зберігали б "А" як "1000001" з оригінальним кодуванням ASCII, відомим тепер як US-ASCII. На сьогоднішній день, коли звичні 8-бітні комп’ютери, це буде 01000001 (8 біт = 1 байт).

Ми дотримуємося одного і того ж процесу для кожного символу, тому, маючи 7 біт, ми можемо зберігати до 2 ^ 7 символів = 127.

Ось повна таблиця:


(З http://www.plcdev.com/ascii_chart)

Проблема з ASCII

Що сталося б, якби ми хотіли додати ще один символ, наприклад французький ç або японський символ 大?

Так, у нас була б проблема.

Після ASCII люди намагалися вирішити цю проблему, створивши власні системи кодування. Вони використовували більше бітів, але це врешті-решт спричинило ще одну проблему.

Основна проблема полягала в тому, що під час читання файлу ви не знали, чи маєте ви певну систему кодування. Спроба інтерпретувати це з неправильним кодуванням призвела до дрібниць типу " " або "Ã, ÂÃ⠀ šÃ‚Â".

Еволюція цих систем кодування була великою і широкою. Залежно від мови, у вас були різні системи. Мови з більшою кількістю символів, такі як китайська, повинні були розробляти більш складні системи для кодування своїх алфавітів.

Після багатьох років боротьби з цим було створено новий стандарт: Unicode. Цей стандарт визначав спосіб, яким сучасні комп’ютери кодують та декодують інформацію.

Юнікод (1988)

Мета Unicode дуже проста. Відповідно до офіційного сайту:
"Надати унікальний номер для кожного персонажа, незалежно від платформи, програми чи мови."

Отже, кожному символу мови присвоєно унікальний код, також відомий як кодова точка. В даний час налічується понад 137 000 символів.

Як частина стандарту Unicode, у нас є різні способи кодування цих значень або кодових точок, але UTF-8 є найширшим.

Ті ж люди, які створили мову програмування Go, Роб Пайк і Кен Томпсон, також створили UTF-8. Це вдалося, оскільки воно ефективно і розумно в тому, як кодує ці цифри. Подивимось чому саме.

UTF-8: Формат перетворення Unicode (1993)

Зараз UTF-8 є фактичним кодуванням веб-сайтів (понад 94% веб-сайтів використовують це кодування). Це також кодування за замовчуванням для багатьох мов програмування та файлів. То чому це було так успішно і як це працює?

UTF-8, як і інші системи кодування, перетворює числа, визначені в Unicode, у двійкові, щоб зберігати їх у комп'ютері.

Є два дуже важливі аспекти UTF-8:
- Це ефективно при зберіганні бітів, оскільки символ може зайняти від 1 до 4 байт.
- Використовуючи Unicode та динамічну кількість байтів, він сумісний з кодуванням ASCII, оскільки перші 127 символів займають 1 байт. Це означає, що ви можете відкрити файл ASCII як UTF-8.

Давайте розберемо, як працює UTF-8.

UTF-8 з 1 байтом

Залежно від значення в таблиці Unicode, UTF-8 використовує різну кількість символів.

У перших 127 він використовує такий шаблон:
Іржа1
0_______

Отже, 0 завжди буде там, а потім двійкове число, що представляє значення в Unicode (яке також буде ASCII). Наприклад: A = 65 = 1000001.

Давайте перевіримо це на Ruby, використовуючи метод распакування в рядку:

B означає, що ми хочемо спершу двійковий формат із найзначнішим бітом. У цьому контексті це означає біт з найбільшим значенням.
Зірочка каже Рубі продовжувати, поки не буде більше біт. Якби замість цього ми використовували число, ми отримували б лише біти до цього числа:

UTF-8 з 2 байтами

Якщо у нас є символ, значення чи кодова точка якого в Unicode перевищує 127, до 2047 року, ми використовуємо два байти з таким шаблоном:

Отже, у нас є 11 порожніх бітів для значення в Unicode. Подивимось приклад:

À дорівнює 192 в Unicode, отже, в двійковому - 11000000, приймаючи 8 бітів. Він не вміщується в першому шаблоні, тому ми використовуємо другий:

Ми починаємо заповнювати пробіли справа наліво:

Що відбувається з порожніми бітами там? Ми просто поставили 0, тому кінцевий результат: 11000011 10000000.

Тут ми можемо побачити закономірність. Якщо ми почнемо читати зліва направо, перша група з 8 бітів має два одиниці на початку. Це означає, що символ займе 2 байти:

Знову ж таки, ми можемо перевірити це у Рубі:

Маленька порада тут полягає в тому, що ми можемо краще форматувати вихідні дані за допомогою:

Ми отримуємо масив з 'À'.unpack (' B8 B8 '), а потім об'єднуємо елементи пробілом, щоб отримати рядок. Знаки 8 в параметрі розпакування говорять Ruby отримати 8 бітів у 2 групах.

UTF-8 з 3 байтами

Якщо значення в Unicode для символу не вміщується в 11 бітах, доступних у попередньому шаблоні, нам потрібен додатковий байт:

Знову ж таки, три одиниці на початку шаблону говорять нам, що ми збираємось прочитати 3-байтовий символ.

Той самий процес застосовувався б до цього шаблону; перетворіть значення Unicode у двійкове і почніть заповнювати слоти справа наліво. Якщо після цього у нас залишилося кілька порожніх пробілів, заповніть їх 0.

UTF-8 з 4 байтами

Деякі значення приймають навіть більше, ніж 11 порожніх бітів, які ми мали в попередньому шаблоні. Давайте подивимось приклад із смайликами?, Які для Unicode також можна розглядати як символи типу "a" або "大".

Значення або кодова точка "?" в Unicode - 128578. Це число в двійковому файлі: 11111011001000010, 17 біт. Це означає, що він не вміщується в 3-байтовий шаблон, оскільки у нас було лише 16 порожніх слотів, тому нам потрібно використовувати новий шаблон, який займає 4 байти в пам'яті:

Починаємо знову, заповнюючи його двома числами:
Іржа1
11110___ 10_11111 10011001 10000010

А тепер заливаємо решту 0:
Іржа1
1111000 10011111 10011001 10000010

Давайте подивимося, як це виглядає в Ruby.

Оскільки ми вже знаємо, що це займе 4 байти, ми можемо оптимізувати для кращої читабельності на виході:

Але якби ми цього не зробили, ми могли б просто використати:

Ми також можемо використовувати рядовий метод "bytes" для вилучення байтів у масив:

А потім ми могли б відобразити елементи у двійкові:

І якби ми хотіли рядок, ми могли б використовувати join:

UTF-8 має більше місця, ніж потрібно для Unicode

Іншим важливим аспектом UTF-8 є те, що він може включати всі значення Unicode (або кодові точки) - і не тільки ті, що існують сьогодні, але й ті, які будуть існувати в майбутньому.

Це пов’язано з тим, що в UTF-8 із 4-байтовим шаблоном ми маємо 21 слот для заповнення. Це означає, що ми можемо зберігати до 2 ^ 21 (= 2 097 152) значень, що набагато більше, ніж найбільша кількість значень Unicode, яку ми коли-небудь матимемо зі стандартом, близько 1,1 мільйона.

Це означає, що ми можемо використовувати UTF-8 з упевненістю, що нам не потрібно буде переходити на іншу систему кодування в майбутньому, щоб розподіляти нові символи або мови.

Робота з різними кодуваннями в Ruby

У Ruby ми можемо відразу побачити кодування даного рядка, виконавши це:

Ми також можемо кодувати рядок за допомогою іншої системи кодування. Наприклад:

Якщо перетворення несумісне, ми отримуємо помилку за замовчуванням. Скажімо, ми хочемо перетворити "привіт?" від UTF-8 до ASCII. Оскільки смайлик "?" не вміщується в ASCII, ми не можемо. У цьому випадку Рубі викликає помилку:

Але Ruby дозволяє нам мати винятки, коли, якщо символ неможливо закодувати, ми можемо замінити його на "?".

У нас також є можливість замінити певні символи дійсним символом у новому кодуванні:

Перевірка кодування сценарію сценарію в Ruby

Щоб побачити кодування файлу сценарію, над яким ви працюєте, файлу ".rb", ви можете зробити наступне:

Починаючи з Ruby 2.0, кодування за замовчуванням для скриптів Ruby - UTF-8, але ви можете змінити це за допомогою коментаря в першому рядку:

Але краще дотримуватися стандарту UTF-8, якщо у вас немає поважних причин змінити його.

Кілька порад щодо роботи з кодуваннями в Ruby

Ви можете побачити весь список підтримуваних кодувань у Ruby із Encoding.name_list. Це поверне великий масив:

Іншим важливим аспектом при роботі з символами поза англійською мовою є те, що до Ruby 2.4 деякі методи, такі як upcase або reverse не працювали належним чином. Наприклад, у Ruby 2.3 upcase не працює, як ви думаєте:

Обхідним шляхом було використання ActiveSupport, від Rails, або іншого зовнішнього каменя, але з Ruby 2.4 ми маємо повне відображення випадків Unicode:

Деякі розваги з смайликами

Давайте подивимося, як смайли працюють в Unicode та Ruby:

Це "Піднята рука з частиною між середнім і кільцевим пальцями", також відома як смайлик "Вулканський салют". Якщо у нас однакові смайли, але в іншому відтінку шкіри, який не є типовим, трапляється щось цікаве:

Тому замість того, щоб бути лише одним персонажем, у нас є два для одного смайлика.

Що там сталося?

Ну, деякі символи в Unicode визначаються як комбінація декількох символів. У цьому випадку, якщо комп’ютер бачить ці два символи разом, він показує лише одного із застосованим відтінком шкіри.

Є ще один цікавий приклад, який ми можемо побачити з прапорами.

В Unicode прапорці смайлів внутрішньо представлені деякими абстрактними символами Unicode, які називаються "регіональними індикаторними символами", наприклад? або?. Зазвичай вони не використовуються поза прапорами, і коли комп’ютер бачить два символи разом, він показує прапорець, якщо є такий для цієї комбінації.

Щоб переконатися в цьому, спробуйте скопіювати це та видалити кому в будь-якому текстовому редакторі або полі:

Висновок

Я сподіваюся, що цей огляд того, як працюють Unicode та UTF-8, і як вони відносяться до Ruby та потенційних помилок був для вас корисним.

Найважливіший урок, який слід забрати, - це пам’ятати, що коли ви працюєте з будь-яким текстом, у вас є пов’язана система кодування, і важливо зберігати його при зберіганні чи зміні. Якщо ви можете, скористайтеся сучасною системою кодування, такою як UTF-8, щоб вам не потрібно було її змінювати в майбутньому.

Примітка про випуски Ruby

Я використовував Ruby 2.6.5 для всіх прикладів у цій статті. Ви можете спробувати їх в Інтернеті REPL або локально, перейшовши до свого терміналу та виконавши irb, якщо у вас встановлений Ruby.

Оскільки підтримка Unicode була покращена в останніх випусках, я вирішив використовувати останню, щоб ця стаття залишалася актуальною. У будь-якому випадку, з Ruby 2.4 і вище, усі приклади повинні працювати, як показано тут.