Perličky: kódování znaků a unicode

25. 9. 2008
Doba čtení: 9 minut

Sdílet

Podpora různých znakových sad, vícebajtové kódování, Unicode a UTF-8. To jsou pojmy nahánějící strach nejednomu správci nebo programátorovi. Jak to bylo krásné, když jsme měli jenom 7 bitů a všichni mluvili anglicky. Anebo ne? Vzpomeňme na EBCDIC nebo zavedení znaku Euro a pojďme se podívat, jak se věci mají.

Znakové sady, kódování a trocha historie

Než se pustíme do samotné práce v Perlu, je potřeba uvést několik obecných pojmů a fakt. Znaková sada je množina symbolů, kterou jsme schopni reprezentovat. K této znakové sadě zpravidla existuje jedno nebo více kódování této znakové sady jakožto seznam bitových reprezentací pro tyto symboly. Názvy kódování jsou obvykle jednoznačné, je tedy možno identifikovat jedním názvem (např. ASCII) kódování i znakovou sadu současně. Unicode je ale název znakové sady, ke které existuje několik různých kódování (viz níže).

Různé znakové sady a zejména jejich různá kódování vznikaly historicky postupně dle potřeb reprezentace různých znaků, často ignorujíce již zavedené kódování téže znakové sady. Snad prvním takovým exemplářem je kódování EBCDIC, které vzniklo v šedesátých letech téměř paralelně k tehdy již známému kódování ASCII. Další přehršle kódování pak vzniklo za účelem reprezentace znaků národních abeced. Valná většina takto vzniklých kódování byla osmibitová, tj. měla schopnost reprezentace pouze 256 symbolů, přičemž prvních 128 symbolů bylo totožných s ASCII. Výhodou osmibitové reprezentace mělo být snadné zacházení s řetězci, respektive kompatibilita s tehdejšími knihovnami pro zpracování řetězců. Nevýhodou bylo, že se do rozmezí 128 symbolů nemohly vejít znaky všech abeced, vznikly tedy tzv. kódové stránky pro různé abecedy. Software používající kódovou stránku pro jednu abecedu (např. „západoevropskou“ latin-1) pak nemohl zobrazit správně znaky jiné abecedy (např. latin-2 pro střední Evropu). Navíc různé operační systémy si zavedly pro tyto znakové sady různé reprezentace – jen pro češtinu máme nejméně pět kódování (DOS 852, Windows 1250, ISO8859–2, mac-ce a hrdinný počin bratří Kamenických). Další náklad do tohoto chaosu pak přinášejí vícebajtová kódování, například skupina CJK pro čínské, japonské a korejské znaky (na tato kódování nejsem expert, tudíž se otevírá prostor pro objasnění v diskusi pod článkem).

Sjednocená znaková sada

Iniciativa kolem sjednocené znakové sady sice zametla chaos kolem kódových stránek, nicméně kvůli některým kompromisům a chybám při adopci zavedla chaos nový. Pro zevrubnou informaci doporučuji přečíst dostupné zdroje na Internetu, nicméně pro základní pochopení problematiky stačí uvést několik tvrzení. Existují dva standardy, zabývající se sjednocením znakových sad. Prvním je univerzální znaková sada (UCS), jakožto standard ISO10646. Druhým je standard Unicode, který není ISO. Tyto standardy se nejprve vyvíjely odděleně, UCS bylo koncipováno pro až 32 bitů na jeden symbol, zatímco Unicode počítalo s 16 bity. Posléze došlo ke sjednocení, oba standardy mají nyní společnou znakovou sadu (číslo symbolu i jeho význam), která obsahuje v současné době něco přes milion (17 × 65536) symbolů (UCS bylo tedy omezeno shora a Unicode rozšířeno hackem, který se jmenuje surrogate pairs).

Tyto symboly jsou rozděleny do 17 takzvaných rovin po 65536 symbolech. První rovina se nazývá základní (Basic Multilingual Plane, BMP) a za ní následují další roviny pro znaky na vyšší rovině existence. Roviny jsou pak dále členěny do bloků, které můžou mít libovolnou délku (ačkoliv často jsou násobky 128). První blok v BMP má délku 128 symbolů a obsahuje původní tabulku ASCII.

Společná sada symbolů navíc nedefinuje pouze znaky abeced, jak je tomu u předchozích sad (a kódových stránek), ale také jejich části, které je možno posléze různě skládat. Například písmeno „č“ je možno reprezentovat jedním symbolem s číslem 10D16 („Latin Small Letter C with caron“), nebo jako dvojici znaků 6316 („Latin Small Letter C“) a 2C716 („Caron“).

