Čeština a kódování v XML

XML parser čte bajty, ne písmena. Aby z bajtů poskládal správné znaky, musí vědět, v jakém kódování je soubor uložený. U češtiny se to pozná hned: stejné písmeno ř je v šesti českých kódováních šest různých bajtů. Kapitola ukazuje, proč je dnes správná volba UTF-8, jak poznat starší česká kódování a co parser udělá, když kódování nesedí.

Autor: Aktualizováno

Proč diakritika v XML dělá problémy

Písmena bez diakritiky mají ve všech běžných kódováních stejné bajty jako v ASCII, proto se anglický text rozbije jen výjimečně. České znaky ale každé kódování ukládá jinak:

ZnakUTF-8Windows-1250ISO-8859-2CP852KamenickýchKOI-8 CS2
řC5 99F8F8FDA9D2
šC5 A19AB9E7A8D3
žC5 BE9EBEA791DA
ůC5 AFF9F98596CA

Parser z bajtu F8 nepozná, jestli jde o ř z Windows-1250, nebo o neplatný bajt UTF-8. Kódování mu musí říct deklarace, BOM nebo protokol, kterým soubor přišel (třeba hlavička HTTP). Bez nich podle XML 1.0 platí UTF-8.

UTF-8 jako výchozí volba

UTF-8 obsáhne všechny znaky Unicode, takže v jednom souboru můžeš mít češtinu, polštinu, azbuku i emoji. Navíc ho podle specifikace musí umět přečíst každý XML parser, zatímco ostatní kódování parser podporovat nemusí. Například .NET bez registrace zvláštního poskytovatele nezná Windows-1250 a validátor na XML.cz ho neumí vůbec. Přehled podpory je na stránce Špatné kódování.

Doporučený zápis
<?xml version="1.0" encoding="UTF-8"?>
<zprava>Příliš žluťoučký kůň úpěl ďábelské ódy</zprava>

U UTF-8 a UTF-16 (s BOM) se deklarace smí vynechat. Uvádět ji je přesto dobrý zvyk, protože člověk i nástroj pak kódování vidí hned na prvním řádku.

Starší česká kódování

KódováníNázev v IANAKde se používaloStav dnes
UTF-8UTF-8Unicode; web, Linux, macOS, většina formátůvýchozí volba
Windows-1250 (CP1250)windows-1250Windows ve střední Evropěstále v exportech starších programů
ISO 8859-2 (Latin 2)ISO-8859-2Unix, e-mail a web v 90. letechna ústupu
PC Latin 2 (CP852)IBM852DOS a OS/2historické
Kamenických (KEYBCS2)není registrovánoDOS v Československu zhruba 1985–1995historické
KOI-8 CS2 (ČSN 36 9103)není registrovánostarší terminály, volba v editoru T602historické
  • Windows-1250 obsahuje všechny tisknutelné znaky ISO 8859-2, ale některé na jiných místech. V češtině se liší š, ž, ť a jejich velké tvary. Windows-1250 navíc využívá oblast 80–9F, kterou ISO 8859-2 vyhrazuje pro řídicí znaky. Není to tedy nadmnožina ISO 8859-2, i když to tak často vypadá.
  • CP852 (PC Latin 2) má stejnou sadu písmen jako ISO 8859-2, ale úplně jiné rozložení. Kvůli tomu, že se mu říkalo „Latin 2“, se dodnes plete s ISO 8859-2.
  • Kamenických vzniklo úpravou kódové stránky 437 a pojmenované je podle bratří Kamenických, autorů ovladače klávesnice KEYBCS2. Oblíbené bylo hlavně proto, že zachovalo znaky pro kreslení rámečků v DOSu. Někdy se mu říká CP895, ale IBM ani Microsoft takovou stránku pro češtinu nedefinovaly.
  • KOI-8 CS2 definovala norma ČSN 36 9103. Nejčastější písmena s diakritikou leží o 80 (šestnáctkově) výš než odpovídající velké písmeno bez diakritiky, takže po vymazání osmého bitu zůstane text aspoň trochu čitelný: z „Příliš žluťoučký kůň“ vznikne PRIliS ZluTouCkY kJN.

Kamenických a KOI-8 CS2 v registru IANA nejsou. XML 1.0 pro neregistrovaná kódování doporučuje názvy s prefixem x-, takový název ale parsery neznají. Prakticky se s těmito kódováními setkáš jen ve starých datech, která je potřeba převést.

Deklarace a BOM

Pokud soubor není v UTF-8 ani UTF-16, musí to podle XML 1.0 říct deklarace, jinak je to fatální chyba. V deklaraci použij název z registru IANA. Na velikosti písmen nezáleží, windows-1250 a WINDOWS-1250 znamenají totéž:

Soubor uložený ve Windows-1250
<?xml version="1.0" encoding="windows-1250"?>
<zprava>Příliš žluťoučký kůň úpěl ďábelské ódy</zprava>

