Maison >développement back-end >tutoriel php >Comment implémenter un robot d'exploration en PHP
Utilisez l'extension curl de PHP pour récupérer les données de la page. L'extension curl de PHP est une bibliothèque prise en charge par PHP qui vous permet de vous connecter et de communiquer avec différents serveurs en utilisant différents types de protocoles.
Ce programme capture les données utilisateur de Zhihu Pour pouvoir accéder à la page personnelle de l'utilisateur, celui-ci doit être connecté avant d'y accéder. Lorsque nous cliquons sur un lien d'avatar d'utilisateur sur la page du navigateur pour accéder à la page du centre personnel de l'utilisateur, la raison pour laquelle nous pouvons voir les informations de l'utilisateur est que lorsque nous cliquons sur le lien, le navigateur vous aide à rassembler les cookies locaux et à les soumettre ensemble. vers une nouvelle page, afin que vous puissiez accéder à la page du centre personnel de l'utilisateur. Par conséquent, avant d'accéder à la page personnelle, vous devez obtenir les informations sur les cookies de l'utilisateur, puis apporter les informations sur les cookies à chaque demande curl. En termes d'obtention d'informations sur les cookies, j'ai utilisé mes propres cookies. Vous pouvez voir vos propres informations sur les cookies sur la page :
Copier une par une sous la forme de "__utma=?;__utmb=?;" chaîne de cookies. Cette chaîne de cookie peut ensuite être utilisée pour envoyer des demandes.
Exemple initial :
$url = 'http://www.zhihu.com/people/mora-hu/about'; //此处mora-hu代表用户ID $ch = curl_init($url); //初始化会话 curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_COOKIE, $this->config_arr['user_cookie']); //设置请求COOKIE curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); //将curl_exec()获取的信息以文件流的形式返回,而不是直接输出。 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); $result = curl_exec($ch); return $result; //抓取的结果
Exécutez le code ci-dessus pour obtenir la page du centre personnel de l'utilisateur mora-hu. En utilisant ce résultat, puis en utilisant des expressions régulières pour traiter la page, vous pouvez obtenir le nom, le sexe et d'autres informations que vous devez capturer.
Protection des liens hypertextes d'image
Lors de la sortie des informations personnelles après régularisation des résultats renvoyés, il a été constaté que l'avatar de l'utilisateur ne pouvait pas être ouvert lors de sa sortie sur la page. Après avoir examiné les informations, j'ai découvert que c'était parce que Zhihu avait protégé les images contre les hotlinks. La solution est de forger un référent dans l'en-tête de la requête lors de la demande d'une image.
Après avoir utilisé l'expression régulière pour obtenir le lien vers l'image, envoyez une autre demande Cette fois, apportez la source de la demande d'image, en indiquant que la demande est transmise depuis le site Zhihu. Des exemples spécifiques sont les suivants :
function getImg($url, $u_id){ if (file_exists('./images/' . $u_id . ".jpg")) { return "images/$u_id" . '.jpg'; } if (empty($url)) { return ''; } $context_options = array( 'http' => array( 'header' => "Referer:http://www.zhihu.com"//带上referer参数 ) ); $context = stream_context_create($context_options); $img = file_get_contents('http:' . $url, FALSE, $context); file_put_contents('./images/' . $u_id . ".jpg", $img); return "images/$u_id" . '.jpg';}
Explorer plus d'utilisateurs
Les URL des différents utilisateurs sont presque les mêmes, la différence réside dans le nom d'utilisateur. Utilisez une correspondance régulière pour obtenir la liste des noms d'utilisateur, épelez les URL une par une, puis envoyez les requêtes une par une (bien sûr, une par une est plus lente, il existe une solution ci-dessous, qui sera discutée plus tard). Après être entré dans la page du nouvel utilisateur, répétez les étapes ci-dessus et continuez dans cette boucle jusqu'à ce que vous atteigniez la quantité de données souhaitée.
Nombre de fichiers de statistiques Linux
Une fois le script exécuté pendant un certain temps, vous devez voir combien d'images ont été obtenues. Lorsque la quantité de données est relativement importante, c'est un peu. lent à ouvrir le dossier pour vérifier le nombre de photos. Le script est exécuté dans l'environnement Linux, vous pouvez donc utiliser la commande Linux pour compter le nombre de fichiers :
Parmi eux, ls -l est une longue liste de sortie d'informations sur les fichiers dans le répertoire (les fichiers ici peut être des répertoires, des liens, des fichiers de périphérique, etc.); grep "^-" filtre les informations de sortie de longue liste, "^-" ne conserve que les fichiers généraux, si seul le répertoire est conservé, "^d" est le nombre de lignes d’informations de sortie statistique. Voici un exemple concret :
Gestion des données en double lors de l'insertion dans MySQL
Après avoir exécuté le programme pendant un certain temps, il a été constaté que les données de nombreux utilisateurs ont été dupliquées, elles doivent donc être traitées lors de l'insertion de données utilisateur en double. La solution est la suivante :
1) Vérifiez si les données existent déjà dans la base de données avant de les insérer dans la base de données
2) Ajoutez un index unique et utilisez INSERT INTO.. .ON DUPliCATE KEY UPDATE lors de l'insertion ..
3) Ajoutez un index unique, utilisez INSERT INGNO lors de l'insertion
<br/>RE INTO...
4) Ajoutez un index unique, lors de l'insertion Utilisez REPLACE INTO...
Utilisez curl_multi pour implémenter le multiplexage d'E/S pour capturer des pages
Au début, un seul processus et une seule boucle a été utilisée pour capturer les données. La vitesse était très lente et le processus d'exploration a été suspendu. Après une nuit, je n'ai pu capturer que 2 W de données. J'ai donc réfléchi à la possibilité de demander plusieurs utilisateurs à la fois lors de la saisie d'un nouvel utilisateur. page et en faisant une requête curl Plus tard, j'ai découvert la bonne chose curl_multi. Des fonctions telles que curl_multi peuvent demander plusieurs URL en même temps au lieu de les demander une par une. Il s'agit d'un mécanisme de multiplexage d'E/S. Voici un exemple d'utilisation du robot curl_multi :
$mh = curl_multi_init(); //返回一个新cURL批处理句柄 for ($i = 0; $i < $max_size; $i++) { $ch = curl_init(); //初始化单个cURL会话 curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_URL, 'http://www.zhihu.com/people/' . $user_list[$i] . '/about'); curl_setopt($ch, CURLOPT_COOKIE, self::$user_cookie); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); $requestMap[$i] = $ch; curl_multi_add_handle($mh, $ch); //向curl批处理会话中添加单独的curl句柄 } $user_arr = array(); do { //运行当前 cURL 句柄的子连接 while (($cme = curl_multi_exec($mh, $active)) == CURLM_CALL_MULTI_PERFORM); if ($cme != CURLM_OK) {break;} //获取当前解析的cURL的相关传输信息 while ($done = curl_multi_info_read($mh)) { $info = curl_getinfo($done['handle']); $tmp_result = curl_multi_getcontent($done['handle']); $error = curl_error($done['handle']); $user_arr[] = array_values(getUserInfo($tmp_result)); //保证同时有$max_size个请求在处理 if ($i < sizeof($user_list) && isset($user_list[$i]) && $i < count($user_list)) { $ch = curl_init(); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_URL, 'http://www.zhihu.com/people/' . $user_list[$i] . '/about'); curl_setopt($ch, CURLOPT_COOKIE, self::$user_cookie); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); $requestMap[$i] = $ch; curl_multi_add_handle($mh, $ch); $i++; } curl_multi_remove_handle($mh, $done['handle']); } if ($active) curl_multi_select($mh, 10); } while ($active); curl_multi_close($mh); return $user_arr;
HTTP 429 Trop de requêtes
L'utilisation de la fonction curl_multi peut envoyer plusieurs requêtes en même temps, mais lors de l'exécution lors de l'envoi de 200 requêtes en même temps, il a été constaté que de nombreuses requêtes ne pouvaient pas être renvoyées, c'est-à-dire qu'une perte de paquets a été découverte. Après une analyse plus approfondie, utilisez la fonction curl_getinfo pour imprimer les informations de chaque requête. Cette fonction renvoie un tableau associatif contenant les informations de réponse HTTP. L'un des champs est http_code, qui représente le code d'état HTTP renvoyé par la requête. J'ai vu que le code http de nombreuses requêtes était 429. Ce code retour signifie que trop de requêtes ont été envoyées. J'ai deviné que Zhihu avait mis en œuvre une protection anti-crawler, je l'ai donc testé sur d'autres sites Web et j'ai constaté qu'il n'y avait aucun problème lors de l'envoi de 200 requêtes à la fois, ce qui a prouvé que Zhihu avait mis en œuvre une protection à cet égard. des demandes ponctuelles est limitée. J'ai donc continué à réduire le nombre de requêtes et j'ai constaté qu'il n'y avait aucune perte de paquets à 5. Cela montre que dans ce programme, vous ne pouvez envoyer que jusqu'à 5 requêtes à la fois. Même si ce n'est pas beaucoup, c'est une petite amélioration.
使用Redis保存已经访问过的用户
抓取用户的过程中,发现有些用户是已经访问过的,而且他的关注者和关注了的用户都已经获取过了,虽然在数据库的层面做了重复数据的处理,但是程序还是会使用curl发请求,这样重复的发送请求就有很多重复的网络开销。还有一个就是待抓取的用户需要暂时保存在一个地方以便下一次执行,刚开始是放到数组里面,后来发现要在程序里添加多进程,在多进程编程里,子进程会共享程序代码、函数库,但是进程使用的变量与其他进程所使用的截然不同。不同进程之间的变量是分离的,不能被其他进程读取,所以是不能使用数组的。因此就想到了使用Redis缓存来保存已经处理好的用户以及待抓取的用户。这样每次执行完的时候都把用户push到一个already_request_queue队列中,把待抓取的用户(即每个用户的关注者和关注了的用户列表)push到request_queue里面,然后每次执行前都从request_queue里pop一个用户,然后判断是否在already_request_queue里面,如果在,则进行下一个,否则就继续执行。
在PHP中使用redis示例:
<?php $redis = new Redis(); $redis->connect('127.0.0.1', '6379'); $redis->set('tmp', 'value'); if ($redis->exists('tmp')) { echo $redis->get('tmp') . "\n"; }
使用PHP的pcntl扩展实现多进程
改用了curl_multi函数实现多线程抓取用户信息之后,程序运行了一个晚上,最终得到的数据有10W。还不能达到自己的理想目标,于是便继续优化,后来发现php里面有一个pcntl扩展可以实现多进程编程。下面是多编程编程的示例:
//PHP多进程demo //fork10个进程 for ($i = 0; $i < 10; $i++) { $pid = pcntl_fork(); if ($pid == -1) { echo "Could not fork!\n"; exit(1); } if (!$pid) { echo "child process $i running\n"; //子进程执行完毕之后就退出,以免继续fork出新的子进程 exit($i); } } //等待子进程执行完毕,避免出现僵尸进程 while (pcntl_waitpid(0, $status) != -1) { $status = pcntl_wexitstatus($status); echo "Child $status completed\n"; }
在linux下查看系统的cpu信息
实现了多进程编程之后,就想着多开几条进程不断地抓取用户的数据,后来开了8调进程跑了一个晚上后发现只能拿到20W的数据,没有多大的提升。于是查阅资料发现,根据系统优化的CPU性能调优,程序的最大进程数不能随便给的,要根据CPU的核数和来给,最大进程数最好是cpu核数的2倍。因此需要查看cpu的信息来看看cpu的核数。在linux下查看cpu的信息的命令:
其中,model name表示cpu类型信息,cpu cores表示cpu核数。这里的核数是1,因为是在虚拟机下运行,分配到的cpu核数比较少,因此只能开2条进程。最终的结果是,用了一个周末就抓取了110万的用户数据。
多进程编程中Redis和MySQL连接问题
在多进程条件下,程序运行了一段时间后,发现数据不能插入到数据库,会报mysql too many connections的错误,redis也是如此。
下面这段代码会执行失败:
<?php for ($i = 0; $i < 10; $i++) { $pid = pcntl_fork(); if ($pid == -1) { echo "Could not fork!\n"; exit(1); } if (!$pid) { $redis = PRedis::getInstance(); // do something exit; } }
根本原因是在各个子进程创建时,就已经继承了父进程一份完全一样的拷贝。对象可以拷贝,但是已创建的连接不能被拷贝成多个,由此产生的结果,就是各个进程都使用同一个redis连接,各干各的事,最终产生莫名其妙的冲突。
解决方法:
程序不能完全保证在fork进程之前,父进程不会创建redis连接实例。因此,要解决这个问题只能靠子进程本身了。试想一下,如果在子进程中获取的实例只与当前进程相关,那么这个问题就不存在了。于是解决方案就是稍微改造一下redis类实例化的静态方式,与当前进程ID绑定起来。
改造后的代码如下:
<?php public static function getInstance() { static $instances = array(); $key = getmypid();//获取当前进程ID if ($empty($instances[$key])) { $inctances[$key] = new self(); } return $instances[$key]; }
PHP统计脚本执行时间
因为想知道每个进程花费的时间是多少,因此写个函数统计脚本执行时间:
function microtime_float() { list($u_sec, $sec) = explode(' ', microtime()); return (floatval($u_sec) + floatval($sec)); }$start_time = microtime_float(); //do somethingusleep(100);$end_time = microtime_float();$total_time = $end_time - $start_time;$time_cost = sprintf("%.10f", $total_time);echo "program cost total " . $time_cost . "s\n";
若文中有不正确的地方,望各位指出以便改正。
相关推荐:
nodejs爬虫superagent和cheerio体验案例
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!