Jste zde

Znaková sada vs kódování

Docela často dochází k zaměňování pojmů znaková sada a kódování. Nejčastěji se s tímto setkávám v případech, kdy dotyčný hovoří o Unicode a např. UTF-8 tak, jako by to bylo to stejné. V příspěvku se podíváme na to, jaký je mezi těmito dvěma termíny rozdíl a budeme si to prakticky ilustrovat v Pythonu.

Definice pojmů

Znaková sada (angl. character set) udává množinu znaků, které můžete používat. Tato množina se typicky udává jako tabulka, ve které se nachází dvojice (číslo, grafická reprezentace). Ono číslo bývá někdy nazýváno ordinální hodnota. Mezi typické příklady znakových sad patří ASCII, Windows-1250 (cp1250), ISO 8859-2 (latin2) a Unicode. Když se např. podíváte do ASCII tabulky, tak pro znak Z tam uvidíte ordinální hodnotu 90 (desítkově) či 0x5A (šestnáctkově).

Kódování (angl. character encoding) nám udává způsob, jak znaky ze znakové sady reprezentovat v paměti, v souborech na disku, při přenosu přes síť atd. Dává nám tedy předpis, jak každé číslo ze znakové sady reprezentovat.

Reprezentace znakových sad

Zde přichází zdroj onoho nedorozumění. Znaky ze znakových sad jako je ASCII, Windows-1250 či ISO 8859-2 se totiž reprezentují přímo jejich ordinální hodnotou. Je to dáno tím, že všechny jejich ordinální hodnoty lze reprezentovat jediným bajtem. Např. pokud si otevřete textový soubor v kódování Windows-1250, zapíšete do něj slovo Petr a uložíte, bude tento soubor obsahovat čtyři bajty 0x50 0x65 0x74 0x72. V závislosti na editoru a operačním systému ještě může obsahovat ukončovač řádku, ale toto pomineme, protože to pro nás není podstatné. Podstatné je, že pro některé znakové sady platí, že jsou totožné se svým kódováním (reprezentací).

Rozdíl nastává typicky až u Unicode. Tato znaková sada obsahuje znaků více, než by se dalo reprezentovat jediným bajtem. Musíme tedy použít bajtů více. Ovšem způsobů, jak to udělat, existuje celá řada. Proto pro znakovou sadu Unicode existuje řada různých kódování, např. UTF-8 či UTF-16. Ona existence více kódování je dána tím, že pro různé přirozené jazyky mohou být některá kódování vhodnější, než kódování jiná. Např. UTF-8 kóduje všechny ASCII znaky jedním bajtem. To je velmi vhodné např. pro angličtinu. Tedy např. Petr je v UTF-8 reprezentován jako čtyři bajty 0x50 0x65 0x74 0x72. Pro znaky mimo ASCII se použije bajtů více (2 až 6). UTF-8 totiž patří mezi kódování s proměnlivým počtem bajtů.

Můžeme se však rozhodnout použít jiné kódování, např. UTF-16. Toto kódování oproti UTF-8 používá jako jednotku nikoliv jeden bajt, ale dva bajty. Zde je však potřeba v první řadě vyřešit, v jakém pořadí ony bajty umístíme. Ano, řeč je o endianitě. Např. písmeno P má hodnotu 0x0050. Je ovšem rozdíl, zda v paměti (či do souboru) bude tato hodnota zapsána jako 0x00 0x50 či 0x50 0x00. Proto pro UTF-16 existují dvě varianty: UTF-16LE (little endian) a UTF-16BE (big endian). Pokud tedy čtete soubor zapsaný v UTF-16, je potřeba vědět, o jakou variantu se jedná. K tomu se typicky používá tzv. BOM znak na začátku souboru (z angl. byte order mark). To již ale jde za rozsah tohoto příspěvku. Každopádně, endianitu je potřeba řešit i např. pro UTF-32. Jako zajímavost uvedu, že pro UTF-8 není potřeba endianitu řešit, protože UTF-8 je bajtově orientované. První bajt je vždy první bajt, druhý bajt je vždy druhý bajt atd.

