web-dev-qa-db-fra.com

Comment créer un robot d'exploration simple en PHP?

J'ai une page Web avec un tas de liens. Je veux écrire un script qui viderait toutes les données contenues dans ces liens dans un fichier local.

Est-ce que quelqu'un a fait ça avec PHP? Les directives générales et les pièges suffiraient comme réponse.

64
Kshitij Saxena -KJ-

Meh Don't analyse HTML avec les expressions rationnelles .

Voici une version DOM inspirée de Tatu:

<?php
function crawl_page($url, $depth = 5)
{
    static $seen = array();
    if (isset($seen[$url]) || $depth === 0) {
        return;
    }

    $seen[$url] = true;

    $dom = new DOMDocument('1.0');
    @$dom->loadHTMLFile($url);

    $anchors = $dom->getElementsByTagName('a');
    foreach ($anchors as $element) {
        $href = $element->getAttribute('href');
        if (0 !== strpos($href, 'http')) {
            $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 .= dirname($parts['path'], 1).$path;
            }
        }
        crawl_page($href, $depth - 1);
    }
    echo "URL:",$url,PHP_EOL,"CONTENT:",PHP_EOL,$dom->saveHTML(),PHP_EOL,PHP_EOL;
}
crawl_page("http://hobodave.com", 2);

Edit: J'ai corrigé quelques bugs de la version de Tatu (fonctionne maintenant avec les URL relatives).

Edit: J'ai ajouté une nouvelle fonctionnalité qui l'empêche de suivre deux fois la même URL.

Edit: renvoie maintenant la sortie à STDOUT afin que vous puissiez la rediriger vers le fichier de votre choix

Edit: Correction d'un bug signalé par George dans sa réponse. Les URL relatives ne seront plus ajoutées à la fin du chemin, mais écrasées. Merci à George pour cela. Notez que la réponse de George ne prend en compte aucun des éléments suivants: https, utilisateur, passe ou port. Si vous avez l'extension http PECL chargée, ceci est tout simplement fait en utilisant http_build_url . Sinon, je dois manuellement coller ensemble en utilisant parse_url. Merci encore George.

89
hobodave

Voici ma mise en œuvre basée sur l'exemple/réponse ci-dessus.

  1. C'est basé sur la classe
  2. utilise Curl
  3. supporte HTTP Auth
  4. Ignorer l'URL n'appartenant pas au domaine de base
  5. Renvoyer le code de réponse en-tête HTTP pour chaque page
  6. Heure de retour pour chaque page

CLASSE AU CRAWL:

class crawler
{
    protected $_url;
    protected $_depth;
    protected $_Host;
    protected $_useHttpAuth = false;
    protected $_user;
    protected $_pass;
    protected $_seen = array();
    protected $_filter = array();

    public function __construct($url, $depth = 5)
    {
        $this->_url = $url;
        $this->_depth = $depth;
        $parse = parse_url($url);
        $this->_Host = $parse['Host'];
    }

