БлогNot. PHP: как написать простейший краулер

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 просмотра]


теги: программирование php поиск

К этой статье пока нет комментариев, Ваш будет первым