PHP 使用 CURL 同步抓取多個網頁

一般 CURL 抓網頁的方法, 是一頁一頁抓, 假設要抓 4頁, 所費時間各別是 5,10,7,5 秒, 那全部總合所花的時間就是 5 + 10 + 7 + 5 = 27 秒.

若能同時間去抓取多個網頁, 所花費的時間 5,10,7,5 秒, 全部總合所花的時間是 10 秒.(花費最多時間的秒數)

於 JavaScript 可使用 AJAX 的 async(YAHOO.util.Connect.asyncRequest) 來達成, 於 PHP 可以用 CURL 來達成此 Multi-Threading 的效果.

程式(async.php)

<?php
function async_get_url($url_array, $wait_usec = 0)
{
    if (!is_array($url_array))
        return false;

    $wait_usec = intval($wait_usec);

    $data    = array();
    $handle  = array();
    $running = 0;

    $mh = curl_multi_init(); // multi curl handler

    $i = 0;
    foreach($url_array as $url) {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return don't print
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect
        curl_setopt($ch, CURLOPT_MAXREDIRS, 7);

        curl_multi_add_handle($mh, $ch); // 把 curl resource 放進 multi curl handler 裡

        $handle[$i++] = $ch;
    }

    /* 執行 */
    /* 此種做法會造成 CPU loading 過重 (CPU 100%)
    do {
        curl_multi_exec($mh, $running);

        if ($wait_usec > 0) // 每個 connect 要間隔多久
            usleep($wait_usec); // 250000 = 0.25 sec
    } while ($running > 0);
    */

    /* 此做法就可以避免掉 CPU loading 100% 的問題 */
    // 參考自: http://www.hengss.com/xueyuan/sort0362/php/info-36963.html
    /* 此作法可能會發生無窮迴圈
    do {
        $mrc = curl_multi_exec($mh, $active);
    } while ($mrc == CURLM_CALL_MULTI_PERFORM);

    while ($active and $mrc == CURLM_OK) {
        if (curl_multi_select($mh) != -1) {
            do {
                $mrc = curl_multi_exec($mh, $active);
            } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        }
    }
    */
    /*
    // 感謝 Ren 指點的作法. (需要在測試一下)
    // curl_multi_exec的返回值是用來返回多線程處裡時的錯誤,正常來說返回值是0,也就是說只用$mrc捕捉返回值當成判斷式的迴圈只會運行一次,而真的發生錯誤時,有拿$mrc判斷的都會變死迴圈。
    // 而curl_multi_select的功能是curl發送請求後,在有回應前會一直處於等待狀態,所以不需要把它導入空迴圈,它就像是會自己做判斷&自己決定等待時間的sleep()。
    */
    do {
        curl_multi_exec($mh, $running);
        curl_multi_select($mh);
    } while ($running > 0);

    /* 讀取資料 */
    foreach($handle as $i => $ch) {
        $content  = curl_multi_getcontent($ch);
        $data[$i] = (curl_errno($ch) == 0) ? $content : false;
    }

    /* 移除 handle*/
    foreach($handle as $ch) {
        curl_multi_remove_handle($mh, $ch);
    }

    curl_multi_close($mh);

    return $data;
}
?>

使用

<?php
$urls = array('http://example1.com', 'http://example2.com');
print_r(async_get_url($urls)); // [0] => example1, [1] => example2
?>

測試

sleep.php # 看時間延長取得的效果

<?php
sleep(intval($_GET['time']));
echo intval($_GET['time']);
?>
<?php
$url_array = array(
'http://example.com/sleep.php?time=5',
'http://example.com/sleep.php?time=10',
'http://example.com/sleep.php?time=7',
'http://example.com/sleep.php?time=5',
);
print_r(async_get_url($url_array));
// 總花費時間會是 10 秒, 並印出 [0] => 5, [1] => 10, [2] => 7, [3] => 5
?>

相關網頁


關於 Tsung

對新奇的事物都很有興趣, 喜歡簡單的東西, 過簡單的生活.
本篇發表於 Programming。將永久鏈結加入書籤。

