宝哥软件园

数百万知乎用户数据捕获和分析的PHP开发

编辑:宝哥软件园 来源:互联网 时间:2021-10-01

此次共捕获用户数据110万条,数据分析结果如下:

开发前的准备。

安装Linux系统(Ubuntu14.04),在VMWare虚拟机下安装一个Ubuntu

安装PHP5.6或以上;

安装curl和pcntl加长件。

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

PHP curl扩展是一个PHP支持的库,允许您使用各种类型的协议与各种服务器连接和通信。

这个程序是为了抓取知乎的用户数据。要访问用户的个人页面,用户必须在访问之前登录。当我们在浏览器页面点击一个用户头像链接进入用户个人中心页面时,之所以能看到用户的信息,是因为当我们点击链接时,浏览器会帮你带来本地cookie并提交到新页面,这样你就可以进入用户个人中心页面了。因此,在访问个人页面之前,需要先获取用户的cookie信息,然后在每次curl请求时携带cookie信息。在获取cookie信息方面,我使用了自己的cookie,在页面上可以看到自己的cookie信息:

用' __utma='逐个复制?__utmb=?'这种形式构成了一个cookie字符串。然后,您可以使用cookie字符串发送请求。

初始示例:

$ URL=' http://www . zhi Hu.com/people/mora-Hu/about ';//这里,mora-hu代表用户ID $ ch=curl _ init($ URL);//初始化会话curl _ setopt ($ ch,curl opt _ header,0);curl_setopt($ch,CURLOPT_COOKIE,$ this-config _ arr[' user _ COOKIE ']);//设置请求cookie curl _ setopt ($ ch,curl opt _ useragent,$ _ server[' http _ user _ agent ']);curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);//以文件流的形式返回curl_exec()得到的信息,而不是直接输出。curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1);$ result=curl _ exec($ ch);返回$ result//抓取结果运行上面的代码,获取莫拉胡用户的个人中心页面。使用这个结果并使用正则表达式处理页面,您可以获得需要抓取的信息,例如姓名和性别。

图片防盗链

在对返回的结果进行规整后输出个人信息时,发现在页面中输出时无法打开用户的头像。查阅资料,是因为知乎对图片做了防盗链处理。解决方法是在请求图片时在请求头中伪造一个推荐人。

使用正则表达式获取图片链接后,再次发送请求。这时带上图片请求的来源,说明请求来自知乎网站的转发。具体例子如下:

函数getImg($url,$u_id){ if (file_exists)'。/images/'。$u_id。jpg '){ return ' images/$ u _ id '。jpg ';} if(空($ URL)){ return“”;} $ context _ options=array(' http '=array(' header '=' refer :http://www . zhi Hu.com '//with refer参数));$ context=stream _ context _ create($ context _ options);$ img=file _ get _ contents(' http : '。$url,FALSE,$ context);file_put_contents('。/images/'。$u_id。' jpg ',$ img);返回“images/$u_id”。jpg ';}抓取个人信息后,需要访问关注过用户的关注者和用户列表,获取更多的用户信息。然后一层一层地访问它。如您所见,在个人中心页面中,有以下两个链接:

这里有两个环节,一个是关心,一个是关心。以“关注”这个环节为例。使用常规匹配来匹配相应的链接,然后使用curl with cookie在获得url后再次发送请求。抓取用户关注的列表页面后,可以得到如下页面:

分析页面的html结构,因为只要得到用户的信息,只需要框住这一块的div内容,用户名都在里面。可以看到,用户关注的页面url是:

不同用户的网址几乎是一样的,区别在于用户名。使用常规匹配获取用户名列表,逐个拼写URL,然后逐个发送请求(当然,逐个比较慢,下面有解决方案,后面会讨论)。进入新用户页面后,重复上述步骤,以此类推,不断循环,直到到达你想要的数据。

Linux统计文件号。

脚本运行一段时间后,要看采集了多少图片。当数据量比较大的时候,打开文件夹查看图片数量有点慢。脚本在Linux环境中运行,因此您可以使用Linux命令来计算文件的数量:

复制的代码如下:ls -l | grep '^-' | wc -l L。

其中,ls -l是输出该目录下文件信息的长列表(这里的文件可以是目录、链接、设备文件等。);Grep '^-'过滤长列表的输出信息,'-'只保留一般文件,如果只保留目录,则为' d ';Wc -l是统计输出信息的行数。以下是一个运行示例:

插入MySQL时重复数据的处理。

运行程序一段时间后,发现很多用户的数据是重复的,所以在插入重复的用户数据时需要做处理。处理方案如下:

1)在插入数据库之前,检查数据库中是否已经存在数据;

2)使用“插入到”添加唯一索引.插入时重复密钥更新。

