Aller encore plus vite en utilisant cURL en multithreading

Si vous ne comprenez pas tout du titre, n’ayez pas peur il n’y a rien de bien méchant dans tout ça.

Mon premier article présentait ma petite classe SimpleCURL qui permet de pouvoir récupérer du contenu distant facilement tout en utilisant des proxy et user-agents.
Dans un des commentaires il m’a été précisé qu’il était dommage que à cause de cette classe on perdait alors l’usage de multi-curl qui permet de gérer le multithreading avec curl.

A quoi çà sert concrètement le multithreading ?

Le multithreading ou exécution en parallèle permet de pouvoir optimiser les ressources que vous fournis votre processeur. Cela ne vous parlera pas forcément, alors je ne vais pas rentrer dans les détails techniques.
Donc au final, le fait d’utiliser des requêtes en parallèles permet d’améliorer le temps d’exécution.

Jusqu’à présent je n’avais jamais utilisé cette fonctionnalité de cURL, alors j’ai modifié ma classe SimpleCURL pour qu’elle puisse prendre en compte le multithreading.
En suite, j’ai fait des tests pour voir les temps d’exécution des deux méthodes, et les résultats ont été très satisfaisant.

L’expérience pour mesurer les performances est très simple, d’un coté j’exécute 5 requêtes de façon séquentielle, c’est à dire que les requêtes se font les unes après les autres. Et de l’autre j’exécute 5 requêtes simultanément.
Pour que les résultats soient plus précis j’ai effectué chaque expérience 500 fois.

Et voici les résultats :

  • En séquentiel : 155 secondes
  • En utilisant le multithreading : 73 secondes

On divise presque par deux le temps d’exécution entre chaque méthode, ce qui est non négligeable.

Maintenant place à la partie technique avec le code source, et la petite démo.

Nouvelle version de SimpleCURL

Allez, je vous la donne enfin cette classe mise à jour.

<?php
/**
 * Gérez facilement vos proxies et user-agents lors de vos sessions de scrap avec SimpleCURL
 * Prise en compte du multi-threading
 * Guillaume Desbieys
 * http://www.guillaumedesbieys.com
 * Version 1.1
 */
class SimpleCURL
{
	/**
	* @var (Array)
	* @desc Liste des proxy
	*/
	private $proxy = array();
	
	/**
	* @var (String)
	* @desc Proxy a utiliser
	*/
	private $my_proxy;
	
	/**
	* @var (String)
	* @desc Identifiants du proxy sous la forme user:password
	*/
	private $proxy_auth;
	
	/**
	* @var (Array)
	* @desc Referer utilise, si vide pas de référer utilise
	*/
	private $referer = array();
	
	/**
	* @var (Array)
	* @desc Liste des users agents
	*/
	private $user_agent = array();
	
	/**
	* @var (String)
	* @desc User agent a utiliser
	*/
	private $my_user_agent;
	
	/**
	* @var (Array)
	* @desc Entete headers a envoyer
	*/
	private $header = array();
	
	/**
	* @var (String)
	* @desc Nom du fichier contenant les cookies, si vide pas de gestion des cookies
	*/
	private $cookies;
	
	/**
	* @var (String)
	* @desc Contenu recupere par la requete
	*/
	private $page_content;
	
	/**
	* @var (Array)
	* @desc Donnees POST a envoyer
	*/
	private $post = array();
	
	
	/**
	* @var (Boolean)
	* @desc Gestion du multithreading
	*/
	private $multi_thread;
	
	/**
	* @var (cURL multi handler)
	* @desc Gestionnaire des threads
	*/
	private $multi_handler;
	
	/**
	* @var (Array)
	* @desc Liste des threads
	*/
	private $threads = array();
	
	
	public function init(){
		$this->my_user_agent = $this->randomUserAgent();
		$this->my_proxy = $this->randomProxy();
	}
	