    protected function _processAnchors($content, $url, $depth)
    {
        $dom = new DOMDocument('1.0');
        @$dom->loadHTML($content);
        $anchors = $dom->getElementsByTagName('a');

        foreach ($anchors as $element) {
            $href = $element->getAttribute('href');
            if (0 !== strpos($href, 'http')) {
                $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;
                }
            }
            // Crawl only link that belongs to the start domain
            $this->crawl_page($href, $depth - 1);
        }
    }

    protected function _getContent($url)
    {
        $handle = curl_init($url);
        if ($this->_useHttpAuth) {
            curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
            curl_setopt($handle, CURLOPT_USERPWD, $this->_user . ":" . $this->_pass);
        }
        // follows 302 redirect, creates problem wiht authentication
//        curl_setopt($handle, CURLOPT_FOLLOWLOCATION, TRUE);
        // return the content
        curl_setopt($handle, CURLOPT_RETURNTRANSFER, TRUE);

        /* Get the HTML or whatever is linked in $url. */
        $response = curl_exec($handle);
        // response total time
        $time = curl_getinfo($handle, CURLINFO_TOTAL_TIME);
        /* Check for 404 (file not found). */
        $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);

        curl_close($handle);
        return array($response, $httpCode, $time);
    }

    protected function _printResult($url, $depth, $httpcode, $time)
    {
        ob_end_flush();
        $currentDepth = $this->_depth - $depth;
        $count = count($this->_seen);
        echo "N::$count,CODE::$httpcode,TIME::$time,DEPTH::$currentDepth URL::$url <br>";
        ob_start();
        flush();
    }

    protected function isValid($url, $depth)
    {
        if (strpos($url, $this->_Host) === false
            || $depth === 0
            || isset($this->_seen[$url])
        ) {
            return false;
        }
        foreach ($this->_filter as $excludePath) {
            if (strpos($url, $excludePath) !== false) {
                return false;
            }
        }
        return true;
    }

    public function crawl_page($url, $depth)
    {
        if (!$this->isValid($url, $depth)) {
            return;
        }
        // add to the seen URL
        $this->_seen[$url] = true;
        // get Content and Return Code
        list($content, $httpcode, $time) = $this->_getContent($url);
        // print Result for current Page
        $this->_printResult($url, $depth, $httpcode, $time);
        // process subPages
        $this->_processAnchors($content, $url, $depth);
    }

    public function setHttpAuth($user, $pass)
    {
        $this->_useHttpAuth = true;
        $this->_user = $user;
        $this->_pass = $pass;
    }

    public function addFilterPath($path)
    {
        $this->_filter[] = $path;
    }

    public function run()
    {
        $this->crawl_page($this->_url, $this->_depth);
    }
}

USAGE:

// USAGE
$startURL = 'http://YOUR_URL/';
$depth = 6;
$username = 'YOURUSER';
$password = 'YOURPASS';
$crawler = new crawler($startURL, $depth);
$crawler->setHttpAuth($username, $password);
// Exclude path with the following structure to be processed 
$crawler->addFilterPath('customer/account/login/referer');
$crawler->run();
16
WonderLand

Départ PHP Crawler

http://sourceforge.net/projects/php-crawler/

Voyez si ça aide.

11
GeekTantra

Dans sa forme la plus simple:

function crawl_page($url, $depth = 5) {
    if($depth > 0) {
        $html = file_get_contents($url);

        preg_match_all('~<a.*?href="(.*?)".*?>~', $html, $matches);

        foreach($matches[1] as $newurl) {
            crawl_page($newurl, $depth - 1);
        }

        file_put_contents('results.txt', $newurl."\n\n".$html."\n\n", FILE_APPEND);
    }
}

crawl_page('http://www.domain.com/index.php', 5);

Cette fonction obtiendra le contenu d'une page, puis analysera tous les liens trouvés et sauvegardera le contenu dans 'résultats.txt'. La fonction accepte un deuxième paramètre, profondeur, qui définit la durée pendant laquelle les liens doivent être suivis. Passez-y 1 si vous voulez analyser uniquement les liens de la page donnée.

9
Tatu Ulmanen

Avec quelques petites modifications du code de hobodave , voici un code de code que vous pouvez utiliser pour explorer des pages. Cela nécessite que l'extension curl soit activée sur votre serveur.

<?php
//set_time_limit (0);
function crawl_page($url, $depth = 5){
$seen = array();
if(($depth == 0) or (in_array($url, $seen))){
    return;
}   
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
$result = curl_exec ($ch);
curl_close ($ch);
if( $result ){
    $stripped_file = strip_tags($result, "<a>");
    preg_match_all("/<a[\s]+[^>]*?href[\s]?=[\s\"\']+"."(.*?)[\"\']+.*?>"."([^<]+|.*?)?<\/a>/", $stripped_file, $matches, PREG_SET_ORDER ); 
    foreach($matches as $match){
        $href = $match[1];
            if (0 !== strpos($href, 'http')) {
                $path = '/' . ltrim($href, '/');
                if (extension_loaded('http')) {
                    $href = http_build_url($href , array('path' => $path));
                } else {
                    $parts = parse_url($href);
                    $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;
                }
            }
            crawl_page($href, $depth - 1);
        }
}   
echo "Crawled {$href}";
}   
crawl_page("http://www.sitename.com/",3);
?>

J'ai expliqué ce tutoriel dans ce tutoriel de script de robot

