Перейти до змісту

Кодування текстової інформації

Коли ви кажете "текст", ви напевне маєте на увазі "букви та інші символи на екрані мого комп’ютера". Але комп’ютери не працюють з символами, вони працюють з бітами і байтами. Будь-який текст який ви бачите насправді зберігається в певному кодуванні. Говорячи дуже грубо, кодування символів — це бінарне відношення між зображенням символів які ви бачите на екрані, і даними які комп’ютер насправді зберігає в пам’яті та на диску. Існує багато різноманітних кодувань символів, деякі з них оптимізовані для конкретних мов, наприклад англійської, китайської або української, а інші можуть використовуватись в багатьох мовах.

Насправді все ще трохи складніше. Багато символів є спільними для різних кодувань, але кожне кодування використовує іншу послідовність біт, для збереження тих символів в пам’яті чи на диску. Тому ви можете думати про кодування тексту, як про певний вид криптографічного ключа. Як тільки хтось дає вам послідовність байт — файл чи веб-сторінку, не важливо — і стверджує що це "текст", вам потрібно знати яке кодування вони використали, щоб мати змогу перетворити байти в символи. Якщо вони передали вам неправильний ключ, або взагалі ніякого ключа, задача розшифровки тих даних залишається для вас, і я вам не заздрю. Існує велика ймовірність що ви виберете неправильне кодування, і отримаєте абракадабру.

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

Існує кодування символів для кожної основної мови в світі. Так як всі мови різні, і пам’ять та дисковий простір історично були дорогими, кожне кодування символів оптимізується під конкретну мову. Під цим я маю на увазі, що кожне кодування що використовувало одні і ті ж цифри (0 - 255) для позначення символів тієї мови. Наприклад, ви напевне знайомі з кодуванням ASCII яке зберігає символи англійської мови, як числа що знаходяться між 0 та 127. (65 велике “A”, 97 маленьке “a”, і т.п.) Англійська має дуже простий алфавіт, тому він може бути повністю записаний менш ніж 128 числами. Якщо хтось із вас знає двійкову систему числення, для цього потрібно 7 з восьми біт в байті.

Західноєвропейскі мови, такі як Французька, Іспанська та Німецька мають більше символів ніж Англійська. Або, більш точно, вони мають букви, які комбінуються з різноманітними діакритичними знаками, такі як наприклад символ "ñ" в Іспанській. Найтиповішим кодуванням для цих мов є CP-1252, також відоме як “windows-1252” тому що воно широко використовується на платформі Microsoft Windows. Кодування CP-1252 в діапазоні 0-127 має такі ж символи як і ASCII, та потім доповнює їх в діапазоні 128-255 такими символами як n-з-тильдою-зверху (241), u-з-двома-крапками-зверху (252), і т.п. Це все ще однобайтове кодування, символ з найбільшим кодом, 255, все ще можна записати в одному байті.

Крім того, є ще такі мови як Китайська, Японська та Корейська, які мають так багато символів що потребують багатобайтових кодувань. Тобто кожен символ в них закодовано двобайтовим числом від 0 до 65535. Але різні багатобайтові кодування все ще мають таку ж спільну проблему як і однобайтові — вони використовують однакові числа для запису різних символів. Просто діапазон кодів став ширшим, тому що символів які потрібно закодувати набагато більше.

Це було майже нормальним в світі без мережі, де "текст" — це щось що ви самостійно набрали, і вряди-годи друкували. Не було багато "звичайного тексту". Код програм записувався в ASCII, а всі решта використовували текстові процесори, які описували свої власті (нетекстові) форми, які зберігали інформацію про кодування разом з оформленням та іншими даними. Люди відкривали ці документи тими ж текстовими процесорами що й автор, тому все більш-менш працювало.

Тепер подумайте про зростання глобальних мереж, таких як пошта, чи веб. Багато "звичайного тексту" літає навколо світу, створюючись на одному комп’ютері, і пересилаючись за допомогою другого комп’ютера на третій. Комп’ютери можуть бачити тільки числа, але числа можуть означати різні речі. О ні! Що робити? Ну, системи повинні створюватись так, щоб зберігати інформацію про кодування з кожним шматком "звичайного тексту". Пам’ятайте, це ключ до розшифрування чисел які читає комп’ютер, і перетворення їх в букви які можуть читати люди. Загублений ключ розшифрування означає спотворений текст, абракадабру, чи ще щось гірше.

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

Тепер подумайте про можливість існування багатомовних документів, де символи з кількох мов використовуються поряд. (Підказка: програми які намагались працювати з такими документами зазвичай використовували спеціальні символи для перемикання "режимів". Опа, ви в кириличному кодуванні koi8-r, тому 241 означає Я; опа, тепер ви в грецькому кодуванні для Mac тому, 241 означає ώ.) І звичайно ви теж колись захочете робити пошук по таких документах.

Тепер багато плачте, тому що все що ви знали про текст — неправда. Не існує такої штуки як "простий текст".

З’являється Юнікод.

Юнікод — система, створена для запису кожного символа кожної мови. Юнікод представляє кожну букву, символ чи ідеограму як чотирибайтове число. Кожне число задає унікальний символ якої-небудь мови. (Не всі числа використовуються, але більш ніж 65535 з них так, тому двох байтів не достатньо). Символи, що використовуються в багатьох мовах зазвичай мають один номер, якщо не існує достатньої етимологічної причини для протилежного. Кожному символу відповідає одне число, і кожному числу відповідає один символ. Кожне число завжди означає одне і те ж, бо немає "режимів" за якими потрібно слідкувати. U+0041 - завжди 'A', навіть якщо мова вашого тексту не містить цієї літери.

