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

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 az 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 ezek a problémák megszüntethetőek. Legtöbbször csak egy-két függvényhívásról van szó!
Ha a rémtörténet nem érdekel, szeretnél jól aludni, és csak azért vagy itt, mert az ékezetes szövegek nem látszanak jól a nagyházidban, akkor lapozz az oldal legaljára, a receptekhez.
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, az InfoC site-on használt betűtípus nem tartalmaz minden karaktert, ami a lenti képeken található.)

ISO8859-1 (Latin-1)

IBM-852

ISO8859-2 (Latin-2)
A karakterkódolások közötti inkompatibilitás problémája
akkor jelentkezik, amikor a programunkban ékezetes szöveget
szeretnénk kiírni. Ha azt mondjuk a Code::Blocksban (Windowson),
hogy printf("ő")
, a keletkező sztring a 0xF5
,
0x00
bájtokból áll: az ő
kódja és a lezáró nulla. De
a konzolablakban a 0xF5
a paragrafus jel §
karakterkódja!
Ha beolvasunk egy sztringet, az viszont helyesen fog megjelenni kiíráskor,
mivel a programunkban történő beolvasáskor már az IBM-852 szerinti kódok
vannak:
C:\...\karakter\bin\Debug> karakter.exe ═rd be, hogy teniszŘt§! teniszütő Ezt Ýrtad be: teniszütő
Ha azonban a konzol ablakhoz kiválasztunk egy olyan betűtípust, amely tartalmazza a megfelelő ékezetes karaktereket (pl. a Lucida Console ilyen), és a parancssorban a program futtatása előtt átváltjuk a karakterkódolást arra, amelyik kódolással a forráskódot is elmentettük, helyesen jelenik meg a szöveg:
C:\...\karakter\bin\Debug> chcp 1250 kódlap váltása
Az aktív kódlap: 1250
C:\...\karakter\bin\Debug> karakter.exe
Írd be, hogy teniszütő!
teniszütő
Ezt írtad be: teniszütő
Szóval most már működnek az ékezeteink.
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 két bájttal jelölnek (vagy ritkán néggyel).
Aminek pedig az a következménye, hogy egy Unicode sztring
nem jelenhet meg char[]
típusként a C programunkban, mert a
char
bájtot jelent.
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 veszi 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:
unsigned short bajtcsere(unsigned short kod) /* felteve, hogy 16 bites */ { return (kod & 0xFF)<<8 | (kod & 0xFF00)>>8; }
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ány | Unicode | UTF-8 |
---|---|---|
0x0000-0x007F | 00000000 0xxxxxxx | 0xxxxxxx |
0x0080-0x07FF | 00000yyy yyxxxxxx | 110yyyyy 10xxxxxx |
0x0800-0xFFFF | zzzzyyyy yyxxxxxx | 1110zzzz 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
.
Az UTF-8 kódolású sztringek, mivel bájtokból állnak, a C
forráskódokban „újra” char[]
-ként jelenhetnek meg.
Ezeknél azonban a beépített sztringkezelő függvényeket használva
elég furcsa dolgokat tapasztalhatunk. Pl. azt, hogy strlen("o")
értéke 1, viszont strlen("ő")
értéke 2.
strlen("€")
meg 3.
Az ő
betűt két bájt kódolja, míg az o
betűt
csak egy, és ezt az strlen()
nem tudja. Továbbá, míg az 'o'
a
fordító számára érthető, 'ő'
és '€'
teljesen teljesen értelmezhetetlenek, szintaktikai hibát jelentenek,
mivel az aposztrófok között a fordító egy bájtot vár, de a
forráskódban kettőt vagy hármat talál. Végleg át kell állítanunk
az agyunkat: a char
nem karaktert, hanem bájtot jelent!
Még jó, hogy a többi függvény, pl. a strcpy()
és a strcmp()
nagyjából helyesen működik. (Végülis ez
volt a célja az UTF-8 megalkotóinak.)
Egy Unicode kódolású szöveget UTF-8 bájtsorozattá alakítani könnyű, néhány bitműveletről van szó:
#include <stdio.h> typedef unsigned char Bajt; typedef unsigned short UniKar; /* Unicode sztringbol UTF-8 sztringet csinal. a bemenet es a * kimenet is nullaval terminalt tomb. */ void unicode_2_utf8(UniKar const *be, Bajt *ki) { int pb, pk; pk=0; for (pb=0; be[pb]!=0x0000; ++pb) { /* 00000000 0xxxxxxx 0x0000-0x007F 0xxxxxxx */ if (be[pb]<=0x007F) ki[pk++] = be[pb]; else /* 00000yyy yyxxxxxx 0x0080-0x07FF 110yyyyy 10xxxxxx */ if (be[pb]<=0x07FF) { ki[pk++] = 0xC0 | be[pb]>>6; /* 0xC0 = 11000000 */ ki[pk++] = 0x80 | (be[pb]&0x3F); /* 0x80 = 10000000, 0x3F = 00111111 */ } /* zzzzyyyy yyxxxxxx 0x0800-0xFFFF 1110zzzz 10yyyyyy 10xxxxxx */ else { ki[pk++] = 0xE0 | be[pb]>>12; /* 0xE0 = 11100000 */ ki[pk++] = 0x80 | ((be[pb]>>6)&0x3F); ki[pk++] = 0x80 | (be[pb]&0x3F); } } ki[pk]=0; } int main() { UniKar arvizturo[] = { 0x00E1, 'r', 'v', 0x00ED, 'z', 't', 0x0171, 'r', 0x0151, ' ', 0x263A, ' ', 0x20AC, 0x0000 }; unsigned char arvizturo_utf8[30]; unicode_2_utf8(arvizturo, arvizturo_utf8); printf("arvizturo szmajli, es euro: %s\n", arvizturo_utf8); return 0; }
A visszaalakítás ugyanilyen egyszerű. A programot elindítva
ennek kell megjelennie: árvíztűrő ☺ €. Linuxon egyből ez fog
megjelenni (ezek UTF-8 kódolást használnak szinte mindenhol),
a Windowsokon meg a fenti módon a kódlapot át kell állítani,
csak most a chcp 65001
paranccsal, ahogy a hatodik
előadás szmájlis példaprogramjához is kellett.
4 Ékezetek: receptek
Az alábbi kódrészletek a nagyházikban szabadon használhatóak, a forrás megjelölése mellett.
Windows parancssor (konzol ablak) és fájlok
Egy magyar nyelvűre állított Windowson a legtöbb program Windows-1250 karakterkódolást használ, ami nagyjából kompatibilis a Latin-2-vel. Ezért érdemes a forráskódot is így elmenteni (a Code::Blocks alapbeállítás szerint ezt teszi), és a szövegfájlokban is ezt használni. A parancssori ablakot pedig úgy beállítani, hogy a Lucida Console, vagy egy másik, Unicode-kompatibilis betűtípust használjon. (Az ablak ikonjára klikk, tulajdonságok, stb.)
A konzol ablak kódlapjának beállítása megtehető a programból is,
két függvényhívással: SetConsoleCP(1250)
és
SetConsoleOutputCP(1250)
. Az egyik a bemeneti kódlapot
állítja be, a másik pedig a kimeneti kódlapot. (Hogy miért tér el a
beolvasáskor (scanf) és kiíráskor (print) használt karakterkódolás
(!) a Windowsban, miért kell ezeket külön beállítani, egy örök
rejtély a világ számára.) Vigyázat, ezek nem szabványos
függvényhívások! Illik őket #ifdef
-ek közé tenni,
hogy maradjanak hatástalanok, ha más operációs rendszeren fordítja
valaki a programot:
#include <stdio.h> #include <string.h> #if defined(WIN32) || defined(_WIN32) #include <windows.h> #endif int main() { char s[100]; #if defined(WIN32) || defined(_WIN32) SetConsoleCP(1250); SetConsoleOutputCP(1250); #endif printf("Írd be, hogy teniszütő!\n"); gets(s); printf("Ezt írtad be: %s.", s); return 0; }
Ha minden jól van beállítva, ennek működnie kell. A két függvényhívást
elég a program elején egyszer megtenni (praktikusan a main()
elején valamikor), többször már nem kell.
Linux parancssor és fájlok
Ez könnyű. A legtöbb Linux UTF-8 kódolást használ a parancssori
ablakokban és a fájlokban is, úgyhogy semmi extra teendő nincsen, rögtön
működnek az ékezetes betűt használó programok. Egy dologra kell figyelni,
hogy az UTF-8-ban karakter≠bájt! Mivel az ékezetes betűk kettő, egyéb
karakterek akár három bájttal lehetnek kódolva, a sztringek indexei
elcsúsznak, és hosszaik nem egyeznek meg az strlen()
által adottakkal. Például strlen("teniszütő")
értéke 11.
Ez 9 karakter, 11 bájt hosszú sztring, 12 bájtnyi memóriafoglalás.
(Az utf8_strlen()
függvény megírása házi feladat.)
Konverzió: Latin-2-ből Unicodeba
Ha egy Windowson egy Latin-2 kódolású fájlból beolvasott szöveget kell megjeleníteni az SDL-lel, akkor ilyen irányú átalakítást kell csinálni. Az alábbi függvénnyel oldható meg:
typedef unsigned char Latin2Kar; typedef unsigned short UniKar; void latin2_2_unicode(Latin2Kar *be, UniKar *ki) { unsigned short tabla[128] = { /* A 0x80-0xFF karakterek Unicode megfeleloje */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0x104, 0x2D8, 0x141, 0xA4, 0x13D, 0x15A, 0xA7, 0xA8, 0x160, 0x15E, 0x164, 0x179, 0xAD, 0x17D, 0x17B, 0xB0, 0x105, 0x2DB, 0x142, 0xB4, 0x13E, 0x15B, 0x2C7, 0xB8, 0x161, 0x15F, 0x165, 0x17A, 0x2DD, 0x17E, 0x17C, 0x154, 0xC1, 0xC2, 0x102, 0xC4, 0x139, 0x106, 0xC7, 0x10C, 0xC9, 0x118, 0xCB, 0x11A, 0xCD, 0xCE, 0x10E, 0x110, 0x143, 0x147, 0xD3, 0xD4, 0x150, 0xD6, 0xD7, 0x158, 0x16E, 0xDA, 0x170, 0xDC, 0xDD, 0x162, 0xDF, 0x155, 0xE1, 0xE2, 0x103, 0xE4, 0x13A, 0x107, 0xE7, 0x10D, 0xE9, 0x119, 0xEB, 0x11B, 0xED, 0xEE, 0x10F, 0x111, 0x144, 0x148, 0xF3, 0xF4, 0x151, 0xF6, 0xF7, 0x159, 0x16F, 0xFA, 0x171, 0xFC, 0xFD, 0x163, 0x2D9 }; int i, j; j=0; /* vegig a sztringen */ for (i=0; be[i]!=0; ++i) { if (be[i]<128) /* ascii? */ ki[j++]=be[i]; else ki[j++]=tabla[be[i]-128]; } /* itt is nulla a lezaro nulla */ ki[j]=0x0000; }
Konverzió: Unicode-ból UTF-8-ba
Ha egy SDL-en, billentyűzetről beolvasott sztringet kell fájlba írni, vagy más, meglévő UTF-8 sztringekbe beilleszteni:
typedef unsigned char Bajt; typedef unsigned short UniKar; void unicode_2_utf8(UniKar const *be, Bajt *ki) { int pb, pk; pk=0; for (pb=0; be[pb]!=0x0000; ++pb) { /* 00000000 0xxxxxxx 0x0000-0x007F 0xxxxxxx */ if (be[pb]<=0x007F) ki[pk++] = be[pb]; else /* 00000yyy yyxxxxxx 0x0080-0x07FF 110yyyyy 10xxxxxx */ if (be[pb]<=0x07FF) { ki[pk++] = 0xC0 | be[pb]>>6; /* 0xC0 = 11000000 */ ki[pk++] = 0x80 | (be[pb]&0x3F); /* 0x80 = 10000000, 0x3F = 00111111 */ } /* zzzzyyyy yyxxxxxx 0x0800-0xFFFF 1110zzzz 10yyyyyy 10xxxxxx */ else { ki[pk++] = 0xE0 | be[pb]>>12; /* 0xE0 = 11100000 */ ki[pk++] = 0x80 | ((be[pb]>>6)&0x3F); ki[pk++] = 0x80 | (be[pb]&0x3F); } } ki[pk]=0; }
Konverzió: UTF-8-ból Unicode-ba
Ha UTF-8 sztringek vagy fájlok karaktereit kellene egyesével látni:
/* UTF-8 bajtsorozatbol allitja elo az Unicode sztringet. Mindketto nullaval terminalt. * A bemeneti UTF-8 bajtsorozatnak helyesnek kell lennie! */ void utf8_2_unicode(Bajt const *be, UniKar *ki) { int pb, pk; pk=0; for (pb=0; be[pb]!=0; ++pb) { if (be[pb]<0x80) { /* 00000000 0xxxxxxx 0x0000-0x007F 0xxxxxxx */ ki[pk++] = be[pb]; } else if (be[pb]>>5 == 6) { /* 0x6 = 110 bin */ /* 00000yyy yyxxxxxx 0x0080-0x07FF 110yyyyy 10xxxxxx */ ki[pk++] = (be[pb]&0x1f)<<6 | (be[pb+1]&0x3f); pb+=1; /* ket bajtot hasznaltunk */ } else { /* zzzzyyyy yyxxxxxx 0x0800-0xFFFF 1110zzzz 10yyyyyy 10xxxxxx */ ki[pk++] = (be[pb]&0x0f)<<12 | (be[pb+1]&0x3f)<<6 | (be[pb+2]&0x3f); pb+=2; /* harom bajtot hasznaltunk */ } } ki[pk]=0x0000; }