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.
<?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.
<?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.
<?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
.
<?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.
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ý.
Diskuse je zrušena z důvodu spamu.