На перший погляд це виглядає як чудова ідея. Одне кодування щоб керувати ними всіма. Багато мов в одному документів. Більш ніяких "змін режиму" щоб змінювати кодування посеред файла. Але в вас має виникнути законне запитання. Чотири байти? На кожен символ‽ Це виглядає страшенно витратно, особливо для таких мов як англійська чи іспанська, які потребують менше одного байта (256 символів) щоб виразити всі можливі символи мови. Насправді це марнотратство і в ієрогліфічних мовах (таких як китайська), яким ніколи не стане потрібно більше ніж два байти на символ.

Існує кодування Юнікоду яке використовує чотири байти на символ. Воно називається UTF-32, тому що 32 біти це і є 4 байти. UTF-32 — пряме кодування. Воно бере кожен номер символу Юнікоду, і записує його в цих чотирьох байтах. Це має певні переваги, найважливішою з яких є те, що ви можете знайти n-тий символ рядка за константний час, тому що n-тий символ починається з (4*n)-того байта. Це також має кілька недоліків, найочевиднішим з яких є те, що щоб зберегти кожен чортів символ потрібно аж чотири чортових байти.

І хоча є страшенно багато символів Юнікоду, виявляється що більшість людей майже ніколи не використовують символів поза першими 65535. Тому, існує інше кодування Юнікоду, назване UTF-16 (тому що 16 бітів = 2 байти). UTF-16 кодує кожен символ від 0 до 65535 двома байтами, після чого використовує деякі брудні хаки в випадках коли вам потрібно записати рідковживані символи юнікоду з "астрального простору" поза 65535-тим символом. Найбільш очевидна перевага: UTF-16 займає вдвічі менше місця ніж UTF-32, тому що кожен символ потребує лише двох байт місця замість чотирьох (звісно окрім тих які таки потребують). І ви все ще можете просто знаходити n-тий символ за константний час, якщо припустите, що рядок не містить жодних символів з "астрального простору", що є гарним припущенням аж поки воно не стає невірним.

Але існує також не такий очевидний недолік, як кодування UTF-32 так і UTF-16. Різні комп’ютерні системи зберігають окремі байти різними способами. Це означає що символ U+4E2D може зберігатись в UTF-16 як 4E 2D та як 2D 4E, залежно від порядку байтів в системі. (Для UTF-32 взагалі можливі чотири різні порядки запису). Поки ваші документи ніколи не покидають вашого комп’ютера, ви в безпеці — різні додатки одного і того ж комп’ютера будуть використовувати один і той самий порядок байтів. Але якщо ви захочете передати документи між системами, чи можливо у всесвітню мережу, вам потрібно буде якимось способом вказати порядок байт вашого кодування. Інакше система що отримує дані не буде знати чи двобайтова послідовність 4E 2D означає U+4E2D U+2D4E.

Щоб розвє’язати цю проблему, багатобайтові кодування Юнікоду описують "Мітку порядку байт" (англ. Byte order mark), яка є спеціальним недрукованим символом який ви можете вставити на початку свого документа щоб вказати в якому порядку розміщені його байти. Для UTF-16 мітка порядку байт — U+FEFF. Якщо ви отримуєте документ, що починається з байт FF FE, ви знаєте що байти йдуть в одному порядку, якщо FE FF - в протилежному.

І все ж, UTF-16 не ідеальна, особливо коли вам доводиться мати справу з великою кількістю символів ASCII. Якщо подумати, то навіть китайська веб-сторінка міститиме багато ASCII символів — елементи та атрибути HTML що оточуватимуть друковані китайські ієрогліфи. Здатність знайти n-тий символ за одиничний час — це добре, але все ще залишається надокучлива проблема тих символів з астральної площини, яка означає що ви не можете гарантувати те що кожен символ — це рівно два байти, тому ви насправді не можете знайти n-тий символ за константний час, якщо звісно не заведете окремий індекс. І звичайно на світі море тексту ASCII...

Інші люди задумувались над цими питаннями і прийшли до рішення:

UTF-8

UTF-8 — система кодування Юнікоду змінної довжини. Тобто, різні символи можуть займати різну кількість байт. Для символів ASCII (A-Z, і т.п.) UTF-8 використовує лише один байт на символ. Насправді вона навіть використовує точно такі ж байти як і в ASCII: перші 128 кодів символів (0-127) в UTF-8 не відрізняються від кодів тих самих символів в ASCII. "Розширені латинські" символи, такі як ñ та ö, і кирилиця займають два байти. (Байти — це не простий номер символа в таблиці Юнікоду, а хитрим чином закодований). Китайські символи, такі як 中, займають три байти. Рідко використовувані символи з "астральної площини" займають чотири байти.

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

Переваги: дуже ефективне кодування стандартних символів ASCII. Не гірше ніж UTF-16 для кирилиці і розширеної латиниці. Краще ніж UTF-32 для китайських ієрогліфів. Також (і ви не мусите мені тут вірити, тому що я не збираюсь показувати вам математику), через природу необхідних операцій з бітами пропадає проблема порядку байт. Документ записаний в UTF-8 використовує однакову послідовність байт на всіх комп’ютерах.

Текст в Python

В Python 3 всі рядки є послідовностями символів Юнікоду. В Python не існує такої штуки, як рядок закодований в UTF-8, чи рядок закодований як cp1251. "Це рядок в UTF-8?" — неправильне запитання. UTF-8 — спосіб кодування символів як послідовностей байт. Якщо ви хочете взяти рядок і перетворити його на послідовність байт в певному кодуванні, Python 3 може допомогти в цьому. Якщо ви хочете взяти послідовність байт, і перетворити в послідовність символів Python може допомогти і в цьому. Байти не є символами: байти це байти. Символи — це абстракція. Рядок — послідовність таких абстракцій.