上面一個系列的教程:
說的都是隻相容unix 伺服器的多程式,下面來講講在window 和 unix 都相容的多程式(這裡是泛指,下面的curl實際上是通過IO複用實現的)。
通過擴充套件實現多執行緒的典型例子是CURL,CURL 支援多執行緒的抓取網頁的功能。
這部分過於抽象,所以,我先給出一個CURL並行抓取多個網頁內容的一個分裝類。這個類實際上很實用,
詳細分析這些函式的內部實現將在下一個教程裡面描述。
你可能不能很好的理解這個類,而且,php curl 官方主頁上都有很多錯誤的例子,在講述了其內部機制
後,你就能夠明白了。
先看程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
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 去寫爬蟲了,對於一箇中型大小的爬蟲,有這個內部多執行緒,就已經足夠了。
最後是上面的類的一個測試的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$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"; |