A Unicode reprezentációk

A legtöbb esetben a gyakorlatban nem szükséges a karakter logikai szintje alá menni, mivel a karakterek reprezentációjának problémája hosszú távra rendezve lett. Ezért is fontos megérteni az elmúlt 200 évben bekövetkezett, a tárolt karakterek számában történt drasztikus léptékváltás mögötti vállalkozás nagyságát (Braille-írás (1821) és Morse-kód (1840) 64, ASCII (1963) 128, Latin kódtáblák (1987) 4096, Unicode 15.1 (2023) 149 813 darab).

Többé már nem fér el a reprezentáció egy bájton, így ki kellett találni, hogy hogyan lehet hatékonyan kezelni több bájtot úgy, hogy a bitek sorrendje architektúránként (big endian, little endian típusú számítógépek lásd A bitek sorrendje a széles karakterekben (big endian, little endian és a byte order mark) rész) eltérhet. Nem a Unicode volt az első karakterkészlet, amely erre vállalkozott, de a Unicode karakterkészletben válik szét először az egyes karakterek logikai, átviteli és tárolási reprezentációja, melyeket eltérő problémaként kezel. Ez kiemeli a többi karakterkódolás közül, érdemes tehát tisztában lenni a döntés hátterével és a napi életre gyakorolt hatásával. A fejezet további részében ezért a különféle reprezentációk közötti konverzió alapjairól és a reprezentációk gyakorlati tulajdonságairól lesz szó.

Élet egy bájton túl (a széles karakterek és az UTF-16/32)

A számítógépes feldolgozás hagyományos egysége a bit (0 vagy 1-es érték), amit történelmi okokból 8-asával bájtokba rendezünk, és a bájtokat úgynevezett szóvá (két bájt, 16bit), duplaszóvá (4 bájt, 32bit), valamint a modern processzorokon négyesszóvá (8 bájt, 64 bit) tudjuk összerakni az egységek méretét duplázva. Ekkora méretekben címezhető a memória az explicit memóriakezelésű nyelvekben, mint a C/C++. A Latin kódtáblák tapasztalataiból tudjuk, hogy a Latin kódtáblacsalád 16*128 = 4096 karaktert akar kódolni (az ASCII 128 karaktere + 15-ször a felső 128 elem, lásd Az ISO 8859 család rész). Ez 11 biten leírható, de nem használható minden karakter egyszerre. A Unicode eközben 21 bitet használ nagyságrendekkel több karakter egyidejű megcímkézésére (de a jelenleg kiosztott karakterek kényelmesen elférnek 18 biten, tehát bőven van még hely).

Összehasonlításképpen az Internet alapját képező IPv4 szabvány 32 bites, és kevésnek bizonyult a világ összes számítógépének megcímzéséhez, ezért bevezették az IPv6-ot, amely 128 bites, és lehetővé teszi, hogy minden számítógépnek akár több címe is legyen a jövőbeli növekedési ütemet figyelembe véve is. A számítógépeken az időt leíró Unix time_t adattípus, amely 1970. január 1. 00:00:00-tól, a 0 ponttól (epoch) másodpercenként egyet növekedve reprezentálja az időt, 32 bites, de csak 2038. január 19. 03:14:07-ig tartanak ki a bitek, így nemrégiben növelték a méretét 64 bitre. A modern processzorok először 8, majd 32 (1995) és manapság 64 bitesek (1999-től kezdve), manapság a grafikus processzorok 128 és 256 bitesek, de a hagyományos processzorok praktikus okokból megmaradtak 64 bitnél. Látható tehát, hogy a bitek számának problémája nem csak a karakterkódolást érinti, és nem is ez a legnagyobb ilyen jellegű probléma. A méretválasztás (és -változtatás) elsősorban a jelenlegi, másodsorban a jövőbeli rendszerekkel való kompatibilitásnak van alárendelve.

A Unicode 1.0 idejében (1991) egyes processzorok már be tudtak fogadni 32 bitet, de számolni kellett a 16 bites processzorok és a 8 és 16 bites programok tömegével, ami meghatározta, hogy hány bitben lehetett a kódoláskor gondolkodni. Bár a régi programok és processzorok eltűntek, a kódtábla mérete velünk maradt, és valószínűleg az utána jövő rendszereket is meg fogja határozni, ahogy ezt az ASCII is tette.