3)添加唯一索引,插入时使用INSERT INGNORE INTO。

4)添加唯一索引,插入时使用REPLACE INTO。

使用curl_multi实现多线程抓取页面。

开始时,单个进程和单个卷曲抓取数据,但速度非常慢。挂掉电话爬了一晚上,只能抓到2W的数据。因此,当我进入一个新的用户页面发送curl请求时,我考虑了是否可以一次请求多个用户。后来我发现了curl_multi,一个好东西。curl_multi这样的函数可以同时请求多个url,而不是一个一个的请求,类似于linux系统中一个进程运行多个线程的函数。下面是一个使用curl_multi实现多线程爬虫的例子:

$ MH=curl _ multi _ init();//返回一个新卷曲批处理句柄对于($ I=;$ I $ max _ size $ I){ $ ch=curl _ init();//初始化单个卷曲会话curl_setopt($ch,CURLOPT_HEADER,‘http://www.zhihu.com/people/'。’$user_list[$i]./about’);curl_setopt($ch,CURLOPT_COOKIE,self : $ user _ COOKIE);curl_setopt($ch,CURLOPT_USERAGENT ',Mozilla/.(Windows NT .WOW) AppleWebKit/.(KHTML,喜欢壁虎)Chrome/.safari/');curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);curl_setopt($ch,CURLOPT_FOLLOWLOCATION,$ RequestMap[$ I]=$ ch;curl_multi_add_handle($mh,$ ch);//向卷曲批处理会话中添加单独的卷曲句柄} $ user _ arr=array();do { //运行当前卷曲句柄的子连接while($ CME=curl _ MULTI _ exec($ MH,$ active))==CURLM _ CALL _ MULTI _ PERFORM);if ($cme!=CURLM _ OK){ break;} //获取当前解析的卷曲的相关传输信息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,‘http://www.zhihu.com/people/'。’$user_list[$i]./about’);curl_setopt($ch,CURLOPT_COOKIE,self : $ user _ COOKIE);curl_setopt($ch,CURLOPT_USERAGENT ',Mozilla/.(Windows NT .WOW) AppleWebKit/.(KHTML,喜欢壁虎)Chrome/.safari/');curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);curl_setopt($ch,CURLOPT_FOLLOWLOCATION,$ RequestMap[$ I]=$ ch;curl_multi_add_handle($mh,$ ch);$ I;} curl_multi_remove_handle($mh,$ done[' handle ']);} if($ active)curl _ multi _ select($ MH,} while($ active);curl _ multi _ close($ MH);返回$ user _ arrHTTP 429请求过多

使用卷曲_多函数可以同时发多个请求,但是在执行过程中使同时发200个请求的时候,发现很多请求无法返回了,即发现了丢包的情况。进一步分析,使用curl_getinfo函数打印每个请求句柄信息,该函数返回一个包含超文本传送协议响应信息的关联数组,其中有一个字段是http_code,表示请求返回的超文本传送协议状态码。看到有很多个请求的http_code都是429,这个返回码的意思是发送太多请求了。我猜是知乎做了防爬虫的防护,于是我就拿其他的网站来做测试,发现一次性发200个请求时没问题的,证明了我的猜测,知乎在这方面做了防护,即一次性的请求数量是有限制的。于是我不断地减少请求数量,发现在5的时候就没有丢包情况了。说明在这个程序里一次性最多只能发5个请求,虽然不多,但这也是一次小提升了。

