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

Czirkos Zoltán · 2019.02.27.

É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, 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, amirő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 kiterjeszté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 ű. A testvérében, a Latin-2-ben (ISO8859-2) már 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ípus leírófájljában van benne helytelenül, hogy melyik alakzat melyik karaktert jelenti.

A Latin-2-höz hasonló kódolást használ a Windows a szövegfájloknál (Windows-1250). A konzol ablakban meg egy negyediket (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. (Muszáj volt képként beilleszteni, ugyanis itt, a tárgy oldalán használt betűtípus nem tartalmaz minden karaktert, ami a lenti képeken található.)

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. Mivel azonban az összes létező írásjelek jóval többen vannak, mint 256, ebben egy karaktert már nem egy, hanem kettő vagy négy bájttal jelölnek.

Az egybájtos karakterkódokról a kétbájtos Unicode kódra á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ó (65536 elemű tömb tárolja a cél kódtábla karaktereit), azonban könnyen előfordulhat, hogy olyan karaktert kell átkódolni, ami a cél kódtáblában nem létezik.

És fölmerül még egy probléma. 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, utána pedig a felső 8 bitet (előbb a kicsi – little endian). Más gépek meg épp fordítva, előre veszik a felső 8 bitet, és utána, a következő memóriacímre pedig az alsó 8 bitet (big endian). Ez egészen addig nem gond, amíg két, egymástól eltérő típusú számítógépnek kommunikálnia nem kell 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 (pl. Unicode kódolású szövegeket), akkor már figyelni kell arra, hogy 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.

Ezért a 16 bites 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.

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 mégsem kompatibilisek egymással: a „HELLO” szöveg ASCII kóddal 0x48, 0x45, 0x4C, 0x4C, 0x4F, Unicodeban 0x0048, 0x0045, 0x004C, 0x004C, 0x004F, amiből aztán a használt számítógép típusától függően vagy a bal, vagy a jobb oldali bájtsorozat lesz a fájlban. A BOM-mal együtt ezek így néznek ki:

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

Ezért találták ki az UTF-8 szövegkódolást. Az ilyen szövegekben a Unicode kódszámokat használjuk, azonban mindig 8 bites értékekből építjük 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 0x0000 és 0x007F között van), akkor levágjuk 8 bitre, és úgy tesszük a fájlba. Ha ennél nagyobb, akkor kettő, három, sőt néha még több bájtos sorozattal írjuk le. A bájtok sorrendje azonban az ilyen sorozatokban kötött, és 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
0x0000-0x007F00000000 0xxxxxxx0xxxxxxx
0x0080-0x07FF00000yyy yyxxxxxx110yyyyy 10xxxxxx
0x0800-0xFFFFzzzzyyyy yyxxxxxx1110zzzz 10yyyyyy 10xxxxxx

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ó. A visszaalakítás ugyanilyen egyszerű.

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ó