A Unicode 1.0 szabvány a processzorok bitjeinek szélességéhez igazodva maradt tehát a két bájtos UCS-2-nél, azaz az úgynevezett széles karaktereknél (wide character), mely később önmagával kompatibilis módon át lett alakítva a (két bájt széles karaktert felhasználó ún. surrogate pair-okkal) több karaktert reprezentálni tudó UTF-16-ra. Sajnos egyik változat sem kompatibilis az ASCII-val, és nem képes az összes jelenlegi Unicode karaktert reprezentálni. A széles karakterek rendszere egy nagyon innovatív elképzelés volt, melynek vannak előnyei és hátrányai: a (kétbájtos) fix bájtszélesség azért jó, mert így az adat ismerete nélkül is meg lehet mondani, hogy hol kezdődik, és hol ér véget az n-edik karakter a stringben (n*2 bájt és +1 a karakter vége), nem vágódhatnak „ketté” véletlenül karakterek bájtműveleteknél, viszont a bitek sorrendje architektúránként eltérhet (lásd A bitek sorrendje a széles karakterekben (big endian, little endian és a byte order mark) rész).

Később megjelent az UTF-32, mely 4 bájtot foglal, és bár az ASCII-val még mindig nem kompatibilis, szemben az UTF-16-tal képes reprezentálni az összes Unicode karaktert, ámbár igen pazarló módon (elméletileg is csak 21 bitnyi információ egységesen 32 biten tárolva). A megjelenésekor még nagyon szűkösek voltak a tárolókapacitások, különösen a gépek memóriája. Ezért, valamint az akkoriban nagyon elterjedt ASCII-val való inkompatibilitása miatt nem váltotta be a hozzá fűzött reményeket. Manapság az UTF-32 a programok belső reprezentációjaként használatos, ott, ahol fontos, hogy a karakterek a fix bájtszélesség előnyével rendelkezzenek (indexelés, stringműveletek), és minden Unicode karakter reprezentálható legyen (a memóriaigény rovására).

A bitek sorrendje a széles karakterekben (big endian, little endian és a byte order mark)

A különböző számítógép architektúrák között nincs egyetértés abban, hogy a memóriában elölről hátra (big endian) vagy hátulról előre (little endian) olvassák a biteket. A jelenleg használt számítógépek kevés kivétellel little endian típusúak, de ennek egyre kevesebb a jelentősége. Egy számítógépen belül nem tud összekeveredni a két reprezentáció, és az UTF-16BE és UTF-16LE (valamint az UTF-32BE és UTF-32LE) szabvány képes leírni a két állapotot egymástól függetlenül.

../_images/0023_en_big-endian.png

„SUOY BDIA NE-GI NAID ?” A szavak karaktereit hátulról előre olvasva: YOUS AIDB IG-EN DIAN ?” azaz „YOU SAID BIG-ENDIAN ?” (forrás: 0xbabaf000l.blogspot.com)

Az Internet elterjedésével viszont valahogy meg kellett egyeznie a gépeknek a bitek sorrendjéről az átküldendő adatot illetően. Erre az esetre vezették be az úgynevezett byte order mark-ot (BOM), amely egy speciális szóközkarakternek felel meg („nulla hosszú nem törő szóköz”, „angolul zero width no-break space”, ZWNBSP). Ezt küldték át a bemenet első karaktereként, jelezve a fogadó félnek, hogy bitfordításra lesz szükség a többi karakter esetében – mivel rossz sorrendben olvasva a biteket, az nem eredményez helyes karaktert. Ilyenkor a helyes bitsorrendtől függetlenül az első karakter nem az üzenet része, hanem eldobandó, mivel csak az átvitelt segíti.

A gyakorlatban néha előfordul az a hiba, hogy mégsem dobódik el, és két string a BOM-al együtt összefűződik. Ekkor a BOM vagy speciális szóközként értelmeződik, vagy nem helyes karakterként, ami később váratlan hibát okozhat. A Windows Jegyzettömbje például, amikor UTF-8 kódolással menti a fájlokat, hozzáadja a BOM karaktert, ami néhány program esetében zavart okozhat. Erről részletesen a UTF-8, a több bájtos, nem fix szélességű karakterkódolás részben lesz szó.

UTF-8, a több bájtos, nem fix szélességű karakterkódolás

Nem sokkal a Unix megalkotása után egyik találkozójukon, vacsora közben Ken Thompson és Robert Pyke rájött, hogyan lehetne karaktereket megfelelően reprezentálni úgy, hogy egy karaktert több bájton lehessen tárolni, viszont a bitsorrend ne jelentsen problémát a rendszerek közötti feldolgozásban, ráadásul kompatibilis maradjon az ASCII kódtáblával. Az UTF-8 először így került leírásra egy alátétre (placemat).

