上面一個系列的教程:
PHP多進程編程(一)
PHP多進程編程(二)管道通訊
PHP多進程編程(三)多進程抓取網頁的示範
說的都是只相容unix 伺服器的多進程,下面來講講在window 和 unix 都相容的多進程(這裡是泛指,下面的curl實際上是通過IO複用實現的)。
通過擴充實現多線程的典型例子是CURL,CURL 支援多線程的抓取網頁的功能。
這部分過於抽象,所以,我先給出一個CURL並行抓取多個網頁內容的一個分裝類。這個類實際上很實用,
詳細分析這些函數的內部實現將在下一個教程裡面描述。
你可能不能很好的理解這個類,而且,php curl 官方首頁上都有很多錯誤的例子,在講述了其內部機制
後,你就能夠明白了。
先看代碼:
class Http_MultiRequest{ //要並行抓取的url 列表 private $urls = array(); //curl 的選項 private $options; //建構函式 function __construct($options = array()) { $this->setOptions($options); } //設定url 列表 function setUrls($urls) { $this->urls = $urls; return $this; } //設定選項 function setOptions($options) { $options[CURLOPT_RETURNTRANSFER] = 1; if (isset($options['HTTP_POST'])) { curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $options['HTTP_POST']); unset($options['HTTP_POST']); } if (!isset($options[CURLOPT_USERAGENT])) { $options[CURLOPT_USERAGENT] = 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1;)'; } if (!isset($options[CURLOPT_FOLLOWLOCATION])) { $options[CURLOPT_FOLLOWLOCATION] = 1; } if (!isset($options[CURLOPT_HEADER])) { $options[CURLOPT_HEADER] = 0; } $this->options = $options; } //並行抓取所有的內容 function exec() { if(empty($this->urls) || !is_array($this->urls)) { return false; } $curl = $data = array(); $mh = curl_multi_init(); foreach($this->urls as $k => $v) { $curl[$k] = $this->addHandle($mh, $v); } $this->execMulitHandle($mh); foreach($this->urls as $k => $v) { $data[$k] = curl_multi_getcontent($curl[$k]); curl_multi_remove_handle($mh, $curl[$k]); } curl_multi_close($mh); return $data; } //只抓取一個網頁的內容。 function execOne($url) { if (empty($url)) { return false; } $ch = curl_init($url); $this->setOneOption($ch); $content = curl_exec($ch); curl_close($ch); return $content; } //內建函式,設定某個handle 的選項 private function setOneOption($ch) { curl_setopt_array($ch, $this->options); } //添加一個新的並行抓取 handle private function addHandle($mh, $url) { $ch = curl_init($url); $this->setOneOption($ch); curl_multi_add_handle($mh, $ch); return $ch; } //並存執行(這樣的寫法是一個常見的錯誤,我這裡還是採用這樣的寫法,這個寫法 //下載一個小檔案都可能導致cup佔用100%, 並且,這個迴圈會運行10萬次以上 //這是一個典型的不懂原理產生的錯誤。這個錯誤在PHP官方的文檔上都相當的常見。) private function execMulitHandle2($mh) { $i = 0; $running = null; do { curl_multi_exec($mh, $running); $i++; } while ($running > 0); //var_dump($i); } //應該用這樣的寫法 private function execMulitHandle($mh) { $i = 0; do {$mrc = curl_multi_exec($mh,$active); $i++;} while ($mrc == CURLM_CALL_MULTI_PERFORM); while ($active && $mrc == CURLM_OK) { if (curl_multi_select($mh) != -1) { do {$mrc = curl_multi_exec($mh, $active); $i++;} while ($mrc == CURLM_CALL_MULTI_PERFORM); } $i++; } //var_dump($i); }}
看最後一個注釋最多的函數,這個錯誤在平時調試的時候可能不太容易發現,因為程式完全正常,但是,在生產伺服器下,馬上會引起崩潰效果。
解釋為什麼不能這樣,必須從C 語言內部實現的角度來分析。這個部分將放到下一個教程(PHP進階編程之–單線程實現並行抓取網頁 )。不過不是通過C語言來表述原理,而是通過PHP
這個類,實際上也就很簡單的實現了前面我們費了4個教程的篇幅,並且是九牛二虎之力才實現的多線程的抓取網頁的功能。在純PHP的實現下,我們只能用一個後台服務的方式來比較好的實現,但是當你使用 作業系統介面語言 C 語言時候,這個實現當然就更加的簡單,靈活,高效。
就同時抓取幾個網頁這樣一件簡單的事情,實際上在底層涉及到了很多東西,對很多半路出家的PHP程式員,可能不喜歡談多線程這個東西,深入了就涉及到作業系統,淺點說就是並行運行好幾個“程式”。但是,很多時候,多線程必不可少,比如要寫個快點的爬蟲,往往就會浪費九牛二虎之力。不過,PHP的程式員現在應該感謝CURL 這個擴充,這樣,你完全不需要用你不太精通的 python 去寫爬蟲了,對於一個中型大小的爬蟲,有這個內部多線程,就已經足夠了。
最後是上面的類的一個測試的例子:
$urls = array("http://baidu.com", "http://baidu.com", "http://baidu.com", "http://baidu.com", "http://baidu.com", "http://baidu.com", "http://www.google.com", "http://www.sina.com.cn", );$m = new Http_MultiRequest();$t = microtime(true);$m->setUrls($urls);//parallel fetch(並行抓取):$data = $m->exec();$parallel_time = microtime(true) - $t;echo $parallel_time . "\n";$t = microtime(true);//serial fetch(串列抓取):foreach ($urls as $url){ $data[] = $m->execOne($url);}$serial_time = microtime(true) - $t;echo $serial_time . "\n";