这次抓取了110万的用户数据,数据分析结果如下:

这次抓取了110万的用户数据,数据剖析成果如下:

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第1张

从成果能够看到,知乎的男女散布为61.7和38.3%,关于一个知识型、问答型的社区来说,现已很优异了,女生再多一点的话,知乎差不多都能够做婚恋社区了,开个打趣。

对了,在《爬了3000万QQ用户数据,挖出了花千骨赵丽颖的QQ号》一文中,咱们能够看到,除了没有填写名字的用野外,QQ空间的男女份额为56%和23%。这个数据能够作为一个参阅。且不管女人多少,但看男性用户,知乎只比QQ空间少了5%.

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第2张

从工作散布来看,知乎用户中,从事互联网职业的用户是最多的。传统职业从业者相对较少,这和知乎的定位也有很大的联系。

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第3张

北上广深仍然是用户人群最多的城市。值得注意的是,杭州用户比广州用户还多,这阐明杭州的互联网职业开展迅猛,有阿里巴巴的原因?

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第4张

看懂啦?仍是技能宅比较多,尤其是程序员。结合男女份额来看,知乎做到这样的数据十分不易,从事互联网职业的人较多,而这群人里边,还有38.3%是妹子哦。对了,女人散布为什么是38.3的份额?三八……

下面是技能正文:

开发前的预备

装置Linux体系(Ubuntu14.04),在VMWare虚拟机下装置一个Ubuntu;

装置PHP5.6或以上版别;

装置curl、pcntl扩展。

运用PHP的curl扩展抓取页面数据

PHP的curl扩展是PHP支撑的答应你与各种服务器运用各种类型的协议进行衔接和通讯的库。

本程序是抓取知乎的用户数据,要能拜访用户个人页面,需求用户登录后的才干拜访。当咱们在浏览器的页面中点击一个用户头像链接进入用户个人中心页面的时分,之所以能够看到用户的信息,是由于在点击链接的时分,浏览器帮你将本地的cookie带上一齐提交到新的页面,所以你就能进入到用户的个人中心页面。因而完成拜访个人页面之前需求先取得用户的cookie信息,然后在每次curl恳求的时分带上cookie信息。在获取cookie信息方面,我是用了自己的cookie,在页面中能够看到自己的cookie信息:

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第5张

一个个地仿制,以”__utma=?;__utmb=?;”这样的方法组成一个cookie字符串。接下来就能够运用该cookie字符串来发送恳求。

初始的示例:

  1. $url='http://www.zhihu.com/people/mora-hu/about';//此处mora-hu代表用户ID
  2. $ch=curl_init($url);//初始化会话
  3. curl_setopt($ch,CURLOPT_HEADER,0);
  4. curl_setopt($ch,CURLOPT_COOKIE,$this->config_arr['user_cookie']);//设置恳求COOKIE
  5. curl_setopt($ch,CURLOPT_USERAGENT,$_SERVER['HTTP_USER_AGENT']);
  6. curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);//将curl_exec()获取的信息以文件流的方法回来,而不是直接输出。
  7. curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1);
  8. $result=curl_exec($ch);
  9. return$result;//抓取的成果

运转上面的代码能够取得mora-hu用户的个人中心页面。运用该成果再运用正则表达式对页面进行处理,就能获取到名字,性别等所需求抓取的信息。

图片防盗链

在对回来成果进行正则处理后输出个人信息的时分,发现在页面中输出用户头像时无法翻开。通过查阅材料得知,是由于知乎对图片做了防盗链处理。处理计划便是恳求图片的时分在恳求头里假造一个referer。

在运用正则表达式获取到图片的链接之后,再发一次恳求,这时分带上图片恳求的来历,阐明该恳求来自知乎网站的转发。详细比如如下:

  1. functiongetImg($url,$u_id)
  2. {
  3. if(file_exists('./images/'.$u_id.".jpg"))
  4. {
  5. return"images/$u_id".'.jpg';
  6. }
  7. if(empty($url))
  8. {
  9. return'';
  10. }
  11. $context_options=array(
  12. 'http'=>
  13. array(
  14. 'header'=>"Referer:http://www.zhihu.com"//带上referer参数
  15.   )
  16.   );
  17. $context=stream_context_create($context_options);
  18. $img=file_get_contents('http:'.$url,FALSE,$context);
  19. file_put_contents('./images/'.$u_id.".jpg",$img);
  20. return"images/$u_id".'.jpg';
  21. }