	public function initFromFile($file_proxy, $file_UA){
	
		if (!empty($file_proxy)){
			if (file_exists($file_proxy)){
				$handle = fopen($file_proxy, "rb");
			
				while (($buffer = fgets($handle, 4096)) !== false){
					$buffer = trim($buffer);
					preg_match("#([^:]+:[^:]+)[:]*(.*)#", $buffer, $match);
									
					$proxy = $match[1];
					$auth = $match[2];
					$this->addProxy($proxy);	
					
					if (!empty($auth))
						$this->setProxyAuth($auth);
				}
				
				fclose($handle);
			}else{
				echo "Le fichier $file_proxy est introuvable !";
			}
		}
		
		if (!empty($file_UA)){
			if (file_exists($file_UA)){
				$handle = fopen($file_UA, "rb");
			
				while (($buffer = fgets($handle, 4096)) !== false){
					$buffer = trim($buffer);
					$this->addUserAgent($buffer);	
				}
    		
				fclose($handle);
			}else{
				echo "Le fichier $file_UA est introuvable !";
			}
		}
		
		$this->init();
	}
	
	public function setProxy($proxy){
		$this->proxy = $proxy;
	}
	
	public function setProxyAuth($ident){
		$this->proxy_auth = $ident;
	}
	
	public function addProxy($proxy){
		array_push($this->proxy, $proxy);
	}
	
	public function setHeader($head){
		$this->header = $head;
	}
	
	public function addHeader($head){
		array_push($this->header, $head);
	}
	
	public function setCookies($file){
		if (!is_file(__PATH__ . $file))
			touch(__PATH__ . $file);
			
		$this->cookies = __PATH__ . $file;
	}
	
	public function setReferer($ref){
		$this->referer = $ref;
	}
	
	public function addReferer($ref){
		array_push($this->referer, $ref);
	}
	
	public function addPost($data){
		if (sizeof($data) == 2 && is_array($data))
			$this->post[$data[0]] = $data[1];
	}
	
	public function setUserAgent($UA){
		$this->user_agent = $UA;
	}
	
	public function reloadCookies(){
		if (is_file($this->cookies)){
			$f = fopen($this->cookies,"w");
			ftruncate($f,0);
		}
	}
	
	public function deleteCookies(){
		if (is_file($this->cookies)){
			@unlink(__PATH__ . $this->cookies);
		}
	}
	
	public function addUserAgent($UA){
		array_push($this->user_agent, $UA);
	}
	
	public function getPageContent(){
		return $this->page_content;
	}
	
	private function randomProxy(){
		if (sizeof($this->proxy)>0)
			return $this->proxy[rand(0, (sizeof($this->proxy)-1))];
		else
			return null;
	}
	
	private function randomUserAgent(){
		if (sizeof($this->user_agent)>0)
			return $this->user_agent[rand(0, (sizeof($this->user_agent)-1))];
		else 
			return null;
	}
	
	public function setMultiThread($bool){
		$this->multi_thread = $bool;
		
		if ($bool){
			$this->multi_handler = curl_multi_init();
		}
	}
	
	public function curl($URL){
		
		$ch = curl_init($URL); 
		curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); 
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		
		
		/**
		* Gestion du multithreading
		*/
		if ($this->multi_thread){
			curl_multi_add_handle($this->multi_handler, $ch);
		}
		
		/**
		* Gestion du proxy
		*/
		if (!empty($this->my_proxy)){
			curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, true); 
			curl_setopt($ch, CURLOPT_PROXY, $this->my_proxy); 
			
			if (!empty($this->proxy_auth)){
				curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_auth);
			}
		} 
		
		/**
		* Gestion des cookies
		*/
		if (!empty($this->cookies)){
			curl_setopt ($ch, CURLOPT_COOKIEJAR, $this->cookies); 
			curl_setopt ($ch, CURLOPT_COOKIEFILE, $this->cookies); 
		}
		
		
		/**
		* Gestion des headers
		*/
		if (sizeof($this->post) > 0){
			curl_setopt($ch, CURLOPT_POST, true);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $this->post);
		}
		
		if (sizeof($this->header) > 0){
			curl_setopt($ch, CURLOPT_HTTPHEADER, $this->header);
		}
		
		if (!empty($this->referer)){
			curl_setopt($ch, CURLOPT_REFERER, $this->referer);
		}
		
		if (!empty($this->my_user_agent)){
			curl_setopt($ch, CURLOPT_USERAGENT, $this->my_user_agent);
		}
		
		if (!$this->multi_thread)
			$this->page_content = curl_exec($ch); 
		
		curl_close($ch);

		return $ch;
	}
	
	public function curlMulti($URL, $reload = false){
	
		if ($reload){
			$this->init();
		}
		
		array_push($this->threads, $this->curl($URL));
		
	}
	
	/*
	* Execute les threads
	*/
	public function startMulti($callback){
		if ($this->multi_thread){
		
			$running = null;
			
			do{
				curl_multi_exec($this->multi_handler, $running);
			}while($running > 0);
			
			$i = 0;
			
			foreach($this->threads as $thread){
				$html[$i] = curl_multi_getcontent($thread);
				
				call_user_func($callback, $html[$i]);
				//echo $html[$i];
				$i++;
			}
			
			foreach($this->threads as $thread){
				curl_multi_remove_handle($this->multi_handler, $thread);
			}
			
			curl_multi_close($this->multi_handler);
		}
	}

}
?>

