Odstranění mezer z HTML dokumentu

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

Potřeboval jsem ověřit, zda se od sebe dva HTML dokumenty liší pouze mezerami. To není tak jednoduché, protože texty mohou být od sebe oddělené inline značkami. Pokud jsou mezery naopak před nebo za blokovými značkami, tak je musíme ignorovat. A konečně uvnitř některých značek (např. <pre>) je potřeba mezery zachovat. Tohle chování se dá změnit i pomocí CSS, to jsem ale naštěstí nepotřeboval.

<?php
class HtmlSpaces {
    const INLINE_TAGS = 'b|big|i|small|tt|abbr|acronym|cite|code|dfn|em|kbd|strong|samp|var|a|bdo|img|q|span|sub|sup|button|input|label|select|textarea';
    const IGNORED_TAGS = 'script|style|textarea|pre';
    
    static private $inText;
    static private $addSpace;
    
    /** Odstranění nadbytečných mezer z HTML dokumentu
    * @param string
    * @return string normalizovaný kód
    * @copyright Jakub Vrána, https://php.vrana.cz/
    */
    static function normalize($html) {
        $dom = new DOMDocument;
        $dom->loadHTML($html);
        self::$inText = false;
        self::stripSpaces($dom);
        return $dom->saveHTML();
    }
    
    static private function stripSpaces(DOMNode $node) {
        $isBlock = false;
        
        if ($node instanceof DOMElement) {
            if (!preg_match("~^(" . self::INLINE_TAGS . ")$~i", $node->tagName)) {
                $isBlock = true;
                self::$inText = false;
            }
            if (preg_match('~^(' . self::IGNORED_TAGS . ')$~i', $node->tagName)) {
                return;
            }
            
        } elseif ($node instanceof DOMText) {
            $data = trim(preg_replace('~\s+~', " ", $node->data));
            if ($data != '') {
                if (self::$inText) {
                    if (self::$addSpace) {
                        self::$addSpace->data .= "\n";
                    } elseif (preg_match('~^\s~', $node->data)) {
                        $data = "\n$data";
                    }
                }
                self::$inText = true;
                self::$addSpace = null;
            }
            if (self::$inText && !self::$addSpace && preg_match('~\s$~', $node->data)) {
                self::$addSpace = $node;
            }
            $node->data = $data;
        }
        
        if ($node->childNodes) {
            foreach ($node->childNodes as $node) {
                self::stripSpaces($node);
            }
        }
            
        if ($isBlock) {
            self::$inText = false;
        }
    }
    
}
?>

Kód postupně prochází všechny uzly. U značek rozlišuje, zda se jedná o blokový nebo inline element, některé značky přeskakuje úplně. U textových uzlů odstraňuje nadbytečné mezery a pamatuje si, jestli už našel nějaký neprázdný text. Pokud ano, tak si zapamatuje, kam má doplnit případnou mezeru, pokud by následoval další text uvnitř stejné blokové značky.

Důležité je upozornit na to, že kód kromě odstranění mezer dokument také normalizuje – přidá uvozovky kolem hodnot atributů (pokud někde chybí), doplní uzavírací značky a podobně. O to se stará použitá knihovna DOM. Mně to nevadí, protože potřebuji jen porovnat dva dokumenty, ale někdy to může být spíše na škodu.

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

Diskuse

zrůdy kvítek:

Díky za článek. Normalizace pomocí DOM je zajímavé řešení. Co když je ale dokument větší než dostupná paměť?

ikona v6ak OpenID:

Potom to asi místo DOM chce nějaké API, které to umí řešit streamově. Přemýšlím, jak by se to dalo řešit v PHP. No byla tam nějaká knihovna, která volala handlery - to by šlo využít, ale znamenalo by to ukládat mezivýsledek nějak třeba do souboru. Optimální to není, ale může to stačit.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 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.