爬取更多用户

抓取了自己的个人信息后,就需求再拜访用户的重视者和重视了的用户列表获取更多的用户信息。然后一层一层地拜访。能够看到,在个人中心页面里,有两个链接如下:

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第6张

这儿有两个链接,一个是重视了,另一个是重视者,以“重视了”的链接为例。用正则匹配去匹配到相应的链接,得到url之后用curl带上cookie再发一次恳求。抓取到用户重视了的用于列表页之后,能够得到下面的页面:

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第7张

右键点击可扩大

剖析页面的html结构,由于只需得到用户的信息,所以只需求框住的这一块的div内容,用户名都在这儿边。能够看到,用户重视了的页面的url是:

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第8张

不同的用户的这个url几乎是相同的,不同的当地就在于用户名那里。用正则匹配拿到用户名列表,一个一个地拼url,然后再逐一发恳求(当然,一个一个是比较慢的,下面有处理计划,这个稍后会提到)。进入到新用户的页面之后,再重复上面的进程,就这样不断循环,直到到达你所要的数据量。

Linux计算文件数量

脚本跑了一段时刻后,需求看看终究获取了多少图片,当数据量比较大的时分,翻开文件夹检查图片数量就有点慢。脚本是在Linux环境下运转的,因而能够运用Linux的指令来计算文件数量:

ls -l | grep "^-" | wc -l

其间,ls -l是长列表输出该目录下的文件信息(这儿的文件能够是目录、链接、设备文件等);grep “^-“过滤长列表输出信息,“^-”只保存一般文件,假如只保存目录是“^d”;wc -l是计算输出信息的行数。下面是一个运转示例:

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第9张

刺进MySQL时重复数据的处理

程序运转了一段时刻后,发现有许多用户的数据是重复的,因而需求在刺进重复用户数据的时分做处理。处理计划如下:

1)刺进数据库之前检查数据是否现已存在数据库;

2)增加仅有索引,刺进时运用INSERT INTO … ON DUPLICATE KEY UPDATE…

3)增加仅有索引,刺进时运用INSERT INGNORE INTO…

4)增加仅有索引,刺进时运用REPLACE INTO…

运用curl_multi完成I/O复用抓取页面

刚开始单进程并且单个curl去抓取数据,速度很慢,挂机爬了一个晚上只能抓到2W的数据,所以便想到能不能在进入新的用户页面发curl恳求的时分一次性恳求多个用户,后来发现了curl_multi这个好东西。curl_multi这类函数能够完成一起恳求多个url,而不是一个个恳求,这是一种I/O复用的机制。下面是运用curl_multi爬虫的示例:

 

  1. $mh=curl_multi_init();//回来一个新cURL批处理句柄
  2. for($i=0;$i<$max_size;$i++)
  3. {
  4. $ch=curl_init();//初始化单个cURL会话
  5. curl_setopt($ch,CURLOPT_HEADER,0);
  6. curl_setopt($ch,CURLOPT_URL,'http://www.zhihu.com/people/'.$user_list[$i].'/about');
  7. curl_setopt($ch,CURLOPT_COOKIE,self::$user_cookie);
  8. curl_setopt($ch,CURLOPT_USERAGENT,'Mozilla/5.0(WindowsNT6.1;WOW64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/44.0.2403.130Safari/537.36');
  9. curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
  10. curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1);
  11. $requestMap[$i]=$ch;
  12. curl_multi_add_handle($mh,$ch);//向curl批处理会话中增加独自的curl句柄
  13. }
  14. $user_arr=array();
  15. do{
  16. //运转当时cURL句柄的子衔接
  17. while(($cme=curl_multi_exec($mh,$active))==CURLM_CALL_MULTI_PERFORM);
  18. if($cme!=CURLM_OK){break;}
  19. //获取当时解析的cURL的相关传输信息
  20. while($done=curl_multi_info_read($mh))
  21. {
  22. $info=curl_getinfo($done['handle']);
  23. $tmp_result=curl_multi_getcontent($done['handle']);
  24. $error=curl_error($done['handle']);
  25. $user_arr[]=array_values(getUserInfo($tmp_result));
  26. //确保一起有$max_size个恳求在处理
  27. if($i<sizeof($user_list)&&isset($user_list[$i])&&$i<count($user_list))
  28. {
  29. $ch=curl_init();
  30. curl_setopt($ch,CURLOPT_HEADER,0);
  31. curl_setopt($ch,CURLOPT_URL,'http://www.zhihu.com/people/'.$user_list[$i].'/about');
  32. curl_setopt($ch,CURLOPT_COOKIE,self::$user_cookie);
  33. curl_setopt($ch,CURLOPT_USERAGENT,'Mozilla/5.0(WindowsNT6.1;WOW64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/44.0.2403.130Safari/537.36');
  34. curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
  35. curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1);
  36. $requestMap[$i]=$ch;
  37. curl_multi_add_handle($mh,$ch);
  38. $i++;
  39. }
  40. curl_multi_remove_handle($mh,$done['handle']);
  41. }
  42. if($active)
  43. curl_multi_select($mh,10);
  44. }while($active);
  45. curl_multi_close($mh);
  46. return$user_arr;