Standard Unicode nad rámec UCS definuje některé další vlastnosti znaků. Kvůli možnosti skládání symbolů existuje nejednoznačnost reprezentace mnoha písmen; standard Unicode definuje takzvanou kanonickou formu reprezentace, do které lze každý takový znak převést. Dalšími vlastnostmi jsou velikost písmen (velká/malá) a další třídy, řazení znaků, případně jejich numerická hodnota (u číslic, znaků pro zlomky a dalších). Je možno říci, že Unicode je v současné době nadmnožina UCS.

Kódování Unicode a UCS

Pro úplnou reprezentaci znakové sady Unicode (nebo UCS) s pevným počtem bitů na symbol existují kódování UCS-4 nebo UTF-32, původně odlišné, ale nyní identické. Toto kódování používá fixních 32 bitů pro každý znak, čímž sice usnadňuje práci, ale je velmi neefektivní (většina často používaných znaků se nachází v BMP, a většina z toho v prvním bloku). Jak víme, vícebajtové číselné hodnoty lze reprezentovat dvěma způsoby (Little vs. Big Endian). Tomu odpovídají dvě varianty UCS-4LE a UCS-4BE.

Poněkud úspornější reprezentaci získáme pomocí kódování UCS-2 nebo UTF-16. Kódování UCS-2 používá pevných 16 bitů na symbol a dnes se již doporučuje jej nepoužívat, jelikož kvůli tomu umí reprezentovat pouze symboly z BMP. Kódování UTF-16 má pohyblivou délku 16 nebo 32 bitů, přičemž většina znaků z BMP je zakódována přímo. Ostatní znaky jsou kódovány pomocí dvojice znaků z intervalu D80016–DFFF16, tzv. surrogate pairs. Opět se můžeme setkat s variantou pro velké i malé indiány. Kódování UTF-16LE se často používá pro kódování souborů v operačních systémech Windows.

Kódování UTF-8 reprezentuje symbol pomocí 8 až 32 bitů. Výhodou je, že prvních 128 symbolů (ASCII znaky) je reprezentováno stejně jako ASCII. Jakýkoliv text v kódování ASCII je tedy také v kódování UTF-8. Další symboly jsou reprezentovány sekvencemi dvou až čtyř bajtů. Jelikož se jedná o kódování bajt po bajtu, nemusíme zde rozlišovat reprezentaci vícebajtových hodnot (neexistují varianty -LE a -BE). Nevýhodou je poměrně komplikovaná práce s řetězci. Kódování UTF-8 je často používané pro obsah přenášený přes síť Internet (e-maily, www stránky) a také v operačním systému GNU/Linux.

Dalšími, již méně známými kódováními jsou UTF-7 – podobné jako UTF-8, ale používá striktně sedmibitové hodnoty, UTF-1 – původní, ale špatný návrh kódování Unicode, UTF-EBCDIC – podobné jako UTF-8, ale prvních 128 znaků je stejných jako u EBCDIC.

BOM

Pořadí bajtů lze „zadrátovat“ přímo do textu pomocí speciálního znaku BOM (Byte Order Mark) s číslem FEFF16. Správné pořadí bajtů po přečtení tohoto znaku určíme na základě faktu, že znaky FFFE16 ani FFFE000016 neexistují. U kódování UTF-8 se BOM používá pro rozlišení UTF-8 textu od ASCII. Samotný znak BOM je vykreslován jako neviditelný.

Kódování, Unicode a Perl

Experimentální podpora Unicode byla zavedena již ve verzi 5.6, ve verzi 5.8 byl odstraněn status „experimentální“. Podpora se dělí na práci s Unicode textem a podpora skriptů psaných v Unicode.

Pro práci s Unicode textem Perl používá interní reprezentaci textu, která je platformově závislá a můžeme si ji představit jako černou skříňku. Perlu potřebujeme pouze dát najevo, jakým kódováním jsou vstupní/výstupní data zakódována a/nebo konverzi udělat ručně. Na většině systémů je Perl schopen také převádět mezi starými znakovými sadami (kódovými stránkami) a interní Unicode reprezentací.

Nastavit kódování na souboru lze pomocí systému vrstev PerlIO. Tento systém umožňuje nastavovat na souborech různé vlastnosti kódování, ne nutně spjaté s Unicode (např. režim ukončování řádků). Jednou z vrstev je utf8 pro UTF-8, druhou je encoding(…) pro jakékoliv jiné kódování. Nastavení vrstvy lze provést při otevírání souboru pomocí open, nebo na již otevřeném souboru operátorem binmode.

use strict;
use warnings;

binmode(STDIN, ':encoding(iso8859-2)');
binmode(STDOUT, ':utf8');

print while (<>);

V případě otevírání by se použila syntax open(my $f, '<:utf8', 'soubor.txt'). Ruční konverzi provedeme pomocí modulu Encode:

use strict;
use warnings;

use Encode qw/encode decode/;

while (my $line = <>) {
    $line = decode('iso8859-2', $line);

    # v $line je nyní text v interní reprezentaci

    print encode('utf8', $line);
}