A mára de facto alapértelmezett megoldás okosabb, mint az elődei, de azon az áron, hogy változó bájthosszúságon reprezentálja az egyes Unicode karaktereket. Ennek következménye, hogy az egyes karakterek megkülönböztetése csak a stringnek az adott karakterig történő elolvasásával tehető meg, mert máskülönben nem lehet előre meghatározni az n-edik karakter helyét.

A UTF-8 minden karaktert, amely az ASCII táblázatban benne van, egy nullával, majd az ASCII 7-bites megjelölésével reprezentál, mivel az ASCII kódtáblában a nyolcadik bit értéke nem definiált (ezt X-szel jelöljük a táblázatban):

ASCII (1 + 7-bit)

UTF-8 (8-bit)

A

X1000001

01000001

a

X1100001

01100001

!

X0100001

00100001

6

X0110110

00110110

Így az eredeti angol ábécé a lehető legkevesebb helyet foglalja egy bájtokra épülő rendszerben, és a UTF-8 kompatibilis minden régebbi, az ASCII-ra épülő kódrendszerrel (lásd Az ISO 8859 család rész) a 7-bites ASCII karakterek értelmezésében. Ezt pedig a nyolcadik bit nullára állításával jelzi.

Ha ennél több helyre van szükség, akkor a UTF-8 a következő konvenciót követi: a bitfolyamban (bit stream) ha egyessel kezd egy (8-bites) bájtot (pl. 110XXXXX), akkor az azt jelenti, hogy kezdődik egy új karakter. Emellett az első bájt utal arra is, hogy a karakter összesen hány bájt helyet fog elfoglalni. Az első bájt elején szereplő 110 például azt jelzi, hogy a karakter összesen két bájton van kódolva, az első bájt 1110 kezdete pedig a karakter hárombájtos hosszúságára utal. A folytatásként értelmezendő bájtok, például kétbájtos karakter esetén a második, hárombájtos karakter esetén pedig a második és a harmadik bájt mindig 01-gyel kezdődik: ez jelzi, hogy folytatásról van szó, nem pedig egy új karaktert kódoló bájtsorozat kezdetéről. (Illusztráció: Characters, Symbols and the Unicode Miracle - Computerphile):

Első bájt

Második bájt

Harmadik bájt

Ha egybájtos a karakter

0 _ _ _ _ _ _ _

Ha kétbájtos a karakter

1 1 0 _ _ _ _ _

0 1 _ _ _ _ _ _

Ha hárombájtos a karakter

1 1 1 0 _ _ _ _

0 1 _ _ _ _ _ _

0 1 _ _ _ _ _ _

Így minden bájtnál marad olyan bit, amit még nem használtunk fel semmire. Az első bájtnál ez változó, ha például kétbájtos a karakter, azaz 110-val kezdődik az első bájt, akkor ott 5 üres bit marad, de a többi bájtnál fix 6 üres bit áll rendelkezésre. Így két bájton 25+6 = 2 048-ig lehet elszámolni, három bájton 24+6+6 = 65 536-ig, és így tovább.

Mivel a karaktereket a használathoz mindenképp át kell kódolni, az UTF-8 a big endian és little endian rendszerekkel is kompatibilis (lásd A bitek sorrendje a széles karakterekben (big endian, little endian és a byte order mark) rész). A fájlrendszereken tárolt adatok esetén ez a tulajdonság azért jön kapóra, mert ha kivesszük a háttértárat, és áttesszük egy másik architektúrájú számítógépbe, akkor a számítógép a bitsorrendtől függetlenül képes értelmezni a karaktereket. Ez hasonlóan igaz a hálózaton átküldött szöveges adatokra is, aminek az UTF-8 az igazi népszerűségét köszönheti.

Kompatibilitási okokból definiálható UTF-8-ban is a BOM. A Windows 10 2019-es kiadása előtti verzióknál a Jegyzettömb (Notepad) UTF-8 kódolással való mentéskor a program mindenképp belerakja a fájlba a BOM-ot, ami az egyéb programokat megzavarja, mivel UTF-8 esetén nem számítanak BOM-ra. Emiatt a Notepad használata nem ajánlott, helyette például a Notepad++ használata javasolható. A különféle programnyelvekben a beolvasáskor beállítható, hogy számítsanak UTF-8 esetén a BOM-ra, de kész programoknál ez többnyire nem lehetséges.

Melyik reprezentáció mire jó?