HTTP 429Too Many Requests

运用curl_multi函数能够一起发多个恳求,可是在履行进程中使一起发200个恳求的时分,发现许多恳求无法回来了,即发现了丢包的状况。进一步剖析,运用curl_getinfo函数打印每个恳求句柄信息,该函数回来一个包括HTTP response信息的相关数组,其间有一个字段是http_code,表明恳求回来的HTTP状况码。看到有许多个恳求的http_code都是429,这个回来码的意思是发送太多恳求了。我猜是知乎做了防爬虫的防护,所以我就拿其他的网站来做测验,发现一次性发200个恳求时没问题的,证明了我的猜想,知乎在这方面做了防护,即一次性的恳求数量是有约束的。所以我不断地削减恳求数量,发现在5的时分就没有丢包状况了。阐明在这个程序里一次性最多只能发5个恳求,尽管不多,但这也是一次小提高了。

运用Redis保存现已拜访过的用户

抓取用户的进程中,发现有些用户是现已拜访过的,并且他的重视者和重视了的用户都现已获取过了,尽管在数据库的层面做了重复数据的处理,可是程序仍是会运用curl发恳求,这样重复的发送恳求就有许多重复的网络开支。还有一个便是待抓取的用户需求暂时保存在一个当地以便下一次履行,刚开始是放到数组里边,后来发现要在程序里增加多进程,在多进程编程里,子进程会同享程序代码、函数库,可是进程运用的变量与其他进程所运用的天壤之别。不同进程之间的变量是别离的,不能被其他进程读取,所所以不能运用数组的。因而就想到了运用Redis缓存来保存现已处理好的用户以及待抓取的用户。这样每次履行完的时分都把用户push到一个already_request_queue行列中,把待抓取的用户(即每个用户的重视者和重视了的用户列表)push到request_queue里边,然后每次履行前都从request_queue里pop一个用户,然后判别是否在already_request_queue里边,假如在,则进行下一个,不然就持续履行。

在PHP中运用redis示例:


  1. <?php
  2. $redis=newRedis();
  3. $redis->connect('127.0.0.1','6379');
  4. $redis->set('tmp','value');
  5. if($redis->exists('tmp'))
  6. {
  7. echo$redis->get('tmp')."\n";
  8. }

运用PHP的pcntl扩展完成多进程

改用了curl_multi函数完成多线程抓取用户信息之后,程序运转了一个晚上,终究得到的数据有10W。还不能到达自己的抱负方针,所以便持续优化,后来发现php里边有一个pcntl扩展能够完成多进程编程。下面是多编程编程的示例:

 
  1. //PHP多进程demo
  2. //fork10个进程
  3. for($i=0;$i<10;$i++){
  4. $pid=pcntl_fork();
  5. if($pid==-1){
  6. echo"Couldnotfork!\n";
  7. exit(1);
  8. }
  9. if(!$pid){
  10. echo"childprocess$irunning\n";
  11. //子进程履行结束之后就退出,防止持续fork出新的子进程
  12. exit($i);
  13. }
  14. }
  15. //等候子进程履行结束,防止呈现僵尸进程
  16. while(pcntl_waitpid(0,$status)!=-1){
  17. $status=pcntl_wexitstatus($status);
  18. echo"Child$statuscompleted\n";
  19. }