5
Team Webgalli

Pourquoi utiliser PHP pour cela, quand vous pouvez utiliser wget , par ex.

wget -r -l 1 http://www.example.com

Pour savoir comment analyser le contenu, voir Meilleures méthodes pour analyser HTML et utiliser la fonction de recherche pour exemples . Comment analyser le langage HTML a fait l'objet d'une réponse à plusieurs reprises auparavant.

5
Gordon

Hobodave vous étiez très proche. La seule chose que j'ai modifiée est dans l'instruction if qui vérifie si l'attribut href de la balise d'ancrage trouvée commence par "http". Au lieu d'ajouter simplement la variable $ url qui contiendrait la page qui a été passée, vous devez d'abord la supprimer de l'hôte, ce qui peut être fait à l'aide de la fonction php parse_url.

<?php
function crawl_page($url, $depth = 5)
{
  static $seen = array();
  if (isset($seen[$url]) || $depth === 0) {
    return;
  }

  $seen[$url] = true;

  $dom = new DOMDocument('1.0');
  @$dom->loadHTMLFile($url);

  $anchors = $dom->getElementsByTagName('a');
  foreach ($anchors as $element) {
    $href = $element->getAttribute('href');
    if (0 !== strpos($href, 'http')) {
       /* this is where I changed hobodave's code */
        $Host = "http://".parse_url($url,PHP_URL_Host);
        $href = $Host. '/' . ltrim($href, '/');
    }
    crawl_page($href, $depth - 1);
  }

  echo "New Page:<br /> ";
  echo "URL:",$url,PHP_EOL,"<br />","CONTENT:",PHP_EOL,$dom->saveHTML(),PHP_EOL,PHP_EOL,"  <br /><br />";
}

crawl_page("http://hobodave.com/", 5);
?>
3
George

Comme mentionné, il existe des frameworks de crawler prêts à être personnalisés, mais si ce que vous faites est aussi simple que vous l'avez mentionné, vous pouvez le créer facilement.

Gratter les liens: http://www.phpro.org/examples/Get-Links-With-DOM.html

Dumping des résultats dans un fichier: http://www.tizag.com/phpT/filewrite.php

2
Jens Roland

Merci @hobodave.

Cependant, j'ai trouvé deux faiblesses dans votre code. Votre analyse de l'URL d'origine pour obtenir le segment "Host" s'arrête à la première barre oblique. Cela suppose que tous les liens relatifs commencent dans le répertoire racine. Ce n'est que vrai parfois.

original url   :  http://example.com/game/index.html
href in <a> tag:  highscore.html
author's intent:  http://example.com/game/highscore.html  <-200->
crawler result :  http://example.com/highscore.html       <-404->

résoudre ce problème en cassant à la dernière barre oblique pas la première

un deuxième bogue sans rapport, est-ce que $depth _ ne suit pas vraiment la profondeur de récursivité, il suit largeur du premier niveau de récursivité.

Si je croyais que cette page était en cours d'utilisation, je pourrais déboguer ce deuxième numéro, mais je suppose que le texte que je suis en train d'écrire ne sera jamais lu par personne, humain ou robot, car ce numéro date de six ans et je n'en ai même pas assez. réputation d’avertir directement + hobodave de ces défauts en les faisant figurer sur son code. Merci quand même hobodave.

1
Dov Jacobson

J'ai utilisé le code de @ hobodave, avec ce petit Tweak pour empêcher la ré-exploration de toutes les variantes de fragments de la même URL:

