Navštívení všech odkazů na webu

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

V minulosti jsem řešil úlohu, jak vytvořit seznam všech stránek na webu, které jsou dostupné z dané adresy. Samozřejmě na to existuje řada nástrojů, např. wget nebo Xenu, někdy ale potřebujeme větší variabilitu, než jakou tyto nástroje nabízí. Nebo to můžeme vzít prostě jako cvičení. Než začneme psát kód, tak je dobré si uvědomit, co bude úzké hrdlo programu. V tomto případě to určitě bude síťová komunikace, takže kód rovnou napíšeme s využitím futures.

Procházení URL

<?php
/** Visit links reachable from URL
* @param string
* @param int number of URLs to download in parallel
* @return array [ $url => $valid ]
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function crawl($url, $limit = 8) {
    $url = resolveLink($url, '');
    $visited = array($url => false);
    $futures = array($url => new HTTPSFuture($url));
    $futures = Futures($futures)->limit($limit);

    foreach ($futures as $base => $future) {
        list($status, $body, $headers) = $future->resolve();
        if ($status->isError()) {
            continue;
        }
        $visited[$base] = true;
        if (!preg_match('~^text/html(;|$)~', getHeader($headers, 'Content-Type'))) {
            continue;
        }

        foreach (findLinks($body, $base) as $link) {
            if (isset($visited[$link])) {
                continue;
            }
            if (strncmp($link, $url, strlen($url)) != 0) {
                continue;
            }
            $visited[$link] = false;
            $futures->addFuture(new HTTPSFuture($link), $link);
        }

    }
    return $visited;
}
?>

Funkce prochází všechna URL (začne v předaném kořeni) a pomocí funkce findLinks v nich hledá odkazy. Když najde nějaký nový, tak ho přidá do futures. Externí odkazy nenavštěvuje, jinak bychom nejspíš stáhli celý Internet.

Nacházení odkazů

<?php
/** Find links in HTML documents, ignore forms
* @param string
* @param string
* @return array empty array for invalid document
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function findLinks($html, $root) {
    if ($html == '') {
        return array();
    }
    $dom = new DOMDocument;
    if (!$dom->loadHTML($html)) {
        return array();
    }
    
    foreach ($dom->getElementsByTagName('base') as $base) {
        $href = $base->getAttribute('href');
        if ($href != '') {
            $root = resolveLink($href, $root);
            break;
        }
    }

    $return = array();
    $tag_attrs = array('a href', 'area href', 'frame src', 'iframe src');
    foreach ($tag_attrs as $tag_attr) {
        list($tag, $attr) = explode(' ', $tag_attr);
        foreach ($dom->getElementsByTagName($tag) as $el) {
            $href = $el->getAttribute($attr);
            if ($href != '') {
                $return[] = resolveLink($href, $root);
            }
        }
    }
    return $return;
}
?>

Funkce findLinks pomocí metody DOMDocument::loadHTML načte předaný dokument, zpracuje značku <base href> a hledá odkazy ve značkách <a href>, <area href>, <frame src> a <iframe src>. Formuláře prozatím ignoruje – mohli bychom zpracovat ty odesílané metodou GET, ale to by bylo složitější. Parsování nevalidních dokumentů produkuje spoustu chyb, které můžeme vypnout funkcí libxml_use_internal_errors.

Absolutizace odkazu

<?php
/** Create absolute link
* @param string
* @param string
* @return string with stripped #anchor part
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function resolveLink($href, $root) {
    $url = parse_url($href);
    $rootUrl = parse_url($root);
    $path = idx($url, 'path');
    $query = idx($url, 'query');
    
    if (!isset($url['scheme'])) {
        if (isset($url['host'])) {
            $url['scheme'] = idx($rootUrl, 'scheme', 'http');
        } else {
            if ($path == '') {
                $path = idx($rootUrl, 'path');
                $query = ($href == '' ? idx($rootUrl, 'query') : $query);
            } elseif (substr($path, 0, 1) != '/') {
                $rootPath = idx($rootUrl, 'path', '/');
                $path = preg_replace('~[^/]+$~', '', $rootPath) . $path;
            }
            $url = $rootUrl;
        }
    }
    
    $path = preg_replace('~(/\.)+(/|$)~', '/', $path);
    do {
        $path = preg_replace('~(^|/[^/]*)/\.\.(/|$)~', '/', $path, 1, $count);
    } while ($count);
    
    $pass = (isset($url['pass']) ? ":$url[pass]" : '');
    $user = (isset($url['user']) ? "$url[user]$pass@" : '');
    $port = (isset($url['port']) ? ":$url[port]" : '');
    $path = ($path != '' ? $path : '/');
    $query = ($query != '' ? "?$query" : '');
    return "$url[scheme]://$user$url[host]$port$path$query";
}
?>

Funkce pro vytvoření absolutního odkazu zohledňujeme pět případů – máme absolutní URL, máme absolutní URL bez schématu, máme absolutní cestu, máme relativní cestu a nemáme žádnou cestu. Část pro kanonizaci URL je poměrně nechutná – potřebujeme odstranit odkazy na ten stejný adresář ./, počáteční ../ a jakékoliv abc/../. Škoda, že nemůžeme použít funkci realpath. Na několika místech se používá jednoduchá funkce idx, která měla být zahrnuta do PHP v momentě, kdy přístup k neinicializovaným prvkům pole začal vyhazovat E_NOTICE.

Získání hlavičky

<?php
/** Find value of the first header with given name
* @param array of [ $name, $value ]
* @param string case-insensitive
* @return string or null if not found
*/
function getHeader(array $headers, $search) {
    foreach ($headers as $header) {
        list($name, $value) = $header;
        if (strcasecmp($name, $search) == 0) {
            return $value;
        }
    }
    return null;
}
?>

Jde o jednoduchou funkci, která ze seznamu dvojic vezme hodnotu hledaného názvu.

Závěr

Jedná se o celkem jednoduchou úlohu, na které je asi nezajímavější použití futures. Nejdelší je kód pro absolutizaci odkazu, který je naopak nezajímavý.

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

Diskuse

Honza:

V PHP dělám minimálně, takže možná primitivní otázka. Proč se používá DomDocument místo SAXu? Ten by měl být daleko rychlejší ...

ikona Jakub Vrána OpenID:

Především proto, že zpracováváme HTML a nikoliv XML. Navíc je to pohodlnější a neočekávám, že by to bylo úzké hrdlo programu.

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.