在Linux下检查体系的cpu信息

完成了多进程编程之后,就想着多开几条进程不断地抓取用户的数据,后来开了8调进程跑了一个晚上后发现只能拿到20W的数据,没有多大的提高。所以查阅材料发现,依据体系优化的CPU功能调优,程序的最大进程数不能随便给的,要依据CPU的核数和来给,最大进程数最好是cpu核数的2倍。因而需求检查cpu的信息来看看cpu的核数。在Linux下检查cpu的信息的指令:

cat /proc/cpuinfo

用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)  php 数据分析 爬虫 第10张

右键点击可扩大

其间,model name表明cpu类型信息,cpu cores表明cpu核数。这儿的核数是1,由所以在虚拟机下运转,分配到的cpu核数比较少,因而只能开2条进程。终究的成果是,用了一个周末就抓取了110万的用户数据。

多进程编程中Redis和MySQL衔接问题

在多进程条件下,程序运转了一段时刻后,发现数据不能刺进到数据库,会报mysql too many connections的过错,redis也是如此。

下面这段代码会履行失利:


  1. functiongetImg($url,$u_id)
  2. {
  3. if(file_exists('./images/'.$u_id.".jpg"))
  4. {
  5. return"images/$u_id".'.jpg';
  6. }
  7. if(empty($url))
  8. {
  9. return'';
  10. }
  11. $context_options=array(
  12. 'http'=>
  13. array(
  14. 'header'=>"Referer:http://www.zhihu.com"//带上referer参数
  15.   )
  16.   );
  17. $context=stream_context_create($context_options);
  18. $img=file_get_contents('http:'.$url,FALSE,$context);
  19. file_put_contents('./images/'.$u_id.".jpg",$img);
  20. return"images/$u_id".'.jpg';
  21. }
1

底子原因是在各个子进程创立时,就现已承继了父进程一份彻底相同的复制。目标能够复制,可是已创立的衔接不能被复制成多个,由此发生的成果,便是各个进程都运用同一个redis衔接,各干各的事,终究发生不可思议的抵触。

处理方法: 程序不能彻底确保在fork进程之前,父进程不会创立redis衔接实例。因而,要处理这个问题只能靠子进程自身了。试想一下,假如在子进程中获取的实例只与当时进程相关,那么这个问题就不存在了。所以处理计划便是略微改造一下redis类实例化的静态方法,与当时进程ID绑定起来。 改造后的代码如下:
  1. functiongetImg($url,$u_id)
  2. {
  3. if(file_exists('./images/'.$u_id.".jpg"))
  4. {
  5. return"images/$u_id".'.jpg';
  6. }
  7. if(empty($url))
  8. {
  9. return'';
  10. }
  11. $context_options=array(
  12. 'http'=>
  13. array(
  14. 'header'=>"Referer:http://www.zhihu.com"//带上referer参数
  15.   )
  16.   );
  17. $context=stream_context_create($context_options);
  18. $img=file_get_contents('http:'.$url,FALSE,$context);
  19. file_put_contents('./images/'.$u_id.".jpg",$img);
  20. return"images/$u_id".'.jpg';
  21. }
2

PHP计算脚本履行时刻

由于想知道每个进程花费的时刻是多少,因而写个函数计算脚本履行时刻:

  1. functiongetImg($url,$u_id)
  2. {
  3. if(file_exists('./images/'.$u_id.".jpg"))
  4. {
  5. return"images/$u_id".'.jpg';
  6. }
  7. if(empty($url))
  8. {
  9. return'';
  10. }
  11. $context_options=array(
  12. 'http'=>
  13. array(
  14. 'header'=>"Referer:http://www.zhihu.com"//带上referer参数
  15.   )
  16.   );
  17. $context=stream_context_create($context_options);
  18. $img=file_get_contents('http:'.$url,FALSE,$context);
  19. file_put_contents('./images/'.$u_id.".jpg",$img);
  20. return"images/$u_id".'.jpg';
  21. }
3

若文中有不正确的当地,望各位指出以便改正。

代码保管地址:https://github.com/hhqcontinue/zhihuSpider

转载请说明出处
知优网 » 用php做爬虫 百万等级知乎用户数据爬取与剖析(php能写爬虫吗)

发表评论

您需要后才能发表评论