Rémtörténet a karakterkódolásokról

Czirkos Zoltán · 2022.08.31.

Ékezetes betűk, szövegek kódolása és megjelenítése a programokban. Az itt tárgyalt dolgoknak nagy részét megoldja a Python nyelv szabványos könyvtára magától, de nem árt tudni, mi történik a háttérben.

18+

Az ékezetes betűk kódolásával máig gondok vannak. Sokféle szabvány létezik arra, hogy mely ékezetes betűt milyen számkóddal jelölünk, ami azért nehéz ügy, mert ezek a kódtáblázatok általában egymással inkompatibilisek.

A probléma ugyan elméletben megoldott, létezik olyan karakterkódolás, a Unicode szabvány részeként, amely a világ (majdnem) összes nyelvének (majdnem) összes írásjelét tartalmazza, mégis rendszeresen találkozunk árvíztûrõ tükörfúrógépekkel (meg ĂĄrvĂ­ztĹąrĹ tükörfúrógépekkel) még nyomtatott szövegekben is. Ennek oka sokszor a programozók figyelmetlensége. A karakterkódolási szabványok követésével és a programok helyes beállításával a problémák megszüntethetőek. Legtöbbször csak egy-két függvényhívásról van szó!

1. Az egybájtos karakterkódolások

Az angol nyelvben használt, ékezet nélküli betűkhöz az ASCII kódolás terjedt el. Erről előadáson is volt szó. Egykor voltak más kódolások is, de a ASCII mára gyakorlatilag egyeduralkodóvá vált.

A nyugat-európai nyelvekhez (pl. a franciához) használják ennek a Latin-1, vagy más néven ISO8859-1-es bővítését. Ez az ASCII kódolás 128 kódját újabb 96 karakterrel egészíti ki a 160-255 tartományban, így ez már 8 bites. Ebben sajnos nincsen benne a magyar ő és ű betű. A testvérében, a Latin-2-ben (ISO8859-2) benne van, így ezzel bármilyen magyar szöveg leírható. Ebben a magyar ű betű helyén a Latin-1-esben û van, az ő helyén pedig õ. Ezért találkozni néha ilyenekkel: árvíztûrõ tükörfúrógép, amikor egy Latin-2 kódolással megadott sztringet Latin-1 kódolásúnak gondol egy program, vagy esetleg egy betűtípusban szerepel helytelenül, hogy melyik „alakzat” (graféma) melyik karaktert is jelenti.

A Latin-2-höz hasonló kódolást használ a Windows a szövegfájloknál (Windows-1250). A konzol ablakban sajnos egy másikat (IBM-852), amely a Latin-1-2-re egyáltalán nem hasonlít. Ezek a kódolások a lenti képeken láthatóak.

ISO8859-1 (Latin-1)
ISO8859-2 (Latin-2)
IBM-852

2. A Unicode kódolás

A többnyelvű szövegek nem írhatóak le a fenti kódolásokkal. Nem csak az a baj, hogy egy cirill vagy japán betűk nem szerepelnek bennük, hanem például még egy latin betűs útikönyvvel is gondban vagyunk! A Latin-1-ben nincs ő, a Latin-2-ben nincs ø, ezért ez a mondat nem írható le egyikkel sem: Dánia fővárosa København.

A '80-as évek vége táján felmerült, hogy létre kellene hozni egy olyan kódtáblát, amely a világ összes nyelvének összes karakterét tartalmazza, mert akkor nem lesz ilyen gond. Ez lett a Unicode szabvány része. Mivel azonban az összes létező írásjelek 256-nál többen vannak, ebben egy karaktert már nem egy bájttal, hanem egy nagyobb számmal jelölnek.

A Unicode szabványban a legtöbb karakter elfér 16 biten, de újabb verziókban akár még nagyobbak lehetnek. Míg pl. az ő betű vagy az € jel ábrázolható 16 bites számmal (karakterkódjuk 337 és 8364), addig más jelekhez, pl. emoji-khoz 216, azaz 65536 feleti szám tartozik (😺 kódja 128570, 🤡 pedig 129313).

Az egybájtos karakterkódokról Unicode-ra átalakítani egy szöveget nagyon könnyű; egy 256 elemű tömbben eltárolhatjuk, melyik kódból mi lesz. Az egyes kódolásokhoz (Latin-1, Latin-2 stb.) azonban eltérő táblázatok tartoznak. A visszaalakítás nem ilyen egyszerű, mert bár technikailag könnyen megvalósítható, könnyen előfordulhat, hogy olyan karaktert kell átkódolni, ami a cél kódtáblában nem létezik. A halmazelméletben használt ∉ „nem eleme” szimbólum például semelyik fenti táblázatban nem létezik.

