Kontrola pravopisu v HTML dokumentu

Školení, která pořádám

Článek vyšel v rámci PHP okénka na serveru Root.cz.

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 řetězec ke zkontrolování v kódování UTF-8
* @param string kód jazyka používaný knihovnou PSpell
* @return array slova nenalezená ve slovníku, false v případě chyby
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
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 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));
    } else {
        return chr(240 | ($code >> 18)) . chr(128 | (($code >> 12) & 63)) . chr(128 | (($code >> 6) & 63)) . chr(128 | ($code & 63));
    }
}
*/

define('WORD_BOUNDARY', '[^A-Za-z\\x{C0}-\\x{233}]');
?>

Znaky s kódy C0 až 233 hexadecimálně 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 kontrolovaný DOM objekt
* @param string kód jazyka používaný knihovnou PSpell
* @return array slova nenalezená ve slovníku
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
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::loadHTMLFile 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ů.

Jakub Vrána, Řešení problému, 30.5.2005, diskuse: 5 (nové: 0)

Diskuse

ikona Marty:

přínosný článek :-), jen by me zajimalo, jakou vyhodu ma vedeni slov jakozto indexů pole - $return[$word] = true

SendiMyrkr:

Obrovskou, pokud budu mit posle obsahujicí slova která jsou pravopisně správná tedy pole
<?php $return['slovnik']=array('slovo1','slovo2',...) ?>
tak budu muset procházet celé pole abych zjistil jestli se v něm kazdě jedno slovo vyskytuj. Pokud ale pouziji pole
<?php
$retrun
=array('slovo1' => true,'slovo2' => true...)
?>
Pak mohu jednoduse rozdelit text na slova a jejich spravnost
overovat jednoduchou podminkou
<?php
if($return[$slovo[1]]){
  echo "slovo je spravne";
}
?>

ikona Marty:

Pěkné :), v pozadí ale musí probíhat víceméně to samé, ať při jednom nebo při druhém postupu, jde tedy i o výkonové zlepšení?

Ten druhý zápis vypadá samozřejmě elegantněji a bude kratší než zápis s array_search()..

ikona Jakub Vrána OpenID:

U obou přístupů probíhá v pozadí něco docela jiného. U klíče pole se vypočte hash a zjistí se jeho příslušnost do pole pohledem do tabulky, časová složitost v průměrném případě O(1). U hodnoty pole se prochází jeden prvek za druhým a porovnávají se, časová složitost O(n). U velkých polí je rozdíl tedy zcela zásadní.

ikona dgx:

I v případě malých polí je rychlostní rozdíl dramatický - http://www.dgx.cz/trine/item/php-pomale-switch-a-case

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.