Az UTF-16 kompromisszumos megoldásként a mai napig fontos szerepet tölt be, mivel a rendszer korai használói (early adopter), mint például a Java és JavaScript programozási nyelvek, valamint a Windows API-ja (a Windows NT 3.1 első 32 bites kiadása 1993-ban) ezt implementálták, és kompatibilitási okokból azóta sem tudták meglépni a váltást. A Windows XP (2001) óta 65001-es kódlap néven elérhető az UTF-8 kódolás, de nem rendszerszinten. 2018 áprilisa óta a Windows 10 és az azóta megjelent Windows 11 béta állapotban szállítja a rendszerszintű UTF-8 kódolást, amelynek használata a Windows 10-ben sok hibát okoz, ezért nem ajánlott. A régóta létező Unix és az új Linux rendszerek először maradtak a „8-bit tiszta” működésnél (lásd Karakterhivatkozások (entitások) rész) és az ASCII/Latin kódolásoknál, később pedig átálltak az utóbb megjelent, kompatibilis UTF-8-ra, fenntartva a folyamatos kompatibilitást. Az interpretált programnyelvek (mint a Python) belső használatra UTF-32-t használnak, de a külső programokkal (pl. hálózat, háttértár) alapértelmezésben UTF-8-ban, illetve a beállított kódolásban kommunikálnak. Az internetes szöveges protokollok pedig szinte kivétel nélkül átálltak az UTF-8 kódolásra, hiszen kompatibilis az addig használt ASCII-val, helytakarékos, és nem szükséges a karakterek értelmezése (lásd Karakterhivatkozások (entitások) rész), amelyhez kellene az indexelés.

Látható tehát, hogy mindegyik reprezentációnak megvan az ideális felhasználási módja. A hálózati átvitelhez és a lemezen való tároláshoz, ahol a karakterek adatként reprezentálódnak, a helytakarékos UTF-8 használatos. Ezzel szemben a programokon belül, ahol potenciálisan nyelvfüggő műveleteket kell végezni az egyes karaktereken, amihez szükséges az indexelés, az UTF-16 vagy szükség esetén az UTF-32 reprezentáció jellemző. Ezzel a Unicode minden modern használati esetet le tud fedni, és teljesíti a tervezett funkcióját.

A karakterreprezentáció-konverziós hibák és a betűszemét (mojibake)

A sok reprezentáció közötti átalakítás nem mindig zökkenőmentes, de a régi szövegek konverziójakor is előfordulhatnak hibák. Ilyenkor nagyon fontos tudnunk, hogy mire számíthatunk, és mit szeretnénk elérni, hogy elkerüljük a duplán kódolt karakterhivatkozásokat vagy az adatvesztést amiatt, mert egy kevesebb karaktert reprezentálni tudó rendszerbe illesztjük a bemenetünket (pl. ASCII kompatibilis JSON-t készítünk, amely az alapbeállítás Pythonban, lásd Kódtáblafüggetlen karakterhivatkozások előállítása és feloldása rész). Amikor a konverzió nem sikerül, akkor az így keletkezett zavaros karakterhalmazt hívjuk betűszemétnek, angolul „mojibake”-nek. A karakterek bájtokká alakítására és a bájtok karakterekre alakítására a következő beállítások állnak rendelkezésre (lásd A Python hozzáállása a bájtokhoz és karakterekhez rész):

  • strict: Az első nem helyesen kódolt karakter beolvasásánál vagy a kimeneti kódlapon nem kódolható karakter írásánál hibát ad.

  • ignore: (A fenti) hibák esetén a kérdéses karaktert kihagyva folytatja a (de)kódolást. (Mindenképp lefut, de adatvesztés keletkezhet.)

  • replace: (A fenti) hibák esetén a kérdéses karaktert kicseréli „replacement karakterre” (), amely kizárólag a hibásan kódolt karakterek jelzésére fenntartott „extremális” karakter. (Mindenképp lefut, de adatvesztés keletkezhet.)

  • backslashreplace (csak Python): A nem dekódolható bájtokat átalakítja \xNN formára, a nem kódolható karaktereket \uNNNN alakúra.

  • xmlcharrefreplace és namereplace (csak Python, csak kódolásnál): A nem kódolható karaktereket átalakítja XML karakterhivatkozás vagy név szerinti Unicode hivatkozás formátumra.

  • transliterate (csak iconv, csak kódolásnál): A nem kódolható karaktert kicseréli egy hasonló glifájú karakterre, pl. é -> e. (Mindenképp lefut, de adatvesztés keletkezhet.)

  • escape: A nem kódolható karaktereket XML (&…;)/hexadecimális (x1234)/Unicode (uXXXX)/N{…} alakú hivatkozássá alakítja, illetve ezeket visszaalakítja.