<?php
function crawl_page($url, $depth = 5)
{
  $parts = parse_url($url);
  if(array_key_exists('fragment', $parts)){
    unset($parts['fragment']);
    $url = http_build_url($parts);
  }

  static $seen = array();
  ...

Ensuite, vous pouvez également omettre la ligne $parts = parse_url($url); dans la boucle for.

1
pasqal

Vous pouvez essayer cela peut être une aide pour vous

$search_string = 'american golf News: Fowler beats stellar field in Abu Dhabi';
$html = file_get_contents(url of the site);
$dom = new DOMDocument;
$titalDom = new DOMDocument;
$tmpTitalDom = new DOMDocument;
libxml_use_internal_errors(true);
@$dom->loadHTML($html);
libxml_use_internal_errors(false);
$xpath = new DOMXPath($dom);
$videos = $xpath->query('//div[@class="primary-content"]');
foreach ($videos as $key => $video) {
$newdomaindom = new DOMDocument;    
$newnode = $newdomaindom->importNode($video, true);
$newdomaindom->appendChild($newnode);
@$titalDom->loadHTML($newdomaindom->saveHTML());
$xpath1 = new DOMXPath($titalDom);
$titles = $xpath1->query('//div[@class="listingcontainer"]/div[@class="list"]');
if(strcmp(preg_replace('!\s+!',' ',  $titles->item(0)->nodeValue),$search_string)){     
    $tmpNode = $tmpTitalDom->importNode($video, true);
    $tmpTitalDom->appendChild($tmpNode);
    break;
}
}
echo $tmpTitalDom->saveHTML();
1
Niraj patel

Je suis venu avec le code d'araignée suivant. Je l'ai adapté un peu comme suit: PHP - Existe-t-il un moyen sûr d'effectuer une récursion profonde? cela semble assez rapide ....

    <?php
function  spider( $base_url , $search_urls=array() ) {
    $queue[] = $base_url;
    $done           =   array();
    $found_urls     =   array();
    while($queue) {
            $link = array_shift($queue);
            if(!is_array($link)) {
                $done[] = $link;
                foreach( $search_urls as $s) { if (strstr( $link , $s )) { $found_urls[] = $link; } }
                if( empty($search_urls)) { $found_urls[] = $link; }
                if(!empty($link )) {
echo 'LINK:::'.$link;
                      $content =    file_get_contents( $link );
//echo 'P:::'.$content;
                    preg_match_all('~<a.*?href="(.*?)".*?>~', $content, $sublink);
                    if (!in_array($sublink , $done) && !in_array($sublink , $queue)  ) {
                           $queue[] = $sublink;
                    }
                }
            } else {
                    $result=array();
                    $return = array();
                    // flatten multi dimensional array of URLs to one dimensional.
                    while(count($link)) {
                         $value = array_shift($link);
                         if(is_array($value))
                             foreach($value as $sub)
                                $link[] = $sub;
                         else
                               $return[] = $value;
                     }
                     // now loop over one dimensional array.
                     foreach($return as $link) {
                                // echo 'L::'.$link;
                                // url may be in form <a href.. so extract what's in the href bit.
                                preg_match_all('/<a[^>]+href=([\'"])(?<href>.+?)\1[^>]*>/i', $link, $result);
                                if ( isset( $result['href'][0] )) { $link = $result['href'][0]; }
                                // add the new URL to the queue.
                                if( (!strstr( $link , "http")) && (!in_array($base_url.$link , $done)) && (!in_array($base_url.$link , $queue)) ) {
                                     $queue[]=$base_url.$link;
                                } else {
                                    if ( (strstr( $link , $base_url  ))  && (!in_array($base_url.$link , $done)) && (!in_array($base_url.$link , $queue)) ) {
                                         $queue[] = $link;
                                    }
                                }
                      }
            }
    }


    return $found_urls;
}    


    $base_url       =   'https://www.houseofcheese.co.uk/';
    $search_urls    =   array(  $base_url.'acatalog/' );
    $done = spider( $base_url  , $search_urls  );

    //
    // RESULT
    //
    //
    echo '<br /><br />';
    echo 'RESULT:::';
    foreach(  $done as $r )  {
        echo 'URL:::'.$r.'<br />';
    }
0
Ian

C'est une vieille question. Beaucoup de bonnes choses sont arrivées depuis. Voici mes deux cents sur ce sujet:

  1. Pour suivre avec précision les pages visitées, vous devez d’abord normaliser l’URI. L'algorithme de normalisation comprend plusieurs étapes:

    • Trier les paramètres de requête. Par exemple, les URI suivants sont équivalents après la normalisation: GET http://www.example.com/query?id=111&cat=222 GET http://www.example.com/query?cat=222&id=111
    • Convertir le chemin vide. Exemple: http://example.org → http://example.org/

    • Capitaliser le codage en pourcentage. Toutes les lettres dans un triplet codant en pourcentage (par exemple, "% 3A") sont sensibles à la casse. Exemple: http://example.org/a%c2%B1b → http://example.org/a%C2%B1b

    • Supprimez les segments de points inutiles. Exemple: http://example.org/../a/b/../c/./d.html → http://example.org/a/c/d.html

    • Éventuellement d'autres règles de normalisation

  2. Non seulement la balise <a> A href attribut, mais la balise <area> L'a aussi https://html.com/tags/area/ . Si vous ne voulez rien manquer, vous devez aussi gratter la balise <area>.

  3. Suivre les progrès de l'exploration. Si le site Web est petit, ce n'est pas un problème. Au contraire, cela pourrait être très frustrant si vous explorez la moitié du site et que cela échoue. Pensez à utiliser une base de données ou un système de fichiers pour enregistrer la progression.

  4. Soyez gentil avec les propriétaires du site. Si vous envisagez d'utiliser votre robot d'exploration en dehors de votre site Web, vous devez utiliser des délais. Sans délais, le script est trop rapide et risque de ralentir considérablement certains petits sites. Du point de vue des administrateurs système, cela ressemble à une attaque par déni de service. Un délai statique entre les demandes fera l'affaire.

Si vous ne voulez pas en parler, essayez Crawlzone et faites-moi part de vos commentaires. Consultez également l’article que j’ai écrit il ya longtemps https://www.codementor.io/zstate/this-is-how-i-crawl-n98s6myxm

0
zstate

J'ai créé une petite classe pour récupérer les données de l'URL fournie, puis extraire les éléments HTML de votre choix. La classe utilise CURL et DOMDocument.

php class:

class crawler {


   public static $timeout = 2;
   public static $agent   = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';


   public static function http_request($url) {
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL,            $url);
      curl_setopt($ch, CURLOPT_USERAGENT,      self::$agent);
      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::$timeout);
      curl_setopt($ch, CURLOPT_TIMEOUT,        self::$timeout);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      $response = curl_exec($ch);
      curl_close($ch);
      return $response;
   }


   public static function strip_whitespace($data) {
      $data = preg_replace('/\s+/', ' ', $data);
      return trim($data);
   }


   public static function extract_elements($tag, $data) {
      $response = array();
      $dom      = new DOMDocument;
      @$dom->loadHTML($data);
      foreach ( $dom->getElementsByTagName($tag) as $index => $element ) {
         $response[$index]['text'] = self::strip_whitespace($element->nodeValue);
         foreach ( $element->attributes as $attribute ) {
            $response[$index]['attributes'][strtolower($attribute->nodeName)] = self::strip_whitespace($attribute->nodeValue);
         }
      }
      return $response;
   }


}

