Як працюють кодування тексту. Звідки з'являються «кракозябры». Принципи кодування. Узагальнення і детальний розбір

Alex Alex 04 грудня 2019

Як працюють кодування тексту. Звідки з'являються «кракозябры». Принципи кодування. Узагальнення і детальний розбір
Дана стаття має на меті зібрати і розібрати принципи та механізм роботи кодувань тексту, детально цей механізм розібрати і пояснити. Корисна вона буде тим, хто лише приблизно уявляє, що таке кодування тексту та як вони працюють, чим відрізняються один від одного, чому іноді з'являються не читаються символи, який принцип кодування мають різні кодування.

Щоб отримати детальне розуміння цього питання доведеться прочитати і звести воєдино не одну статтю і витратити досить багато часу на це. В даному матеріалі ж це все зібрано і по ідеї повинно заощадити час і розбір на мій погляд вийшов досить докладний.

Про що буде під катом: принцип роботи одне байтових кодувань (ASCII, Windows-1251 і т. д.), передумови появи Unicode, що таке Unicode, Unicode-кодування UTF-8, UTF-16, їх відмінності, принципові особливості, сумісність і несумісність різних кодувань, принципи кодування символів, практичний розбір кодування і декодування.

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

Передумови Unicode


Думаю варто почати з того часу коли комп'ютеризація ще не була так сильно розвинена і тільки набирала обертів. Тоді розробники і стандартизатори ще не думали, що комп'ютери та інтернет наберуть таку величезну популярність і поширеність. Власне тоді і виникла потреба в кодуванні тексту. В якому то ж вигляді потрібно було зберігати дані в комп'ютер, а він (комп'ютер) тільки одиниці і нулі розуміє. Так була розроблена одне-байтове кодування ASCII (швидше за все вона не перша кодування, але вона найбільш поширена і показова щодо цього її будемо вважати за еталонну). Що вона з себе представляє? Кожен символ в цьому кодуванні закодований 8-ма бітами. Нескладно порахувати, що виходячи з цього кодування може містити 256 символів (вісім біт, нулів або одиниць 28=256).

Перші 7 біт (128 символів 27=128) в цьому кодуванні були віддані під символи латинського алфавіту, керуючі символи (такі як переноси рядків, табуляція і т. д.) і граматичні символи. Решта відводилися під національні мови. Тобто вийшло, що перші 128 символів завжди однакові, а якщо хочеш закодувати свій рідну мову будь ласка, використовуй залишилася ємність. Власне так і з'явився величезний зоопарк національних кодувань. І тепер самі можете уявити, от наприклад я перебуваючи в Росії беру і створюю текстовий документ, у мене за замовчуванням він створюється в кодуванні Windows-1251 (російська кодування використовується в ОС Windows) і відсилаю його комусь, наприклад в США. Навіть те, що мій співрозмовник знає російську мову, йому не допоможе, тому що відкривши мій документ на своєму комп'ютері (у редакторі з дефолтної кодуванням тієї ж самої ASCII) він побачить не російські літери, а кракозябры. Якщо бути точніше, то ті місця в документі які я напишу англійською відобразяться без проблем, тому що перші 128 символів кодування Windows-1251 і ASCII однакові, але от там де я написав російський текст, якщо він у своєму редакторі не вкаже правильну кодування будуть у вигляді кракозябр.

Думаю проблема з національними кодуваннями зрозуміла. Власне цих національних кодувань стало дуже багато, а інтернет став дуже широким, і в ньому кожен хотів писати на своїй мові і не хотів щоб його мова виглядав як кракозябры. Було два виходи, вказувати для кожної сторінки кодування, або створити одну загальну для всіх символів в світі таблицю символів. Переміг другий варіант, так створили таблицю символів Unicode.

Невеликий практикум ASCII


Можливо здасться элементарщиной, але раз вже вирішив пояснювати все і детально, то це треба.

Ось таблиця символів ASCII:



Тут маємо 3 колонки:

  • номер символу в десятковому форматі
  • номер символу в шістнадцятковому форматі
  • уявлення самого символу.

