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.
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ó!
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.
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
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ány | Unicode | UTF-8 |
---|---|---|
00- 7F | 00000000 xxxxxxxx | xxxxxxxx
|
80-07FF | 00000yyy yyxxxxxx | 110yyyyy 1xxxxxxx
|
0800-FFFF | zzzzyyyy yyxxxxxx | 1110zzzz 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ó.
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ó