A HTML és XML fájlokban szövegesen meg lehet jelölni a dokumentum kódolását, amely kiolvasható ASCII betűkkel, mivel a fájl elején direkt csak ilyen karakterek szerepelnek. Ekkor a feldolgozó program átváltja a kódolást továbbhaladás előtt, vagy újra megnyitja a fájlt helyes kódolással, feltételezve, hogy az adat megfelel a jelzett formátumnak. Furcsa hibákat tud eredményezni, ha a jelzett kódolás és a valódi kódolás eltér, mégis gyakorlatban előfordul ilyen eset is. A hibás konverzió megtörténhet például a különböző Latin kódolások keverésével, vagy pedig egy UTF-8 bájtsorozat Latin kódolásúként való megnyitásával. Ilyenkor mindegyik változat beolvasható, „helyes” (valid) kódolást eredményez, de máshogy konvertálódik és jelenik meg:

Valódi

kódolás

Kódolás,

amiben

megnyitjuk

(deklarált)

Eredmény

Megjegyzés

Latin-1

Latin-2

árvíztűrőtükörfúrógép

magyar ű és ő betűk

Latin-2

Latin-1

árvíztûrõtükörfúrógép

kalapos ű és hullámos ő

Latin-1
/Latin-2

UTF-8

�rv�zt�r�t�k�rf�r�g�p
rvztrtkrfrgp
a dekódolástól függően
(replace vs. ignore)

UTF-8

Latin-1

árvÃxadztűrÅx91tükörfúrógép

mojibake

UTF-8

Latin-2

ĂĄrvĂxadztĹąrĹx91tĂźkĂśrfĂşrĂłgĂŠp

mojibake
(más mint a Latin-1-nél)

A szövegfájlokat karakterhivatkozásokkal tarkítva (pl. JSON) még összetettebb hibákat lehet kapni (lásd Kódolási hibák, furcsaságok rész). Ráadásul az eredményt UTF-8 formátumban mentve észrevétlenül tovább lehet súlyosbítani a helyzetet, hiszen kódolás szempontjából helyes UTF-8 lesz a mojibake adatunk. Szerencsére a fájl utólag is javítható, ha a hibás konverziót visszafelé elvégezzük, és a helyes kódolásban nyitjuk meg a fájlt.

Az ilyen, a kódolás szempontjából helyes, de a felhasználó céljával nem egyező kimeneti esetek számosságából következik, hogy a valódi karakterkódolást 100%-osan nem lehet meghatározni automatikus módszerekkel, de karakterstatisztikák és bitminták segítségével jó hatékonysággal meg lehet tippelni. A chardet könyvtár a de facto sztenderd megoldás a karakterkódolás automatikus felismerésére, és a legtöbb esetben működik is. Ugyanakkor a Latin-2 kódtáblával kódolt magyar nyelv felismerése már évek óta ki van kapcsolva benne, ezért általában török kódtáblának ismeri fel az ilyen szövegeket. Linux alatt elérhetőek az iconv és az icu programok, melyek a fájlok konverzióját egyszerűen el tudják végezni, így megfelelő paraméterek megtalálása esetén a javítás már triviális. Érdekesség, hogy az iconv program megengedő, BSDL licencű implementációját Kövesdán Gábor BME mérnök informatikus hallgató írta meg BSc szakdolgozata részeként.

Előfordulhat, hogy a különböző fájlrendszerekből származó fájlok és mappák nevében található ékezetek szenvednek konverziós hibától. A főbb mai fájlrendszerek alatt használható fájlnévkonvenciók szerint a fájlok neve maximum 127 darab bármilyen UTF-16 (Unicode BMP) kódolás karakter lehet, mely osztályból kivételt képeznek a speciális jelentéssel bíró karakterek és karaktersorozatok például a ., .. és a mappa elválasztó /, más néven fordított törtvonal. A rosszul kódolt fájlnevek tömeges átnevezését segítő program, a convmv, az iconv programhoz hasonlóan csak a bemeneti és kimeneti karakterkódolás megadását igényli. ZIP fájlok esetében előfordul, hogy ha az archivált fájlnevek ékezetet tartalmaznak és nem UTF-8 kódolással vannak kódolva, a fájlok és mappák neveiben az ékezetek elromlanak. Például a Windows tömörített mappa funkciójával tömörített fájlok Linux alatti kibontásánál. Ilyenkor meg kell adni a megfelelő kódtáblát a helyes fájlnevek előállításához (lásd Ékezetes fájlneveket tartalmazó ZIP archívum kitömörítése rész).