Отже, можна закодувати рядок «ok» (англ.) в кодуванні ASCII. Символ «o» (англ.) має позицію 111 в десятковому вигляді та 6F в шістнадцятковому. Переведемо це в двійкову систему — 01101111. Символ «k» (англ.) — позиція 107 в десятеричной і 6B в шістнадцятковій, переводимо в двійкову — 01101011. Разом рядок «ok» закодована в ASCII буде виглядати так — 01101111 01101011. Процес декодування буде зворотний. Беремо по 8 біт, переводимо їх у 10-ичную кодування, отримуємо номер символу, дивимося за таблицею що це за символ.

Unicode


З передумовами створення загальної таблиці для всіх у світі символів, розібралися. Тепер, власне, до самої таблиці. Unicode — саме ця таблиця є (це не кодування, а саме таблиця символів). Вона складається з 1 114 112 позицій. Більшість цих позицій поки не заповнені символами, так що навряд чи знадобиться це простір розширювати.

Розділене це загальний простір на 17 блоків, за 65 536 символів у кожному. Кожен блок містить свою групу символів. Нульовий блок — базовий, там зібрані найбільш вживані символи всіх сучасних алфавітів. У другому блоці знаходяться символи вимерлих мов. Є два блоки відведені під приватне використання. Більшість блоків поки що не заповнені.

Загальна ємність символів юнікоду становить від 0 до 10FFFF (в шістнадцятковому вигляді).

Записуються символи в шістнадцятковому вигляді з приставкою «U+». Наприклад перший базовий блок включає в себе символи від U+0000 до U+FFFF (від 0 до 65 535), а останній сімнадцятий блок від U+100000 до U+10FFFF (від 1 048 576 до 1 114 111).

Відмінно тепер замість зоопарку національних кодувань, у нас є всеосяжна таблиця, в якій всі зашифровані символи, які нам можуть стати в нагоді. Але тут теж є свої недоліки. Якщо раніше кожен символ був закодований одним байтом, то тепер він може бути закодований різною кількістю байтів. Наприклад, для кодування всіх символів англійського алфавіту з раніше досить одного байта наприклад той же символ «o» (англ.) має в юнікод номер U+006F, тобто той самий номер як і в ASCII — 6F в шістнадцятковій та 111 в десятеричной. А ось для кодування символу "U+103D5" (це древнеперсидская цифра сто) — 103D5 в шістнадцятковій і 66 517 в десятеричной, тут нам потрібно вже три байти.

Вирішити цю проблему вже повинні юнікод-кодування, такі як UTF-8 і UTF-16. Далі мова піде про них.

UTF-8


UTF-8 є юнікод-кодування змінної довжини, з допомогою якої можна уявити будь-який символ юнікоду.

Давайте детальніше про змінну довжину, що це означає? Насамперед треба сказати, що структурної (атомарної) одиницею цієї кодування є байт. То що кодування змінної довжини, означає, що один символ може бути закодований різною кількістю структурних одиниць кодування, тобто різною кількістю байтів. Так наприклад латиниця кодується одним байтом, а кирилиця двома байтами.

Трохи відступлю від теми, треба написати про сумісність ASCII і UTF


То що латинські символи та основні керуючі конструкції, такі як переноси рядків, табуляції і т. д. закодовані одним байтом робить utf-кодування сумісними з кодуваннями ASCII. Тобто фактично латиниця та керуючі конструкції знаходяться на тих же самих місцях як в ASCII, так і в UTF, і те що вони закодовані і там і там одним байтом і забезпечує цю сумісність.

Давайте візьмемо символ «o»(англ.) з прикладу про ASCII вище. Пам'ятаємо що в таблиці ASCII символів він знаходиться на 111 позиції, у растровому вигляді це буде 01101111. У таблиці unicode цей символ U+006F що в растровому вигляді теж буде 01101111. І тепер так, як UTF — це кодування змінної довжини, то в ній цей символ буде закодований одним байтом. Тобто подання даного символу в обох кодуваннях буде однаково. І так для всього діапазону символів від 0 до 128. Тобто якщо ваш документ складається з англійського тексту, то ви не помітите різниці якщо відкриєте його і в кодуванні UTF-8 і UTF-16 і ASCII, і так до моменту поки ви не почнете працювати з національним алфавітом.

Порівняймо на практиці як буде виглядати фраза «Hello світ» в трьох різних кодуваннях Windows-1251 (російська кодування), ISO-8859-1 (кодування західно-європейський мов), UTF-8 (юнікод-кодування). Суть даного прикладу полягає в тому, що фраза написана на двох мовах. Подивимося, як вона буде виглядати в різних кодуваннях.