Nebo kratším zápisem:

use strict;
use warnings;

use Encode qw/encode decode/;

print (encode('utf8', decode('iso8859-2', $_))) while (<>);

Opačným směrem se pochopitelně ne vždy musí konverze podařit.

echo 'text,смотри' | perl -MEncode -ne 'print encode("iso8859-2", decode("utf8", $_))'
text,??????

Třetím parametrem můžeme modulu Encode říci, co v takové situaci udělat.

echo 'text,смотри' | perl -MEncode -ne 'print encode("iso8859-2", decode("utf8", $_), Encode::FB_HTMLCREF)'
text,&#1089;&#1084;&#1086;&#1090;&#1088;&#1080;

FB_WARN nebo FB_CROAK: "\x{0441}" does not map to iso-8859-2 at...
FB_PERLQQ: text,\x{0441}\x{043c}\x{043e}\x{0442}\x{0440}\x{0438}
FB_XMLCREF: text,&#x441;&#x43c;&#x43e;&#x442;&#x440;&#x438;
FB_QUIET: text,

Zdrojový text v Unicode

Pokud chceme napsat přímo Perlovský skript v Unicode, je situace ještě jednodušší. Pro kódování UTF-8 nebo UTF-16 stačí do skriptu (nejlépe na začátek) umístit BOM. Druhou metodou je pro UTF-8 použít use utf8 a pro jiné kódování use encoding 'kódování'. Navíc tím získáme možnost pojmenovat si proměnné libovolnými znaky.

use strict;
use warnings;
use utf8;

my $β = 2.0;
my %heš;

$heš{'❤'} = $β;

use Data::Dumper;
print Dumper(\%heš);

Výstupem bude:

$VAR1 = {
          "\x{2764}" => '2'
        };

Práce s Unicode znaky

Unicode řetězce a znaky se chovají ve většině případů stejně jako normální řetězce a znaky. Pouze je potřeba rozlišit pojem jeden znak a jeden bajt. Vypsáním jednoho znaku na výstup může vzniknout několikabajtový soubor. Ale všechny funkce pro práci s řetězci, jakož i regulární výrazy (například meta-znak .) pracují se znaky, nikoliv s bajty. Pokud tedy budeme předpokládat, že jeden znak je nějak reprezentovaná entita a řetězec se skládá z posloupnosti znaků (nikoliv bajtů), bude vše fungovat tak, jak má.

Krom možnosti napsat přímo Unicode znaky do zdrojového kódu je můžeme specifikovat pomocí escape sekvence \x{číslo} (viz výstup z  Dumper výše). Další možností je znaky volat jménem:

use strict;
use warnings;
use charnames qw/:full/;

my $text = "\N{HEAVY BLACK HEART}";

use Data::Dumper;
print Dumper(\$text);

Případně si můžeme nechat jméno vypsat:

print charnames::viacode(0x2764);

Vlastnosti znaku si vypíšeme pomocí Unicode::Properties:

use Unicode::Properties qw/uniprops/;
$, = ',';
print uniprops('A');

V regulárních výrazech je možné tyto vlastnosti testovat pomocí \p:

print 'Ž' =~ /\p{UppercaseLetter}/;

# nebo zkráceně
print 'Ž' =~ /\p{Lu}/;

Podobně lze hledat i další vlastnosti (např. směr čtení textu).

Co v Perlu nefunguje samo

Jak bylo řečeno výše, v Unicode existuje více způsobů, jak zapsat jeden znak, a to pomocí modifikujících znaků (např. „č“ = „c“ + háček). Při porovnání řetězců se budou jevit tyto znaky jako různé. To platí i o jménech proměnných, jsou-li v Unicode. Řetězce je možno explicitně převést do jedné z normalizovaných forem pomocí modulu Unicode::Normalize.

Řazení znaků probíhá implicitně pomocí jejich číselné reprezentace, nebo pomocí pořadí, které je definováno v locale. Další možnosti řazení jsou v modulu Unicode::Collate.

bitcoin_skoleni

Některé další drobnosti – například znak „¼“ se nebude automaticky převádět na konstantu 0.25, a podobně znak„²“ není druhá mocnina. V Perlu verze 6 ale například fungují znaky „«“ a „»“ jako uvozovky pro přístup k hashům.

Závěr

Pochopení problematiky znakových sad a kódování je klíčové k pochopení faktu, že neexistuje něco jako „Unicode, které stačí zapnout“ – je potřeba stále řešit otázku kódování reprezentovaných symbolů. Praktické použití v jazyce Perl však není daleko od tohoto cíle – Unicode je „zapnuto“implicitně a stačí pouze říci, v jakém kódování má Perl přijímat a vysílat. Implementace samotného standardu Unicode je pak na velmi dobré úrovni.

Seriál: Perličky

Autor článku