一般 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 的效果.
- 官方文件: PHP: curl_multi_init
程式(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 ?>
/* 執行 */
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/
果然會造成此問題, 測試過 http://www.hengss.com/xueyuan/sort0362/php/info-36963.html 的解法, 可以解決此問題.
已經將此篇的程式修正, 萬分感謝~ orz.
避免掉 CPU loading 100% 的問題的這種方法在PHP 5.3下不曉得為何會有非預期的結果
非預期的效果, 指的是什麼?
我用了這個async_get_url
我並發十條線路去連不同的資料庫的資料表運算,
在我的個人電腦XP Appserv裡面很正常, 但放到公司正式Linux主機上,
發現返回的資料時有時無, 沒有一次10個資料都全的, 很不穩定,
我加了下面二個參數設定, 不過沒什麼用
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
curl_setopt($ch, CURLOPT_REFERER, $url); //設定 referer
上網找了一下有沒有什麼因素, 看到有人說詳細因素有很多可能
可能主機附載能力太差, 無法負荷太多連線數,
可能伺服器網路頻寬不足或網路設備不穩封包遺失
不過我在想LINUX主機應該比個人電腦強吧..
而且我的電腦也是在公司的網路裡面啊...
沒道理我的XP裝Appserv跑的都沒問題, 10筆資料沒有漏失的,
所以尚不知道造成的原因
這個狀況我猜應該是 1. 網路問題, 2. 系統 Loading 太重(或者連現已滿), 不然不太可能會有這種狀況. 🙂
博主你好,我在网络上参考了很多curl多线程的方法,无一没有例外的都会在if (curl_multi_select($mh) != -1)上陷入死循环,我的php版本分别是5.3.13和5.4.4,我怀疑是不是在5.3以上的版本中都会有问题,希望博主看到后能给出一个解决办法,thank you.
是不是你的 curl 拉回資料端, 有其中某些會卡住, 所以造成此狀況?
确实是有这个问题,而且网上的大部分资料都是这么写的,我也不知道何解。
博主,如果我的$url_array是個大數組,例如一千條的數組,會不會電腦卡死呢?
看你的記憶體大小吧, 才 1000條算小的, 不用在意~ 🙂
博主,我在本地電腦上測試了一下,發現連接超時,不知道是怎麼一種情況?
(Fatal error: Maximum execution time of 60 seconds exceeded in E:)
Timeout 了, 看看是不是無法連線, 或是無窮迴圈了?
當我把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
感謝提供, 先將修改過的 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); } }
curl_multi_exec的返回值是用來返回多線程處裡時的錯誤,正常來說返回值是0,也就是說只用$mrc捕捉返回值當成判斷式的迴圈只會運行一次,而真的發生錯誤時,有拿$mrc判斷的都會變死迴圈。
而curl_multi_select的功能是curl發送請求後,在有回應前會一直處於等待狀態,所以不需要把它導入空迴圈,它就像是會自己做判斷&自己決定等待時間的sleep()。
用以下更簡潔的寫法就能把迴圈次數減到最少,請測試看看。
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
萬分感謝, 清楚易懂~ 我先更新此文, 等會把 code 修改上線測試看看~ Orz.
原先很納悶為什麼連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沒問題的情形下去寫,其他版本可以請大家再測試看看。
我在 PHP 5.4.4 測試, 目前測試的情況是.
用原本的 code, 抓取資料的時間, 都會落在 10.04secs 左右, 很固定.
但是改成這段 code 後, 時間都會落在 11.038 secs 和 16.079 secs (都會這兩個值互相跳動).
註: 測試 code 如文章寫的, 用 sleep 做測試.
詳細原因還不明, 您遇到的問題也是這樣子嗎?
明天再來追追看, 我先改回原本的 code, 萬分感謝~ Orz.
我之前測試的時候只有注意CPU使用率、curl_multi_select回傳值還有迴圈跑的次數,沒有注意到抓取時間是多少。
另外補充一下,後來在Lnux平台上的PHP 5.2.17上測試,curl_multi_select沒有無法暫停的錯誤發生,先前回復的那四行code可以正常運行,所以還不確定到底是PHP版本問題還是作業系統的問題。先前五個PHP版本都是在Windows 7 x64上進行測試的。
嗯嗯, 我只有 Linux 的環境, 我再來測測看其他 PHP 版本.
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
這串代碼在windows8 x64,PHP5.5.3下工作很好,迴圈數也很少。
嗯嗯, 測試過看起來這個是最穩定的, 所以現在就留下這個版本囉~ 🙂
請問一下,使用curl可以設定不要等待他回來的就果嗎~因為每次等他回來的結果都需花費一定的時間,造成畫面呈現過慢~
建議會造成 Delay 的這種, 用 AJAX 發送, 會比較容易解決此問題. 🙂
博主你好 必应网页根据 地区跳转 curl 能想办法支持吗
CURL 有個設定可以跟著 302 redirect 跑的, 查查看就會知道了~
你好
我用了你分析的程序
在http連線上沒有問題
但https上就沒有error
但是return是空的
https 需要加上 CURL SSL 的參數.
加入了下列2行代碼就OK ~thx~
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
不错,测试了几个,就您的代码能正常跑。