В кодуванні ISO-8859-1 немає таких символів «м», «і» та «р».

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

Будемо вважати, що спочатку фраза була записана в кодуванні Windows-1251. Виходячи з таблиці вище запишемо цю фразу в двійковому вигляді, в кодуванні Windows-1251. Для цього нам буде потрібно всього лише перевести з десятеричной або шістнадцятковій системи (з таблиці вище) символи в двійкову.

01001000 01100101 01101100 01101100 01101111 00100000 11101100 11101000 11110000
Відмінно, ось це і є фраза «Hello світ» в кодуванні Windows-1251.

Тепер уявімо що ви маєте файл з текстом, але не знаєте, в якому кодуванні цей текст. Ви припускаєте що він в кодуванні ISO-8859-1 і відкриваєте його у своєму редакторі в цьому кодуванні. Як сказано вище з частиною символів все в порядку, вони є в цьому кодуванні, і навіть знаходяться на тих же місцях, але от з символами з слова «мир» все складніше. Цих символів в цьому кодуванні немає, а на їх місцях в кодуванні ISO-8859-1 знаходяться зовсім інші символи. А конкретно «м» — позиція 236, «і» — 232. «р» — 240. І на цих позиціях в кодуванні ISO-8859-1 знаходяться наступні символи позиція 236 — символ "м", 232 — "е", 240 — "ð"

Значить фраза «Hello світ» закодована в Windows-1251 і відкрита в кодуванні ISO-8859-1 буде виглядати так: «Hello ìèð». Ось і виходить, що ці дві кодування сумісні лише частково, і коректно перекодувати рядок з одного кодування в іншу не вийде, тому що там просто напросто немає таких символів.

Тут і будуть необхідні юнікод-кодування, а конкретно в даному випадку розглянемо UTF-8. То що символи можуть бути закодовані різними кількістю байтів від 1 до 4 ми вже з'ясували. Тепер варто сказати, що з допомогою UTF можуть бути закодовані не тільки 256 символів, як у двох попередніх, а вобще всі символи юнікоду