Attendez ! Ne partez pas si vite, vous allez bien resté un peu pour voir comment elle marche.

Exemple et fonctionnement

Cet exemple permet juste de vous montrez comment utiliser SimpleCURL en utilisant le multithreading.
Je vais vous donnez le code de l’exemple et en suite je vais le commenter un peu.

<?php
include "SimpleCURL.php";

$curl = new SimpleCURL();
$curl->initFromFile("proxy.txt","UA.txt");

function afficher($html){                       
	echo $html;
}

$curl->setMultiThread(true);

$curl->curlMulti("http://www.monip.org/");
$curl->curlMulti("http://www.monip.org/", true);
$curl->curlMulti("http://www.monip.org/", true);

$curl->startMulti("afficher");
?>

Pour le début rien n’a changé, cependant maintenant nous voulons utiliser le multithreading et pour cela il faut l’activer en utilisant le code suivant $curl->setMultiThread(true);

En suite il faut renseigner la page que l’on veut atteindre via cette ligne $curl->curlMulti("http://www.monip.org/");
Attention maintenant on n’utilise plus curl() mais curlMulti(), cela aura pour effet de mettre l’url dans une liste et le manager s’occupera de l’exécuter quand il faudra.

Bon c’est bien mais si c’est pour faire une seule requête utiliser le multithreading c’est pas vraiment terrible. Alors ajoutons en une deuxième, et puis allez soyons fou même une troisième.
Là vous pouvez remarquer qu’un « true » est apparu, il permet de spécifier qu’il faut recharger un proxy et un user-agent. Si vous voulez garder le même proxy tout du long il suffit juste d’enlever le true.

Maintenant il faut que les requêtes soient exécutées et le code suivant est là pour ça.$curl->startMulti("afficher");
Ce code vas faire en sorte que vos requêtes soient exécutées en même temps (ici on a 3 requêtes simultanées).

Faire une requête c’est bien, mais on voudrais peut être récupérer le contenu de la page quand même. C’est là que la fonction afficher() entre en jeu.
C’ est une fonction callback qui sera appelée à chaque fois qu’une requête est terminée.
C’est dans cette fonction là que vous pourrez y faire votre traitement.

Dans l’exemple cette fonction ne fait que afficher le contenu de la page, mais vous pouvez y mettre ce que vous voulez comme des regex pour récupérer seulement des bouts de la page.

Dans un prochain article j’utiliserais cette classe pour montrer un exemple concret de son utilisation. Parce que c’est bien beau d’aller sur un site pour connaitre une adresse IP, mais il y a des moyens plus simple pour cela.

21 commentaires

