>  기사  >  백엔드 개발  >  PHP에서 크롤러를 구현하는 방법

PHP에서 크롤러를 구현하는 방법

小云云
小云云원래의
2018-03-10 11:16:0320868검색

PHP의 컬 확장을 사용하여 페이지 데이터를 가져옵니다. PHP의 컬 확장은 다양한 유형의 프로토콜을 사용하여 다양한 서버와 연결하고 통신할 수 있도록 하는 PHP에서 지원하는 라이브러리입니다.

이 프로그램은 Zhihu 사용자 데이터를 캡처합니다. 사용자의 개인 페이지에 액세스하려면 사용자가 액세스하기 전에 로그인해야 합니다. 당사가 이용자의 개인센터 페이지에 진입하기 위해 브라우저 페이지의 이용자 아바타 링크를 클릭할 때 이용자의 정보를 볼 수 있는 이유는, 해당 링크를 클릭할 때 브라우저가 로컬 쿠키를 가져와 함께 제출할 수 있도록 도와주기 때문입니다. 새로운 페이지로 이동하여 사용자의 개인센터 페이지로 진입할 수 있습니다. 따라서 개인 페이지에 접속하기 전에 사용자의 쿠키 정보를 얻은 다음 각 컬 요청마다 쿠키 정보를 가져와야 합니다. 쿠키 정보를 얻는 방법은 나만의 쿠키를 사용하였습니다.

하나씩 복사하여 "__utma=?;__utmb=?;" 형식으로 쿠키 문자를 만듭니다. . 그런 다음 이 쿠키 문자열을 사용하여 요청을 보낼 수 있습니다.

초기예제 :

    $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;  //抓取的结果

위의 코드를 실행하여 mora-hu 사용자의 개인센터 페이지를 가져옵니다. 이 결과를 사용한 다음 정규 표현식을 사용하여 페이지를 처리하면 캡처해야 하는 이름, 성별 및 기타 정보를 얻을 수 있습니다.

사진 핫링크 보호

반환된 결과를 정규화한 후 개인정보를 출력할 때 해당 페이지에 사용자의 아바타 출력시 열리지 않는 현상을 발견했습니다. 정보를 검토한 결과 Zhihu가 사진을 핫링크로부터 보호했기 때문이라는 것을 알게 되었습니다. 해결책은 이미지를 요청할 때 요청 헤더에 리퍼러를 위조하는 것입니다.

정규 표현식을 사용하여 이미지 링크를 얻은 후 이번에는 이미지 요청 소스를 가져와 해당 요청이 Zhihu 웹사이트에서 전달되었음을 나타냅니다. 구체적인 예는 다음과 같습니다.

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';}

더 많은 사용자 크롤링

다른 사용자의 URL은 거의 동일하며 차이점은 사용자 이름에 있습니다. 일반 일치를 사용하여 사용자 이름 목록을 얻고, URL을 하나씩 입력한 다음 요청을 하나씩 보냅니다(물론 하나씩은 더 느리므로 아래에 해결 방법이 있으며 이에 대해서는 나중에 설명합니다). 새 사용자 페이지에 들어간 후 위 단계를 반복하고 원하는 데이터 양에 도달할 때까지 이 루프를 계속합니다.

Linux 통계 파일 개수

스크립트가 한동안 실행된 후 얼마나 많은 사진을 얻었는지 확인해야 합니다. 데이터 양이 상대적으로 많을 때 폴더를 열어 확인하는 것이 약간 느립니다. 사진 수. 스크립트는 Linux 환경에서 실행되므로 Linux 명령을 사용하여 파일 수를 계산할 수 있습니다.

그중 ls -l은 디렉터리에 있는 파일 정보의 긴 목록 출력입니다(여기서 파일은 디렉터리일 수 있음, 링크, 장치 파일 등); grep "^-"는 긴 목록 출력 정보를 필터링하고, "^-"는 일반 파일만 유지하며, wc -l은 통계 출력 줄 수만 유지하는 경우입니다. 정보. 다음은 실행 예시입니다.

PHP爬虫 数据抓取 数据分析 爬虫抓取数据

MySQL에 삽입 시 중복 데이터 처리

프로그램을 일정 시간 실행한 결과 많은 사용자 데이터가 중복된 것으로 확인되어 처리가 필요합니다. 중복된 사용자 데이터를 삽입할 때. 해결 방법은 다음과 같습니다.

1) 데이터를 데이터베이스에 삽입하기 전에 데이터가 이미 데이터베이스에 있는지 확인하세요.

2) 고유 인덱스를 추가하고 INSERT INTO...ON DUPliCATE KEY UPDATE...를 사용하세요.

3) 삽입 시 고유 인덱스 추가, 삽입 시 INSERT INGNO 사용

<br/>

RE INTO...

4) 고유 인덱스 추가, 삽입 시 REPLACE INTO 사용...