PHP 使用 CURL 同步抓取多個網頁 有 31 則回應

  1. hoball 說道:

    /* 執行 */
    do {
    curl_multi_exec($mh, $running);
    if ($wait_usec > 0) /* 每個 connect 要間隔多久 */
    usleep($wait_usec); // 250000 = 0.25 sec
    } while ($running > 0);
    這裏有可能令CPU 100% used,詳情請看:
    http://www.hengss.com/xueyuan/sort0362/php/info-36963.html
    http://www.somacon.com/p537.php
    http://www.onlineaspect.com/2009/01/26/how-to-use-curl_multi-without-blocking/

  2. Tsung 說道:

    果然會造成此問題, 測試過 http://www.hengss.com/xueyuan/sort0362/php/info-36963.html 的解法, 可以解決此問題.
    已經將此篇的程式修正, 萬分感謝~ orz.

  3. Lackneets 說道:

    避免掉 CPU loading 100% 的問題的這種方法在PHP 5.3下不曉得為何會有非預期的結果

  4. maxmas 說道:

    我用了這個async_get_url
    我並發十條線路去連不同的資料庫的資料表運算,
    在我的個人電腦XP Appserv裡面很正常, 但放到公司正式Linux主機上,
    發現返回的資料時有時無, 沒有一次10個資料都全的, 很不穩定,
    我加了下面二個參數設定, 不過沒什麼用
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
    curl_setopt($ch, CURLOPT_REFERER, $url); //設定 referer
    上網找了一下有沒有什麼因素, 看到有人說詳細因素有很多可能
    可能主機附載能力太差, 無法負荷太多連線數,
    可能伺服器網路頻寬不足或網路設備不穩封包遺失

    不過我在想LINUX主機應該比個人電腦強吧..
    而且我的電腦也是在公司的網路裡面啊...
    沒道理我的XP裝Appserv跑的都沒問題, 10筆資料沒有漏失的,
    所以尚不知道造成的原因

  5. 秀才 說道:

    博主你好,我在网络上参考了很多curl多线程的方法,无一没有例外的都会在if (curl_multi_select($mh) != -1)上陷入死循环,我的php版本分别是5.3.13和5.4.4,我怀疑是不是在5.3以上的版本中都会有问题,希望博主看到后能给出一个解决办法,thank you.

  6. RichardXu 說道:

    博主,如果我的$url_array是個大數組,例如一千條的數組,會不會電腦卡死呢?

  7. RichardXu 說道:

    博主,我在本地電腦上測試了一下,發現連接超時,不知道是怎麼一種情況?
    (Fatal error: Maximum execution time of 60 seconds exceeded in E:)

  8. 說道:

    當我把php更新到5.4.3之後, while ($active and $mrc == CURLM_OK) {
    if (curl_multi_select($mh) != -1) {
    do {
    $mrc = curl_multi_exec($mh, $active);
    } while ($mrc == CURLM_CALL_MULTI_PERFORM);
    }
    }
    這段就一直卡住了,後來在網路上找到有人稍微改寫了一下:

    while ($active && $mrc == CURLM_OK) {
    if (curl_multi_select($mh) === -1) {
    usleep(100);
    }
    do {
    $mrc = curl_multi_exec($mh, $active);
    } while ($mrc == CURLM_CALL_MULTI_PERFORM);
    }

    就能正常運行。
    參考網址: https://bugs.php.net/bug.php?id=63842

    • Tsung 說道:

      感謝提供, 先將修改過的 code 改在下面, 之後找時間測試看看, 再來修改上面的. (補上PHP版本判斷)

      while ($active && $mrc == CURLM_OK) {
      if (version_compare(PHP_VERSION, '5.4.3', '<')) { if (curl_multi_select($mh) != -1) { do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); } } else { if (curl_multi_select($mh) === -1) { usleep(100); } do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); } }

  9. Ren 說道:

      curl_multi_exec的返回值是用來返回多線程處裡時的錯誤,正常來說返回值是0,也就是說只用$mrc捕捉返回值當成判斷式的迴圈只會運行一次,而真的發生錯誤時,有拿$mrc判斷的都會變死迴圈。

      而curl_multi_select的功能是curl發送請求後,在有回應前會一直處於等待狀態,所以不需要把它導入空迴圈,它就像是會自己做判斷&自己決定等待時間的sleep()。

      用以下更簡潔的寫法就能把迴圈次數減到最少,請測試看看。
    do {

    curl_multi_exec($mh, $running);
    curl_multi_select($mh);

    } while ($running > 0);

    • Tsung 說道:

      萬分感謝, 清楚易懂~ 我先更新此文, 等會把 code 修改上線測試看看~ Orz.

    • Ren 說道:

        原先很納悶為什麼連PHP官網手冊都用奇怪的好幾個迴圈來寫curl_multi_exec運行,看到有人說新版PHP有問題所以測試了一下,curl_multi_select還真的有Bug...

        我上面回覆的那四行寫法在5.3.13、5.4.16上,運行一開始curl_multi_select一定會不正常的回傳-1並且不進行等待。而在執行抓取海外的或第一次抓取的網址時,回傳-1的迴圈次數更會多達1~10萬次,所以我推測大概是執行DNS查詢期間curl_multi_select會失效。也正是會異常回傳-1,所以像PHP官網判斷if (curl_multi_select($mh) != -1)的寫法會造成上一層迴圈Run不停。

        那為什麼會有那種寫法出現,因為太神奇了舊版PHP竟然也有問題......。在5.2.11、5.3.1,好像也有一開始DNS查詢期間不會等待的問題,但回傳值竟然是正常進行等待時的大於-1的值......。

        不過我原本測試用的PHP 5.3.9完全沒問題。

        所以我想要兼顧各種PHP版本的話,直接在那四行do迴圈裡無條件加上一個usleep()會最穩定,不然可以寫成這樣:
      (縮排有全形符號)
      $debug = (version_compare(PHP_VERSION, '5.3.9', '>=')) ?
       (version_compare(PHP_VERSION, '5.3.9', '>')) ?
       2 : 1 : 0 ;

      do {
       curl_multi_exec($this->mh, $running);
       $status = curl_multi_select($this->mh);
       if (($debug == 0 && $status != -1 && $running > 0) ||
        ($debug == 2 && $status == -1 && $running > 0)) {
        usleep(10000 * ++$i);
       }
      } while ($running > 0);

        usleep的時間可以實測時自行調整,PHP版本我只測過5.2.11、5.3.1、5.3.9、5.3.13、5.4.16,所以我用只有5.3.9沒問題的情形下去寫,其他版本可以請大家再測試看看。

    • Tsung 說道:

      我在 PHP 5.4.4 測試, 目前測試的情況是.

      用原本的 code, 抓取資料的時間, 都會落在 10.04secs 左右, 很固定.

      但是改成這段 code 後, 時間都會落在 11.038 secs 和 16.079 secs (都會這兩個值互相跳動).

      註: 測試 code 如文章寫的, 用 sleep 做測試.

      詳細原因還不明, 您遇到的問題也是這樣子嗎?

      明天再來追追看, 我先改回原本的 code, 萬分感謝~ Orz.

  10. Ren 說道:

      我之前測試的時候只有注意CPU使用率、curl_multi_select回傳值還有迴圈跑的次數,沒有注意到抓取時間是多少。

      另外補充一下,後來在Lnux平台上的PHP 5.2.17上測試,curl_multi_select沒有無法暫停的錯誤發生,先前回復的那四行code可以正常運行,所以還不確定到底是PHP版本問題還是作業系統的問題。先前五個PHP版本都是在Windows 7 x64上進行測試的。

  11. gameclamp 說道:

    do {

    curl_multi_exec($mh, $running);
    curl_multi_select($mh);

    } while ($running > 0);
    這串代碼在windows8 x64,PHP5.5.3下工作很好,迴圈數也很少。

  12. kkman 說道:

    請問一下,使用curl可以設定不要等待他回來的就果嗎~因為每次等他回來的結果都需花費一定的時間,造成畫面呈現過慢~

  13. 你好 說道:

    博主你好 必应网页根据 地区跳转 curl 能想办法支持吗

  14. kei 說道:

    你好
    我用了你分析的程序
    在http連線上沒有問題
    但https上就沒有error
    但是return是空的

  15. Kei 說道:

    加入了下列2行代碼就OK ~thx~
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);

  16. jesse 說道:

    不错,测试了几个,就您的代码能正常跑。

發表迴響