PHP: как написать простейший краулер
...то есть, программу-"паучка", поисковый бот, который ходит по страницам и собирает информацию с них.
По сути, нам просто нужно некий заданный URL-адрес рекурсивно проанализировать на заданную глубину ссылок и получить оттуда контент.
Мы будем просто вываливать разметку в браузер, точней, в отдельные поля <textarea> для каждого URL-адреса, вместо этого в реальном приложении вы будете выполнять с кодом найденных страниц какой-то дополнительный разбор.
Если хочется обойтись одной функцией, для начала она может выглядеть так:
<?php set_time_limit(60); function crawlURL ($url, $depth = 2) { //Аргументы - URL и глубина сканирования static $seen = array(); if (isset($seen[$url]) || $depth === 0) { return; //URL уже проиндексирован, не повторяем } $seen[$url] = true; //проиндексировано! $dom = new DOMDocument('1.0'); //Удобнее всего - через объект DOMDocument @$dom->loadHTMLFile($url); $anchors = $dom->getElementsByTagName('a'); //Получить все ссылки <a> foreach ($anchors as $element) { //И перебрать их $href = $element->getAttribute('href'); //Получить из <a> атрибут href if (strpos($href, 'http')!==0) { //Разбираем полные URL $path = '/' . ltrim($href, '/'); if (extension_loaded('http')) { $href = http_build_url($url, array('path' => $path)); } else { $parts = parse_url($url); $href = $parts['scheme'] . '://'; if (isset($parts['user']) && isset($parts['pass'])) { $href .= $parts['user'] . ':' . $parts['pass'] . '@'; } $href .= $parts['host']; if (isset($parts['port'])) { $href .= ':' . $parts['port']; } $href .= $path; } } crawlURL($href, $depth - 1); } $html = @$dom->saveHTML(); $html = htmlspecialchars($html, ENT_QUOTES | ENT_SUBSTITUTE, 'Windows-1251'); //!!! if (!empty($html)) echo '<br>'.PHP_EOL.$url.' <textarea>'.PHP_EOL.$html.'</textarea>'; } crawlURL('http://nickolay.info', 2); ?>
В последней строке - вызов функции для головной страницы моего домашнего сайта. Обратите также внимание на строчку с комментарием //!!!
в конце листинга. Начиная с PHP 5.4 существенно правильное указание кодировки и опций для функции htmlspecialchars, иначе можно получить на выходе пустую строку.
Если найденный контент никуда выводить не нужно, то действие с htmlspecialchars
лишнее.
Ну и всегда бывает удобнее написать отдельный класс, приведу код несложного класса-краулера crawler.php
<?php class Crawler { private $depth = 2; private $url; private $results = array(); private $same_host = false; private $host; public function setDepth($depth) { $this->depth = $depth; } public function setHost($host) { $this->host = $host; } public function getResults() { return $this->results; } public function setSameHost($same_host) { $this->same_host = $same_host; } public function setUrl($url) { $this->url = $url; $this->setHost ($this->getHostFromUrl($url)); } public function __construct($url = null, $depth = null, $same_host = false) { //Аргументы конструктора: URL, глубина сканирования, сканировать ли только этот хост if (!empty($url)) $this->setUrl($url); if (isset($depth) && !is_null($depth)) $this->setDepth($depth); $this->setSameHost($same_host); } public function crawl() { //Основной вызываемый метод if (empty($this->url)) throw new \Exception('URL not set!'); $this->_crawl ($this->url, $this->depth); return $this->results; } private function _crawl($url, $depth) { //Приватная функция сканирования static $seen = array(); if (empty($url)) return; if (!$url = $this->buildUrl($this->url, $url)) return; if ($depth === 0 || isset($seen[$url])) return; $seen[$url] = true; $dom = new \DOMDocument('1.0'); @$dom->loadHTMLFile($url); $this->results[] = array ( //Массив результатов сканирования 'url' => $url, 'content' => @$dom->saveHTML() ); $anchors = $dom->getElementsByTagName('a'); foreach ($anchors as $element) { if (!$href = $this->buildUrl($url, $element->getAttribute('href'))) continue; $this->_crawl($href, $depth - 1); } return $url; } private function buildUrl($url, $href) { //Построение URL $url = trim($url); $href = trim($href); if (strpos($href, 'http')!==0) { //Не сканируем яваскрипт и внутренние якоря: if (strpos($href, 'javascript:')===0 || strpos($href, '#')===0) return false; //Остальное смотрим: $path = '/' . ltrim($href, '/'); if (extension_loaded('http')) $new_href = http_build_url($url, array('path' => $path), HTTP_URL_REPLACE, $parts); else { $parts = parse_url($url); $new_href = $this->buildUrlFromParts($parts); $new_href .= $path; } //Относительные адреса, типа ./page.php if (strpos($href, './') && !empty($parts['path'])===0) { //Путь не заканчивантся слешем if (!preg_match('@/$@', $parts['path'])) { $path_parts = explode('/', $parts['path']); array_pop ($path_parts); $parts['path'] = implode('/', $path_parts) . '/'; } $new_href = $this->buildUrlFromParts($parts) . $parts['path'] . ltrim($href, './'); } $href = $new_href; } if ($this->same_host && $this->host != $this->getHostFromUrl($href)) return false; return $href; } private function buildUrlFromParts($parts) { $new_href = $parts['scheme'] . '://'; if (isset($parts['user']) && isset($parts['pass'])) $new_href .= $parts['user'] . ':' . $parts['pass'] . '@'; $new_href .= $parts['host']; if (isset($parts['port'])) $new_href .= ':' . $parts['port']; return $new_href; } private function getHostFromUrl($url) { $parts = parse_url($url); preg_match ("@([^/.]+)\.([^.]{2,6}(?:\.[^.]{2,3})?)$@", $parts['host'], $host); return array_shift($host); } } ?>
и пример вызова класса, скажем, из файла index.php
, находящегося в той же папке
<?php set_time_limit(60); require_once 'crawler.php'; $c = new Crawler("http://nickolay.info", 2, true); $results = $c->crawl(); foreach ($results as $item) { $html = htmlspecialchars($item['content'], ENT_QUOTES | ENT_SUBSTITUTE, 'Windows-1251'); //!!! echo PHP_EOL.'<br>'.$item['url'].' <textarea>'.$html.'</textarea>'; } ?>
В классе удобно то, что можно ограничить сканирование только текущим хостом (третий параметр вызова конструктора).
При запуске скрипта нужно учесть, что он может потребовать немалого процессорного времени, особенно исполняясь в один поток. Есть в сети и гораздо более мощные проекты по теме.
Проверено на локалхосте с Denwer и PHP 5.5, сработало.
09.09.2017, 10:59 [4403 просмотра]