Fölmerül még egy probléma az egy bájtnál nagyobb számok miatt. Egyes számítógéptípusok úgy tárolják a 16 bites számokat – amelyeket két 8 bites bájtként kell elhelyezni a memóriában –, hogy az alsó 8 bitet írják előbb, és utána a felső 8-at. Vagyis előbb a kicsit (little endian). Más gépek meg épp fordítva, előre veszi a felső 8 bitet, és utána, a következő memóriarekeszbe pedig az alsó 8 bitet (big endian). Ez egészen addig nem gond, amíg két, eltérő típusú számítógépnek nem kell kommunikálnia egymással. Viszont ha ezek az Interneten keresztül adatot küldenének egymásnak, vagy szeretnék olvasni az egymás által kiírt fájlokat, akkor már figyelni kell arra, hogy esetleg nem ugyanazt a bájtsorrendet használják – különben amit az egyik 0xFCE2-nek mond, azt a másik 0xE2FC-nek fogja értelmezni, és fordítva. Nagyobb számok (pl. 32 bites integerek) esetén hasonló a helyzet.

Ezért a Unicode kódolású szövegekben el szoktak helyezni egy ún. BOM (byte order mark, bájtsorrend jele) karaktert, amelynek a kódja 0xFEFF. Ha a szöveget olvasó számítógép egy 0xFEFF kódot talál a szövegben, akkor tudja, hogy annak bájtsorrendje megegyezik a sajátjával. Ha azonban egy 0xFFFE számot lát (amely szándékosan semmilyen karakternek nem kódja), akkor tudja, hogy minden számban meg kell cserélnie a felső és alsó nyolc bitet.

A BOM-mal kiegészített, „HELLO” szöveget tároló fájlok bájtsorrendtől függően így nézhetnek ki (16 bites tömbelemeket feltételezve):

FE FF 00 48 00 45 00 4C 00 4C 00 4F
FF FE 48 00 45 00 4C 00 4C 00 4F 00

3. Az UTF-8 kódolás

A Unicode kódolás elméletben visszafelé kompatibilis az ASCII kódolással, ugyanis az első 128 karaktere ugyanabban a sorrendben van. Azonban a szövegfájlok a bájtsorrend miatt mégsem kompatibilisek egymással. Ezért találták ki az UTF-8 szövegkódolást. Az ilyen szövegekben a Unicode kódszámokat használják, azonban mindig 8 bites értékekből építik fel azt, átalakítva a nagyobb számokat több bájtos sorozatokká. Ha a leírandó kódszám elfér 7 biten (vagyis 0x00 és 0x7F között van), akkor le kell vágni 8 bitre, és úgy tenni a fájlba. Ha ennél nagyobb, akkor kettő, három, sőt néha még több bájtos sorozattal írható le. A bájtok sorrendje azonban az ilyen sorozatokban kötött, tehát nem függ a számítógép típusától. Az átkódolás az alábbi módon helyezi el a biteket:

TartományUnicodeUTF-8
   00-  7F00000000 xxxxxxxxxxxxxxxx
   80-07FF00000yyy yyxxxxxx110yyyyy 1xxxxxxx
 0800-FFFFzzzzyyyy yyxxxxxx1110zzzz 10yyyyyy 1xxxxxxx
10000–......

A Wikipedia az Euró jelét hozza példának, hogyan néz ki egy karakter UTF-8 kódolása:

  • Az € karakter kódszáma 0x20AC.
  • Ez binárisan 0010000010101100, ami a fenti táblázat alapján a harmadik kategóriába esik. Vagyis három bájton lesz kódolható.
  • Az első bájt viszi az első négy bitet: 11100010. A második a következő hatot: 10000010. Az utolsó a maradékot: 10101100.
  • A kapott bájtok: 0xE2 0x82 0xAC.

Egy Unicode kódolású szöveget UTF-8 bájtsorozattá alakítani könnyű, néhány bitműveletről van szó.

4. Konverziók Python nyelvben

A karakterkódolásokkal szerencsére Pythonban nem sokat kell bajlódni, mert a beépített sztring típus kezeli azokat, és a fájlkezelésnél is megadható a szövegfájlok kódolása.

Ha egy adott sztringnél kíváncsiak vagyunk, hogyan jelenik az meg UTF-8 kódolással:

bytearr = "hello".encode(encoding="UTF-8")
for b in bytearr:
    print("{:02x}".format(b), end=" ")    # 68 65 6c 6c 6f
print()

bytearr = "helló".encode(encoding="UTF-8")
for b in bytearr:
    print("{:02x}".format(b), end=" ")    # 68 65 6c 6c c3 b3
print()

Ebben pont látszik, hogy az ékezetes szöveg a hosszú ó miatt hosszabb egy bájttal.

Ha van egy bájtsorozatunk, amely egy szöveg karakterkódjait tartalmazzal, azt a sztring osztály konstruktorával szöveggé alakíthatjuk. Ekkor meg kell adni a karakterkódolást, aminek alapértelmezett értéke amúgy pont az UTF-8 lenne:

bytearr = bytes([0x68, 0x65, 0x6c, 0x6c, 0xc3, 0xb3])
szoveg = str(bytearr, encoding="UTF-8")
print(szoveg)   # helló