curl_multi를 사용하여 I 구현 /O 멀티플렉싱으로 페이지 캡처

단일 프로세스만 시작했습니다. 게다가 단일 컬은 데이터를 캡처하는 데 매우 느립니다. 전화를 끊고 밤새 크롤링한 후에는 2W의 데이터만 캡처할 수 있을지 고민했습니다. 새로운 사용자 페이지에 들어가서 컬 요청을 할 때 한 번에 여러 사용자를 요청합니다. 나중에 컬이 좋은 점을 알았습니다. cur_multi와 같은 함수는 하나씩 요청하는 대신 동시에 여러 URL을 요청할 수 있습니다. 이는 I/O 다중화 메커니즘입니다. 다음은 컬_멀티 크롤러 사용 예시입니다.

        $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, &#39;http://www.zhihu.com/people/&#39; . $user_list[$i] . &#39;/about&#39;);
            curl_setopt($ch, CURLOPT_COOKIE, self::$user_cookie);
            curl_setopt($ch, CURLOPT_USERAGENT, &#39;Mozilla/5.0 (Windows NT 6.1; WOW64)
            AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36&#39;);
            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[&#39;handle&#39;]);                $tmp_result = curl_multi_getcontent($done[&#39;handle&#39;]);                $error = curl_error($done[&#39;handle&#39;]);                $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, &#39;http://www.zhihu.com/people/&#39; . $user_list[$i] . &#39;/about&#39;); 
                   curl_setopt($ch, CURLOPT_COOKIE, self::$user_cookie);
                    curl_setopt($ch, CURLOPT_USERAGENT, &#39;Mozilla/5.0 (Windows NT 6.1; WOW64) 
                   AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36&#39;);
                    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[&#39;handle&#39;]);
            }            if ($active) 
               curl_multi_select($mh, 10);
        } while ($active);
        curl_multi_close($mh);        return $user_arr;

HTTP 429 Too Many Requests

curl_multi 함수를 사용하면 여러 요청을 동시에 보낼 수 있지만, 실행 과정에서 200개의 요청을 보냈을 때 동시에 많은 요청을 반환할 수 없는 것으로 나타났습니다. 즉, 패킷 손실이 발견되었습니다. 추가 분석 후, 컬_getinfo 함수를 사용하여 각 요청 핸들 정보를 인쇄합니다. 이 함수는 HTTP 응답 정보가 포함된 연관 배열을 반환합니다. 필드 중 하나는 요청에서 반환된 HTTP 상태 코드를 나타냅니다. 많은 요청의 http_code가 429인 것을 확인했습니다. 이 반환 코드는 너무 많은 요청이 전송되었음을 의미합니다. 나는 Zhihu가 크롤러 방지 보호를 구현했다고 추측하여 다른 웹사이트에서 테스트한 결과 한 번에 200개의 요청을 보낼 때 문제가 없다는 것을 발견했습니다. 이는 Zhihu가 이와 관련하여 보호를 구현했음을 입증했습니다. 일회성 요청 수가 제한되어 있습니다. 그래서 요청 횟수를 계속 줄여보니 5시에는 패킷 손실이 없는 것으로 나타났습니다. 이 프로그램에서는 한 번에 최대 5개의 요청만 보낼 수 있음을 보여줍니다. 비록 많지는 않지만 작은 개선입니다.

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

抓取用户的过程中,发现有些用户是已经访问过的,而且他的关注者和关注了的用户都已经获取过了,虽然在数据库的层面做了重复数据的处理,但是程序还是会使用curl发请求,这样重复的发送请求就有很多重复的网络开销。还有一个就是待抓取的用户需要暂时保存在一个地方以便下一次执行,刚开始是放到数组里面,后来发现要在程序里添加多进程,在多进程编程里,子进程会共享程序代码、函数库,但是进程使用的变量与其他进程所使用的截然不同。不同进程之间的变量是分离的,不能被其他进程读取,所以是不能使用数组的。因此就想到了使用Redis缓存来保存已经处理好的用户以及待抓取的用户。这样每次执行完的时候都把用户push到一个already_request_queue队列中,把待抓取的用户(即每个用户的关注者和关注了的用户列表)push到request_queue里面,然后每次执行前都从request_queue里pop一个用户,然后判断是否在already_request_queue里面,如果在,则进行下一个,否则就继续执行。

在PHP中使用redis示例:

<?php    $redis = new Redis();    $redis->connect(&#39;127.0.0.1&#39;, &#39;6379&#39;);    $redis->set(&#39;tmp&#39;, &#39;value&#39;);    if ($redis->exists(&#39;tmp&#39;))
    {        echo $redis->get(&#39;tmp&#39;) . "\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的信息的命令:

PHP爬虫 数据抓取 数据分析 爬虫抓取数据

其中,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(&#39; &#39;, 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体验案例

NodeJS爬虫详解

Node.js爬虫之网页请求模块详解

위 내용은 PHP에서 크롤러를 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.