Hlavní navigace

PHP okénko: Kontrola pravopisu v HTML dokumentu

Jakub Vrána

Dnešní PHP okénko na příkladu kontroly pravopisu ukazuje, jak rozebrat HTML dokument, a představuje dva nové obraty v PHP 5.

Pokud chceme zkontrolovat pravopis jednojazyčného HTML dokumentu, je to s využitím rozšíření PSpell poměrně jednoduché. Funkci, která bude kontrolu zajišťovat, navrhneme s využitím statické proměnné rovnou tak, aby se inicializace slovníku nemusela vykonávat při každém zavolání:

<?php
/** Kontrola pravopisu pro všechna slova v textu
@param string $check řetězec ke zkontrolování v kódování UTF-8
@param string $lang kód jazyka používaný knihovnou PSpell
@return array slova nenalezená ve slovníku, false v případě chyby
*/
function spell_check($check, $lang)
{
    static $pspell = array(); // pole s inicializovanými slovníky pro jednotlivé jazyky
    if (!isset($pspell[$lang])) {
        pspell_config_create($lang); // bez vytvoření konfigurace nemusí pspell_new() najít slovník
        $pspell[$lang] = @pspell_new($lang, "", "", "utf-8");
    }
    if (!$pspell[$lang]) { // nepodařilo se načíst slovník pro daný jazyk
        return false;
    }

    $return = array();
    $words = array_unique(preg_split('~' . WORD_BOUNDARY . '+~u', $check, -1, PREG_SPLIT_NO_EMPTY));
    foreach ($words as $word) {
        if (!pspell_check($pspell[$lang], $word)) {
            $return[$word] = true;
        }
    }
    return $return;
}

$file = file_get_contents($filename);
$file = strip_tags($file); // pozor, může spojit dvě slova do jednoho
$errors = spell_check($file, 'cs');
?> 

Funkce předpokládá, že dokument bude v kódování UTF-8, což v reálných podmínkách často nebude splněno. Pro skutečné nasazení tedy budeme muset kódování detekovat a např. funkcí iconv převést. V kódování UTF-8 nefunguje správně regulární výraz \W, takže slova rozdělujeme pomocí vlastní konstanty WORD_BOUNDARY, kterou můžeme definovat např. takto:

<?php
/** Obdoba funkce chr() pro kódování UTF-8
@param int $code pozice znaku
@return string UTF-8 reprezentace kódu
*/
function chr_utf8($code)
{
    if ($code < 0) {
        return false;
    } elseif ($code < 128) {
        return chr($code);
    } elseif ($code < 2048) {
        return chr(192 | ($code >> 6)) . chr(128 | ($code & 63));
    } elseif ($code < 65536) {
        return chr(224 | ($code >> 12)) . chr(128 | (($code >> 6) & 63)) . chr(128 | ($code & 63));
    }
}

define('WORD_BOUNDARY', '[^A-Za-z' . chr_utf8(192) . '-' . chr_utf8(563) . ']');
?> 

Znaky s kódy 192 až 563 jsou vyhrazeny znakům odvozeným z Latinky, funkce chr_utf8 potom vrací reprezentaci těchto znaků v kódování UTF-8.

S vynaložením určitého úsilí by se dal kód např. s využitím regulárních výrazů vylepšit tak, aby kontroloval kupříkladu i atributy alt a title nebo aby ignoroval obsah značek <script> a <style>. Mnohem zajímavější ale bude kód upravit tak, aby respektoval atribut lang ve vícejazyčných HTML dokumentech. Tento atribut určuje jazyk hodnot textových atributů a obsahu značek a můžou ho používat skoro všechny HTML značky. Abychom atribut dokázali zpracovat, budeme muset HTML dokument rozebrat některým z následujících způsobů:

Využít regulární výrazy
Hledání značek obsahujících daný atribut a odpovídajících uzavíracích značek je mnohokrát zpracovaná úloha, která se pomocí regulárních výrazů neřeší zrovna pohodlně.
Využít rozšíření XML
HTML dokumenty nejsou XML, takže bychom je nejprve museli převést – nejsnáze s využitím rozšíření Tidy. Toto rozšíření ostatně HTML dokument do struktury objektů dokáže rozebrat samo o sobě.
Využít rozšíření DOM
Standard DOM je k rozebírání struktury HTML a XML dokumentů přímo navržen, ale v PHP je toto rozšíření k dispozici až od verze 5.

Nejčistší řešení bude přes rozšíření DOM:

<?php
$SPELL_ATTRS = array("abbr", "alt", "label", "prompt", "standby", "summary", "title");

/** Kontrola pravopisu DOM objektu a jeho potomků na základě hodnoty atributu lang
@param DOMNode $node kontrolovaný DOM objekt
@param string $lang kód jazyka používaný knihovnou PSpell
@return array slova nenalezená ve slovníku
*/
function spell_check_childs($node, $lang)
{
    $return = array();
    // nastavení jazyka
    if ($node->getAttribute("lang")) {
        $lang = $node->getAttribute("lang");
    }
    // kontrola textových atributů
    foreach ($GLOBALS["SPELL_ATTRS"] as $attr) {
        if ($node->getAttribute($attr)) {
            $return += spell_check($node->getAttribute($attr), $lang);
        }
    }
    // kontrola dětí
    foreach ($node->childNodes as $child) {
        if ($child instanceof DOMText) {
            $return += spell_check($child->nodeValue, $lang);
        } elseif ($child instanceof DOMElement && !in_array($child->nodeName, array("script", "style"))) {
            $return += spell_check_childs($child, $lang);
        }
    }
    return $return;
}

$dom = DOMDocument::loadHTMLFile($filename);
$html = $dom->getElementsByTagName("html")->item(0);
$errors = spell_check_childs($html, 'cs');
?> 

Funkcí DOMDocument::lo­adHTMLFile se načte HTML dokument a na kořenovou značku <html> se spustí rekurzivní kontrola pravopisu. Ta v případě nastaveného atributu lang změní aktuální jazyk, zkontroluje atributy obsahující textovou informaci a prochází děti DOM objektu – v textových objektech kontroluje pravopis, značky <script> a <style> a komentáře ignoruje a na ostatní objekty volá rekurzivně sama sebe. Chyby se vracejí nadřazené funkci v návratové hodnotě, alternativním řešením by bylo předávat je v parametru funkce volaném referencí.

U kontroly dětí si lze všimnout toho, že vlastnost childNodes je ve skutečnosti objekt typu DOMNodeList. Díky iteraci objektů zavedené v PHP 5 je ale možné tento objekt přímo procházet konstrukcí foreach.

Zdánlivě jednoduchý příklad kontroly pravopisu v HTML dokumentu se zvrtl na rozebírání dokumentu rozšířením DOM. Využili jsme při tom dva nové obraty v PHP 5 – operátor instanceof a iteraci objektů.


Podobně laděné texty můžete najít i na autorově weblogu PHP triky.

Našli jste v článku chybu?