Ajouter un commentaire
  • Thomas Répondre

    J’utilise une classe similaire dans un projet mais sans multithreading ni user-agent aléatoire.
    Je vais donc la remplacer par la tienne, merci 😉

  • Guillaume from Angkor Wat Répondre

    merci beaucoup, tu viens de me faire gagner pas mal de temps, en codage et en temps de parsage 🙂

  • Guillaume from Blog geek Répondre

    Merci Guillaume pour toutes ces infos 🙂

    Tu connais un endroit où l’on peut obtenir une liste de proxy qui fonctionnent ? Ou comment vérifier qu’ils fonctionnent en PHP ?

    Merci !

    • Guillaume

      Moi perso j’utilise des proxies privés j’en ai 10 et j’en suis très content, je paye 10$ / mois sur Buyproxies du coup les proxies marchent donc je n’ai pas besoin de les tester. Sinon en faisant une recherche sur Google tu peux trouver des sites qui te proposent des proxies public gratuitement mais ce genre de proxy est sur-utilisé, souvent lent. L’utilisation d’un proxy public ne dure généralement pas longtemps. Mais sinon çà pourrait être une idée, je pourrais faire un article qui propose un script qui permet de tester ses proxies et voire même d’en récupérer sur internet.

  • Guillaume from Développeur web Répondre

    Ca, ça serait génial :p

    J’ai trouvé pléthore de proxy sur le web oui, mais aucun qui semblait marcher visiblement. Donc je me tâtais à développer un script qui testerait mais niveau consommation de ressources et rapidité, pas sûr que le script soit idéal !

  • Jeromeweb Répondre

    Sympa l’idée de random des users agents et des proxys.
    Je n’ai pas testé mais en lisant ton code je pense qu’il y a une erreur : double $ dans « le fichier $$file_UA est introuvable »

    • Guillaume

      En effet ! Merci de l’avoir signalée 😉

  • LostSEO Répondre

    Salut Guillaume, merci pour le partage j’apprends beaucoup grâce à tes articles. Super intéressant, sinon l’idée d’un User agent est intéressante mais quelles sont les risques liés à un UA fixe au final ? Il y a peu de risque non ?
    Sinon la fonction afficher me donne mal à la tête… Je comprends pas tout je vais essayer de voir ça en détail. Aussi pour récupérer es résultat, je préfère utiliser XPath, parfois plus facile à utiliser que le regex.

    Il serait aussi intéressant d’insérer une fonction pour temporiser (au choix) le crawl afin de le rendre plus naturel.

    Dernière chose, je suis un peu un programmeur du dimanche et utilise notepad++, quel ide Php me conseilles tu (je suis amateur)?

    • Guillaume

      Alors concernant l’UA fixe ça dépend ce que tu scrappe. Si tu scrappe les résultats de Google il vaut mieux utiliser diverses IP et UA pour pas qu’on ai une captcha. Mais normalement des proxys suffisent. Mais ajouter un UA permet de faire « plus naturel ».
      Et pour l’IDE ça dépend vraiment des gouts et utilités… Si c’est pour du développement occasionnel tu n’a pas besoin forcément d’un IDE ultra complet avec débogueur et compagnie… Donc Notepad++ suffit largement, je l’ai longtemps utilisé mais maintenant je fait du développement quasi exclusivement sous Linux. Et bien souvent un petit « IDE » me suffit, j’utilise SublimeText 2 qui est disponible sur Linux et Windows, il est gratuit et je trouve qu’il est vraiment sympa.

  • LaRose Répondre

    Salut, merci bien c’est excellent, bonne continuation 🙂

  • Pusse Répondre

    Salut,

    je trouve cette classe très intéréssante mais avant de l’utiliser et de la mettre en place, j’ai une question : y-a-t-il une contre-indication à l’utilisation du multithreading sur une seule url ? Car pour un même projet je dois scraper parfois une page et parfois plusieurs pages donc cela m’éviterait d’instancier 2 classes pour un même objectif.

    • Guillaume

      Faire du multithreading sur une seule URL ne sert à rien car une requête seule suffit. Le multithreading sert plutôt quand vous avez beaucoup de requêtes à faire, à ce moment là le multithreading va permettre d’exécuter plusieurs requête simultanément.

  • LaRose Répondre

    Bonjour,
    j’ai un problème avec le proxy, lorsque j’exécute mon code sans modifier le proxy ça fonctionne bien, mais lorsque j’utilise la modification de proxy parfois il me retourne de résultat parfois il me retourne juste le résultat de 2 ou 3 requêtes, et plusieurs fois il ne me donne rien! il ne m’affiche pas même l’erreur, une fois il m’a afficher cette erreur : Received HTTP code 503 from proxy after CONNECT
    est ce que vous avez une idée s’il vous plait sur la cause de l’erreur ?
    merci d’avance.

    • Guillaume

      Cela dépend aussi de la qualité des proxies que vous utilisez. Par exemple l’erreur 503 doit surement provenir d’un problème au niveau du proxy. Il faudrait activer le mode verbose de curl pour voir à quel endroit le problème survient.

    • LaRose

      salut, merci pour votre réponse 🙂
      bon le problème était plus simple que j’ai pensé! c’est le max_execution_time de script php, j’ai oublié totalement de voir les erreurs php, grave faute pour un développeur :/ , et je me suis concentrée sur le curl
      bon maintenant mon code fonctionne parfaitement
      merci 🙂

  • Fotowoltaika Répondre

    merci beaucoup, tu viens de me faire gagner pas mal de temps, en codage et en temps de parsage

  • Cetic Répondre

    Hello! Merci d’avance pour la Classe !

    Question : Peux t’on envisager de mettre de la même façon que les proxys, les IP à binder ? 😀

    Pour mon projet en cours, j’ai 15 IPs, droit à 6 reqs/IP/sec sur les serveurs distants. +-1500 requêtes à faire par heures pour sortir des stats. (Autorisé par le client… et pas d’accès FTP …) Bref le cahiers des charges sympa avec le client sympa qui va avec …

    – Je pensais donc a utiliser 6 URL diff / IP bindée.
    – Instancier 15x ton exemple
    – Ma fonction Afficher serais une fonction de sauvegarde des JSON pour traitement ultérieur.

    Mais à part faire une boucle qui envoi (15 x 6 URLs) genre Nombre de tour = 1500 / (15*6) … for($i=0;$i<=NbTour;$i++)…

    Ne pourrais t'on pas ajouter toutes les URLs dans une Array et indiquer à la classe le nombre de thread max ?

    Ainsi j'instance 15x avec limite à 6 threads et je partage mes URLs à travers les instances non ?

    Le but étant de prendre le moins de temps possible pour pouvoir faire tourner les statistiques ensuite… et repartir pour un tour.

    Si tu as une idée je suis preneur. (même une brève idée ^^ de toute manière j'ai mis ta classe au chaud et je vais voir pour l'exploiter au mieux dans le courant de la semaine)

    Encore merci pour ce beau boulot ^^

    • Guillaume Desbieys

      La classe ne gère pas les IPs failover mais c’est très simple de faire en sorte que ça marche, il faudrait juste stocker les IPs dans une autre variable et dans le traitement avec cURL il faudrait ajouter quelques lignes pour pouvoir utiliser non pas un proxy mais une interface donnée à la place de l’IP par défaut de ton serveur.
      Par contre il ne faudra pas que l’IP soit sélectionnée sinon si dans le pire des cas c’est toujours la même IP qui est choisie il se pourrait que ton IP se fasse bannir par le serveur distant. Le mieux serait probablement d’instancier 15 fois la classe, pour chaque instance spécifier quelle interface choisir et lui spécifier sur quelle URL travailler.

  • Rub's Répondre

    Excellent travail.

    Est-ce qu’avec tes 2 versions tu arrives a scarper les pages de résultats de recherche google ou Yahoo ?

    J’ai l’impression d’être bloqué par ces 2 sites malgré l’utilisation des proxys publique.

  • Marie-Audrey Répondre

    Bonjour,

    Merci pour cette classe 🙂

    Une question :
    Comment récupérer dans la fonction afficher($html) les variables passées dans curlMulti ?

    Exemple :

    while ($icurlMulti("http://www.exemple.com?q=".$tab_lignes[$i], true);
    $tab_lignes[$i];
    $i++;
    }

    je souhaite récupérer $tab_lignes[$i] pour l’afficher dans la fonction afficher($html)

    Merci d’avance

    • Guillaume Desbieys

      Bonjour,
      Dans ce cas il faudrait modifier un petit peu la classe SimpleCurl. Dans la méthode startMulti($callback), il faudrait remplacer call_user_func($callback, $html[$i]); par all_user_func($callback, $html[$i], $i);
      Du coup ta méthode afficher($html) deviendrait afficher($html, $i) et tu auras donc accès à ton indice pour accéder à ton $tab_lignes;

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*