BOM je značka pořadí bajtů na začátku souboru. U UTF-16 je povinná, u UTF-8 dovolená (bajty EF BB BF). BOM a deklarace si nesmějí odporovat. Když UTF-8 s BOM deklaruje windows-1250, libxml2 2.9.13 se řídil deklarací a vypsal rozsypaný text, zatímco Python skončil chybou. Podrobně o autodetekci, BOM a hlavičce HTTP píše kapitola Jak funguje XML.

Co se stane, když kódování nesedí

Text „Příliš žluťoučký kůň úpěl ďábelské ódy“ uložený ve Windows-1250 bez deklarace parser čte jako UTF-8. Na prvním písmenu ř narazí na bajt F8, který v UTF-8 nesmí stát nikde, a skončí:

Skutečný výstup parserů
$ xmllint --noout bez-deklarace.xml       # bajty Windows-1250, bez encoding
bez-deklarace.xml:2: parser error : Input is not proper UTF-8, indicate encoding !
Bytes: 0xF8 0xED 0x6C 0x69

# Python 3.14 (expat)
xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 2, column 9

Tahle chyba je aspoň vidět. Horší je, když soubor deklaruje kódování, které je bajtově platné, jen jiné. Soubor ve Windows-1250 s deklarací ISO-8859-2 parser přijme bez námitek, protože v ISO-8859-2 je platný každý bajt. Z š, ž a ť se ale stanou neviditelné řídicí znaky U+009A, U+009E a U+009D:

Tichá chyba — dokument je well-formed
<?xml version="1.0" encoding="ISO-8859-2"?>   <!-- bajty jsou ve Windows-1250 -->
<zprava>Příliš žluťoučký kůň</zprava>

# xmllint i Python dokument přijmou, text ale obsahuje řídicí znaky:
'Příli\x9a \x9elu\x9doučký kůň'

Specifikace takový soubor označuje za fatální chybu, protože deklarace neodpovídá skutečnému kódování. Parser ji ale nemá jak poznat. Proto po každém převodu text zkontroluj očima nebo testem. Podle vzhledu poškozeného textu se dá odhadnout, co se stalo:

Jak to vypadáSkutečné kódováníČteno jako
PĹ™Ăliš ĹľluĹĄouÄŤkĂ˝UTF-8Windows-1250
PÅÃliÅ¡ žluÅ¥ouÄkýUTF-8ISO-8859-1 (Latin 1)
Příli luoučký (š, ž, ť zmizí)Windows-1250ISO-8859-2
P°ÝliÜ ×luŁouŔkřWindows-1250CP852
Pýˇliç §luśouźkěCP852Windows-1250
P��li� �lu�ou�k�Windows-1250UTF-8 s náhradou chybných bajtů

Stejná chyba nemusí v různých parserech dopadnout stejně. UTF-8 s deklarací windows-1250 vypsal xmllint jako rozsypaný text, ale Python skončil chybou na písmenu ň. Jeho druhý bajt 88 totiž ve Windows-1250 není přiřazený žádnému znaku.

Převod do UTF-8

Než soubor převedeš, zjisti, v čem doopravdy je. Příkaz file jen hádá a Windows-1250 od ISO-8859-2 nerozliší. Spolehlivější je podívat se na bajty a porovnat je s tabulkou výše:

Zjištění kódování a převod
# Zjisti, co v souboru skutečně je
file vstup.xml
xxd vstup.xml | head

# Windows-1250, ISO-8859-2 nebo CP852 → UTF-8
iconv -f WINDOWS-1250 -t UTF-8 vstup.xml > vystup.xml
iconv -f CP852        -t UTF-8 vstup.xml > vystup.xml

# Kamenických a KOI-8 CS2 iconv nezná; umí je cstocs z balíku Cstools (CPAN)
cstocs kam utf8 < vstup.xml > vystup.xml
cstocs koi8 utf8 < vstup.xml > vystup.xml

# Po převodu oprav deklaraci a ověř výsledek parserem
xmllint --noout vystup.xml
  • iconv převede bajty, ale deklaraci v souboru nepřepíše. Po převodu změň encoding="windows-1250" na UTF-8, nebo deklaraci smaž.
  • V editoru jde totéž ručně. Ve VS Code otevři soubor přes Reopen with Encoding ve správném kódování a ulož ho přes Save with Encoding jako UTF-8.
  • Výsledek ověř parserem a zkontroluj písmena š, ž a ť. Právě u nich se Windows-1250 a ISO-8859-2 liší.

Jak načíst soubor ve Windows-1250 přímo v Javě, Pythonu, PHP nebo C# bez převodu na disku, ukazuje stránka Špatné kódování. Rozdíl mezi znakem, entitou a číselným odkazem vysvětluje kapitola Jak funguje XML.