exemple d'utilisation:

$data  = crawler::http_request('https://stackoverflow.com/questions/2313107/how-do-i-make-a-simple-crawler-in-php');
$links = crawler::extract_elements('a', $data);
if ( count($links) > 0 ) {
   file_put_contents('links.json', json_encode($links, JSON_PRETTY_PRINT));
}

exemple de réponse:

[
    {
        "text": "Stack Overflow",
        "attributes": {
            "href": "https:\/\/stackoverflow.com",
            "class": "-logo js-gps-track",
            "data-gps-track": "top_nav.click({is_current:false, location:2, destination:8})"
        }
    },
    {
        "text": "Questions",
        "attributes": {
            "id": "nav-questions",
            "href": "\/questions",
            "class": "-link js-gps-track",
            "data-gps-track": "top_nav.click({is_current:true, location:2, destination:1})"
        }
    },
    {
        "text": "Developer Jobs",
        "attributes": {
            "id": "nav-jobs",
            "href": "\/jobs?med=site-ui&ref=jobs-tab",
            "class": "-link js-gps-track",
            "data-gps-track": "top_nav.click({is_current:false, location:2, destination:6})"
        }
    }
]
0
Jake

Il est bon de se rappeler que lors de l’exploration de liens externes (j’apprécie le fait que le PO se rapporte à la propre page d’un utilisateur), vous devez connaître le fichier robots.txt. J'ai trouvé ce qui suit qui, espérons-le, aidera http://www.the-art-of-web.com/php/parse-robots/ .

0
Antony