Disassembler

Artificial intelligence is no match for natural stupidity.
29březen2013

Převod českého textu z UTF-8 do ASCII


Když jsem byl ještě malé admiňátko, které se učilo PHP, zjistil jsem, že docela často potřebuju převádět český text s diakritikou do podoby, která by byla více přátelská k nejrůznějším technologiím. I přesto, že Unicode je tu s námi už více než 20 let, všelijaké handly, pretty URL a URL vůbec, jména souborů a další věci tak nějak nemají rády, když se do nich tahají neanglické znaky. A když už takové znaky náhodou snesou, martýrium začne v okamžiku, kdy je chcete přestěhovat do jiného prostředí.

Obecně


Kdo tvrdí, že nikdy nepoužíval translační tabulky, dělá to dodnes. Translační tabulky jsou fajn, když přesně víte, jaké znaky se ve vašem textu mohou objevit. Pokud tedy víte, že budete zpracovávat pouze a výhradně češtinu, pro mě za mě si translační tabulku udělejte. Nebudete se pak ale stíhat divit, když vám do ní někdo strčí nějakou přehlásku nebo jiné nečeské nabodeníčko. Když už se do takových věcí máte pouštět tak, aby za něco stály, translační tabulku vyřaďte hned na začátku. Další věc, kterou je v textu zpravidla potřeba eliminovat, jsou mezery, speciální a netisknutelné znaky. V mých příkladech všechny tyto odstraňuji, případně nahrazuji pomlčkami, a to tak, abych se vyhnul mnohonásobným pomlčkám v případě, že nahrazuju několik nevhodných znaků za sebou. Občas se dá udělat i nějaká výhybka, aby se „C++“ převedlo na „c-plus-plus“ a „C#“ na „c-sharp“, ale to už si každý udělá podle svého. No a v neposlední řadě je vhodné písmenka srovnat do latě a udělat ze všech minusky. Pokud tuhle operaci striktně provádíte u názvů soborů, budete od adminů zaštiťujících transfery z Windows na Linuxový hosting dostávat tisíce děkovných dopisů™.

Iconv


Alfou a omegou pro úspěšný převod UTF-8 s diakritikou do ASCII je knihovna iconv, která je pro konverze kódování přímo stvořena. Zapomeňte na translační tabulky a jiná zvěrstva. Iconv totiž umí tzv. transliteraci, takže je schopna UTF-8 rozbít na jednotlivé glyfy a ty následně popřevádět a promazat tak, aby z ní vylezl smysluplný výstup.

PHP5 má dynamickou knihovnu pro iconv v základu, python poskytuje ekvivalentní funkce v bulit-in modulu unicode a jednu užitečnou berličku v modulu unicodedata. A mimo linuxových balíčků je iconv v rámci projektu GnuWin32 dostupná i jako samostatná binárka pro Windows.

PHP


Většina moderních PHP hostingů iconv podporuje bez mrknutí oka. Jak jsem již zmínil výše, PHP5 má iconv v základu, pokud jej někdo násilím neodpáře a PHP4 stačí kompilovat s parametrem --with-iconv. Pokud to váš hosting náhodou neumí, utečte pryč, protože taková firma si nezaslouží zákazníky.

S čím už by ale na některých hostinzích mohl být problém, je nastavení lokálního prostředí. I to se však dá domluvou s inteligentním adminem a za použitím PHP funkce setlocale() vyřešit. Kód pro konverzi pak může vypadat třeba takto:

setlocale(LC_CTYPE, 'cs_CZ.UTF-8');
function asciize($str) {
    $str = strtolower(iconv('UTF-8', 'ASCII//TRANSLIT', $str));
    $str = preg_replace('/[^a-z0-9.]/', ' ', $str);
    $str = preg_replace('/\s\s+/', ' ', $str);
    $str = str_replace(' ', '-', trim($str));
    return $str;
}

Python


Ekvivalentní skript pro python bude vypadat následovně:

import re, unicodedata

def asciize(str):
    str = unicode(str, 'UTF-8')
    str = unicodedata.normalize('NFKD', str.lower()).encode('ascii', 'ignore')
    str = re.sub('[^a-z0-9.]', ' ', str)
    str = re.sub('\s\s+', ' ', str)
    str = str.strip().replace(' ', '-')
    return str

Bash


A pokud náhodou budete podobnou operaci potřebovat vyrobiti v bashi, pak třeba takto:

function asciize() {
   STR=$(echo "${1}" | iconv -f UTF-8 -t ASCII//TRANSLIT)
   STR=$(echo "${STR}" | tr '[:upper:]' '[:lower:]')
   STR=$(echo "${STR}" | sed -e 's/[^a-z0-9.]/ /g')
   STR=$(echo "${STR}" | sed -re 's/\s\s+/ /g')
   STR=$(echo "${STR}" | sed -e 's/^ *//g' -e 's/ *$//g')
   STR=$(echo "${STR}" | sed -e 's/ /-/g')
   echo ${STR}
}

což se samozřejmě dá šoupnout do divokého one-lineru

echo "${UTFSTR}" | iconv -f UTF-8 -t ASCII//TRANSLIT | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9.]/ /g' -re 's/\s\s+/ /g'  -e 's/ /-/g'

PostgreSQL


A chuťovka na závěr. Podobnou transliteraci, ale tentokrát bez užití iconv, je možné udělat i pomocí PL/pgSQL. Je ovšem potřeba workaroundnout bug-nebug a umožnit si převádět kódování i pokud jsou zdrojovými daty pole bytů, jinak by na nás pgSQL prskalo s hláškou

No function matches the given name and argument types. You might need to add explicit type casts.

Převod kódování bytea je podle mě naprosto legitimní operace, na kterou se pravděpodobně jen zapomnělo, protože interně se texty jako pole bytů skutečně převádí. PgSQL přímo nabízí konverzi a transliteraci několika směry, ale transliterace z UTF-8 do ASCII mezi ně nepatří, takže je třeba si dále pomoci přes LATIN2, čímž omezíme rozmanitost znaků, se kterými můžeme zázračit. Na češtinu a slovenštinu to ale stačí, nicméně je třeba si dávat pozor, protože celá konstrukce je se všemi těmi rovnáky na vohejbáky poměrně nebezpečná.

CREATE FUNCTION to_ascii(bytea, name) 
RETURNS text AS 'to_ascii_encname'
LANGUAGE internal;

CREATE FUNCTION asciize(str text)
RETURNS text
LANGUAGE plpgsql IMMUTABLE
AS $$
	BEGIN
	  str := lower(to_ascii(convert_to(str, 'LATIN2'), 'LATIN2'));
	  str := regexp_replace(str, '[^a-z0-9.]', ' ', 'g');
	  str := regexp_replace(str, '\s\s+', ' ', 'g');
	  str := replace(trim(str), ' ', '-');
	  RETURN str;
	END;
$$;