Працює вона наступним чином. Перший біт кожного байта коду символ відповідає не за сам символ, а за визначення байта. Тобто наприклад, якщо ведучий (перший) нульовий біт, то це означає, що для кодування символу використовується всього один байт. Що і забезпечує сумісність з ASCII. Якщо уважно подивіться на таблицю символів ASCII то побачите що перші 128 символів (англійський алфавіт, керуючі символи і знаки пунктуації якщо їх привести до двійковому вигляді, всі починаються з нульового байта (будьте уважні, якщо будете переводити символи в двійкову систему за допомогою онлайн наприклад конвертера, то перший нульовий провідний байт може бути відкинутий, що може збити з пантелику).

01001000 — перший біт нуль, значить 1 байт кодує 1 символ -> «H»

01100101 — перший біт нуль, значить 1 байт кодує 1 символ -> «e»

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

Для двобайтових символів перші три біти повинні бути такі — 110

11010000 10111100 — на початку 110, значить 2 байта кодують 1 символ. Другий байт в такому випадку завжди починається з 10. Разом відкидаємо керуючі біти (початкові, які виділені червоним і зеленим) і беремо все решта (10000111100), переводимо їх в шістнадцятковий вид (043С) -> U+043C в юнікод одно символ «м».

для трьох байтовий символів в першому байті провідні біти — 1110

11101000 10000111 101010101 — підсумовуємо всі крім керуючих бітів і отримуємо що в 16-ричной одно 103В5, U+103D5 — древнеперситдская цифра сто (10000001111010101)

для трьох байтовий символів в першому байті провідні біти — 11110

11110100 10001111 10111111 10111111 — U+10FFFF це останній допустимий символ у таблиці unicode (100001111111111111111)

Тепер, при бажанні, можемо записати нашу фразу в кодуванні UTF-8.

UTF-16


UTF-16 також є кодування змінної довжини. Головна її відмінність від UTF-8 складається в тому, що структурною одиницею в ній є не один а два байти. Тобто в кодуванні UTF-16 будь-символ юнікоду може бути закодований або двома або чотирма байтами. Давайте для зрозумілості надалі пару таких байтів я буду називати кодової парою. Виходячи з цього будь-який символ юнікоду в кодуванні UTF-16 може бути закодований небудь однієї кодової парою, або двома.

Почнемо з символів, які кодуються одній кодовій парою. Легко порахувати що таких символів може бути 65 535 (2в16), що повністю збігається з базовим блоком юнікоду. Всі символи, які знаходяться у цьому блоці юнікоду в кодуванні UTF-16 будуть закодовані одній кодовій парою (двома байтами), тут все просто.

символ «o» (латиниця) — 00000000 01101111
символ «M» (кирилиця) — 00000100 00011100

Тепер розглянемо символи за межами базового юнікод діапазону. Для їх кодування потрібно вже дві кодові пари (4 байти). І механізм їх кодування трохи складніше, давайте по порядку.

Для початку введемо поняття сурогатної пари. Сурогатна пара — це дві кодові пари використовуються для кодування одного символу (разом 4 байта). Для таких сурогатних пар у таблиці unicode відведено спеціальний діапазон від D800 до DFFF. Це означає, що при перетворенні кодової пари з байтового виду в шістнадцятковий ви отримуєте число з цього діапазону, то перед вами не самостійний символ, а сурогатна пара.

Щоб закодувати символ з діапазону 10000 10FFFF (тобто символ для якого потрібно використовувати більше однієї кодової пари) потрібно:

  1. з коду символу відняти 10000(шіснадцяткове) (це найменше число з діапазону 10000 10FFFF)
  2. в результаті першого пункту буде отримано число не більше FFFF, що займає до 20 біт
  3. провідні 10 біт з отриманого числа підсумовуються D800 (початок діапазону сурогатних пар в юнікод)
  4. наступні 10 біт підсумовуються DC00 (теж число з діапазону сурогатних пар)
  5. після цього вийдуть 2 сурогатні пари по 16 біт, перші 6 біт в кожній такій парі відповідають за визначення того, що це сурогат,
  6. десятий біт у кожному сурогаті відповідає за його порядок якщо це 1, то це перший сурогат, якщо 0, то другий

Розберемо це на практиці, думаю стане зрозуміліше.

Для прикладу зашифруем символ, а потім розшифруємо. Візьмемо древнеперсидскую цифру сто (U+103D5):

  1. 103D5 10000 = 3D5
  2. 3D5 = 0000000000 1111010101 (провідні 10 біт вийшли нульові наведемо це до шестнадцатиричному числа, отримаємо 0 (перші десять), 3D5 (другі десять))
  3. 0 + D800 = D800 (1101100000000000) перші 6 біт визначають число з діапазону сурогатних пар десятий біт (праворуч) нульовий, значить це перший сурогат
  4. 3D5 + DC00 = DFD5 (1101111111010101) перші 6 біт визначають число з діапазону сурогатних пар десятий біт (праворуч) одиниця, значить це другий сурогат
  5. загальна даний символ в UTF-16 — 1101100000000000 1101111111010101

Тепер навпаки раскодируем. Припустимо що у нас є ось такий код — 1101100000100010 1101111010001000:

  1. переведемо в шістнадцятковий вигляд: = D822 DE88 (обидва значення з діапазону сурогатних пар, значить перед нами сурогатна пара)
  2. 1101100000100010 — десятий біт (праворуч) нульовий, значить перший сурогат
  3. 1101111010001000 — десятий біт (праворуч) одиниця, значить другий сурогат
  4. відкидаємо по 6 біт відповідають за визначення сурогату, отримаємо 0000100010 1010001000 (8A88)
  5. додаємо 10000 (менше число сурогатного діапазону) 8A88 + 10000 = 18A88
  6. дивимося в таблиці unicode символ U+18A88 = Tangut Component-649. Компоненти тангутського листи.

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

Ось деякі цікаві посилання по темі:
habr.com/ru/post/158895 — корисні загальні відомості з кодуванням
habr.com/ru/post/312642 — про юнікод
unicode-table.com/ru — сама таблиця символів юнікод

Ну і власне куди ж без неї
ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4 юнікод
ru.wikipedia.org/wiki/ASCII — ASCII
ru.wikipedia.org/wiki/UTF-8 — UTF-8
ru.wikipedia.org/wiki/UTF-16 — UTF-16

Source: habr.com

Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.