Ilustrace v Pythonu

Pojďme si to nyní ilustrovat v Pythonu. Budu používat Python verze 3. To jen, abyste případně nebyli překvapeni (práce s řetězci a kódováními se značně liší od verze 2).

Nejdříve si vytvoříme řetězec:

$ python3
>>> s = 'Kočička'

Řetězce v Pythonu jsou tvořeny posloupností čísel z Unicode tabulky, označované jako Unicode code points (bez překladu). Jedná se o ordinální čísla v pojetí Unicode. Tedy řetězec s obsahuje 7 znaků:

>>> len(s)
7

K získání code point hodnoty znaku se v Pythonu používá funkce ord():

>>> ord('č')
269
 
>>> hex(269)
'0x10d'

Když se podíváte do Unicode tabulky, tak Unicode code point 0x10d označuje právě písmeno č.

Unicode code points lze zadávat i přímo při vytváření řetězce:

>>> 'Ko\u010di\u010dka'
'Kočička'

Nyní se pojďme podívat na to, jak onen řetězec zakódovat do posloupnosti bajtů. K tomu se v Pythonu používá metoda encode(). Jejím parametrem je kódování, které chceme použít:

>>> s.encode('utf-8')
b'Ko\xc4\x8di\xc4\x8dka'

Onen prefix b nám značí, že se jedná o posloupnost bajtů. V Pythonu je potřeba důsledně rozlišovat, zda pracujeme s řetězcem či s posloupností bajtů. Jak za chvíli uvidíme, tak pokud máme posloupnost bajtů, tak není jednoznačné, v jakém kódování je text zapsaný. Proto bychom s textem měli pracovat ve formě řetězců a bajty používat pouze při vstupně/výstupních operacích. Pokud např. čtete textový soubor, tak lze uvést kódování, v jakém je soubor zapsán. Výchozí kódování je závislé na nastavení prostředí a systému, viz dokumentace k open(). Když totiž čtete textový soubor, tak byste měli vědět, v jakém je kódování. To jsem ale trochu odbočil. Když se podíváme na onu posloupnost bajtů, tak vidíme, že písmeno č se nám zakódovalo do dvou bajtů \xc4\x8d (do jednoho se totiž nevleze).

Můžeme si vyzkoušet i jiná kódování:

>>> s.encode('utf-16le')
b'K\x00o\x00\r\x01i\x00\r\x01k\x00a\x00'
 
>>> s.encode('utf-16be')
b'\x00K\x00o\x01\r\x00i\x01\r\x00k\x00a'
 
>>> s.encode('iso-8859-2')
b'Ko\xe8i\xe8ka'

Jak vidíme, není v tomto případě problém použít ani jednobajtové kódování ISO 8859-2, protože v něm znak č existuje.

Jako poslední si vyzkoušíme dekódování zakódovaných řetězců. Vezměme si onen řetězec Kočička zakódovaný do UTF-8 a zkusme jej dekódovat:

>>> b'Ko\xc4\x8di\xc4\x8dka'.decode('utf-8')
'Kočička'

Úspěch! Pokud však zvolíme jiné kódování, tak buď dostaneme nesmysl:

>>> b'Ko\xc4\x8di\xc4\x8dka'.decode('latin2')
'KoÄ\x8diÄ\x8dka'

nebo dojde k chybě:

>>> b'Ko\xc4\x8di\xc4\x8dka'.decode('utf-16le')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.4/encodings/utf_16_le.py", line 16, in decode
    return codecs.utf_16_le_decode(input, errors, True)
UnicodeDecodeError: 'utf-16-le' codec can't decode byte 0x61 in position 8: truncated data

Komentáře

Díky! Konečně stránka, která mi to plně a naprosto srozumitelně objasnila .

Přidat komentář