使用Redis保存已经访问过的用户

在抓取用户的过程中,发现部分用户已经访问过,其关注者和关注用户已经获取。虽然重复的数据处理是在数据库级别完成的,但是程序仍然会使用curl发送请求,这会导致大量重复的网络开销。另一个是要被抓取的用户需要临时存储在一个地方,以便下次执行。一开始是放入数组,后来发现程序中要加多个进程。在多进程编程中,子进程将共享程序代码和函数库,但进程使用的变量与其他进程使用的变量有很大不同。不同进程之间的变量是分开的,不能被其他进程读取,所以不能使用数组。因此,人们认为使用Redis缓存来存储已处理的用户和要抓取的用户。这样,用户在每次执行后被推送到一个已经_request_queue,而要抓取的用户(即每个用户的关注者和关注用户列表)被推送到request_queue。然后,在每次执行之前,从request_queue中弹出一个用户,然后判断它是否在已经存在的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多进程演示//fork10进程for($ I=0;10美元;$ I){ $ PID=pcntl _ fork();if ($pid==-1) { echo '无法分叉! n ';出口(1);} if(!$pid) { echo '子进程$i正在运行 n ';//子进程执行后会退出,以免继续分叉出一个新的子进程出口($ I);} }//等待子进程完成执行,避免僵尸进程while (pcntl_waitpid(0,$status)!=-1){ $ status=pcntl _ wexit status($ status);回应“子$状态已完成 n”;}检查Linux下系统的cpu信息。

在实现了多进程编程之后,我考虑再打开几个进程,不断抓取用户的数据。后来开始了8音流程,跑了一晚上,发现只能得到20W的数据,没有太大的提升。因此,参考数据发现,根据系统优化的cpu性能调优,程序的最大进程数不能随便给,而应该按照CPU内核的总和给,最大进程数应该是CPU内核的两倍。因此,有必要检查cpu信息以查看cpu内核的数量。在Linux下查看cpu信息的命令:

复制代码如下:cat /proc/cpuinfo。

型号名称表示cpu类型信息,cpu内核表示cpu内核数量。这里的核心数是1,因为是在一个虚拟机下运行,分配的cpu核心数比较少,所以只能启动两个进程。最终结果是,用了一个周末的时间捕获了110万用户数据。

多进程编程中Redis和MySQL的连接。

在多进程的情况下,当程序运行一段时间后,发现数据无法插入数据库,就会报告mysql连接过多的错误,redis也会如此。

以下代码将无法执行:

?PHP for($ I=0;10美元;$ I){ $ PID=pcntl _ fork();if ($pid==-1) { echo '无法分叉! n ';出口(1);} if(!$ PID){ $ redis=predict 33603360 getinstance();//做某事退出;}}根本原因是当创建每个子进程时,它继承了父进程的一个相同副本。可以复制对象,但不能将创建的连接复制到多个对象中。因此,每个进程都使用相同的redis连接并做自己的事情,从而导致无法解释的冲突。

解决方案:程序不能保证父进程不会在分叉进程之前创建redis连接实例。所以要解决这个问题,只能靠子流程本身。想象一下,如果在子进程中获得的实例只与当前进程相关,那么这个问题就不存在了。所以解决方案是稍微修改实例化redis类的静态方式,并将其绑定到当前的进程ID。修改后的代码如下:

?php公共静态函数getInstance(){ static $ instance=array();$ key=getmypid();//获取当前进程id if($ empty($ instance[$ key]){ $ events[$ key]=new self();}返回$ instance[$ key];}PHP统计脚本执行时间。

因为您想知道每个进程需要多少时间,所以编写一个函数来计算脚本执行时间:

函数microtime_float(){ list($u_sec,$sec)=explode(',micro time());return(float val($ u _ sec)float val($ sec));} $ start _ time=micro time _ float();//做点什么us LEEP(100);$ end _ time=micro time _ float();$ total _ time=$ end _ time-$ start _ time;$time_cost=sprintf('%.10f ',$ total _ time);echo '程序总成本'。$时间_成本。s n ';

更多资讯
游戏推荐
更多+