php程式設計師都知道,使用php寫的程式都是同步的,如何用php寫一個非同步程式呢,答案就是Swoole。這裡以抓取網頁內容為例,來展示如何用Swoole來寫非同步程式。
php的同步程式
在寫非同步程式之前,不要急,先用php實作同步的程式。
<?php /** * Class Crawler * Path: /Sync/Crawler.php */ class Crawler { private $url; private $toVisit = []; public function __construct($url) { $this->url = $url; } public function visitOneDegree() { $this->loadPageUrls(); $this->visitAll(); } private function loadPageUrls() { $content = $this->visit($this->url); $pattern = '#((http|ftp)://(\S*?\.\S*?))([\s)\[\]{},;"\':<]|\.\s|$)#i'; preg_match_all($pattern, $content, $matched); foreach ($matched[0] as $url) { if (in_array($url, $this->toVisit)) { continue; } $this->toVisit[] = $url; } } private function visitAll() { foreach ($this->toVisit as $url) { $this->visit($url); } } private function visit($url) { return @file_get_contents($url); } }
<?php /** * crawler.php */ require_once 'Sync/Crawler.php'; $start = microtime(true); $url = 'http://www.swoole.com/'; $ins = new Crawler($url); $ins->visitOneDegree(); $timeUsed = microtime(true) - $start; echo "time used: " . $timeUsed; /* output: time used: 6.2610177993774 */
Swoole實作非同步爬蟲初探
先參考一下官方的非同步抓取頁面怎麼搞。
使用範例
Swoole\Async::dnsLookup("www.baidu.com", function ($domainName, $ip) { $cli = new swoole_http_client($ip, 80); $cli->setHeaders([ 'Host' => $domainName, "User-Agent" => 'Chrome/49.0.2587.3', 'Accept' => 'text/html,application/xhtml+xml,application/xml', 'Accept-Encoding' => 'gzip', ]); $cli->get('/index.html', function ($cli) { echo "Length: " . strlen($cli->body) . "\n"; echo $cli->body; }); });
似乎稍微改造一下同步的file_get_contents程式碼,就可以實現異步了,看起來成功輕而易舉。
於是,我們得到了下面的程式碼:
<?php /** * Class Crawler * Path: /Async/CrawlerV1.php */ class Crawler { private $url; private $toVisit = []; private $loaded = false; public function __construct($url) { $this->url = $url; } public function visitOneDegree() { $this->visit($this->url, true); $retryCount = 3; do { sleep(1); $retryCount--; } while ($retryCount > 0 && $this->loaded == false); $this->visitAll(); } private function loadPage($content) { $pattern = '#((http|ftp)://(\S*?\.\S*?))([\s)\[\]{},;"\':<]|\.\s|$)#i'; preg_match_all($pattern, $content, $matched); foreach ($matched[0] as $url) { if (in_array($url, $this->toVisit)) { continue; } $this->toVisit[] = $url; } } private function visitAll() { foreach ($this->toVisit as $url) { $this->visit($url); } } private function visit($url, $root = false) { $urlInfo = parse_url($url); Swoole\Async::dnsLookup($urlInfo['host'], function ($domainName, $ip) use($urlInfo, $root) { $cli = new swoole_http_client($ip, 80); $cli->setHeaders([ 'Host' => $domainName, "User-Agent" => 'Chrome/49.0.2587.3', 'Accept' => 'text/html,application/xhtml+xml,application/xml', 'Accept-Encoding' => 'gzip', ]); $cli->get($urlInfo['path'], function ($cli) use ($root) { if ($root) { $this->loadPage($cli->body); $this->loaded = true; } }); }); } }
<?php /** * crawler.php */ require_once 'Async/CrawlerV1.php'; $start = microtime(true); $url = 'http://www.swoole.com/'; $ins = new Crawler($url); $ins->visitOneDegree(); $timeUsed = microtime(true) - $start; echo "time used: " . $timeUsed; /* output: time used: 3.011773109436 */
結果運行了3秒。注意我的實現,在發起抓取首頁的請求以後,我會隔一秒輪詢一次結果,輪詢三次還沒就結束了。這裡的3秒鐘好像是輪詢了3次還沒結果導致的退出。
看來是我太急躁了,給人家的準備時間還不夠充分。好吧,那我們把輪詢次數改為10次,看看結果。
time used: 10.034232854843
此時我的心情,你懂的。
難道說是swoole的效能問題?為什麼10秒還沒結果,難道是我的姿勢不對?馬克思老人家說:「實踐是檢驗真理的唯一標準」。看來需要debug一下才知道原因了。
於是,我在
$this->visitAll();
和
$this->loadPage($cli->body);
兩處加了斷點。最後發現總是先執行到visitAll(),再去執行loadPage()。
想了一下,大概明白原因了。到底是什麼原因呢?
我期望的非同步動態模型是這樣的:
#然而真實的場景不是這樣的。透過調試,我大致了解到實際的模型應該是這樣的:
也就是說,無論我怎麼提高重試次數,數據永遠不會準備好,數據只有在當前函數準備好以後,才會開始執行,這裡的異步,只是減少了準備連接的時間。
那麼問題來了,我該如何讓程式在準備資料之後執行我期望的功能。
先來看看Swoole官方執行非同步任務的程式碼是如何寫的
$serv = new swoole_server("127.0.0.1", 9501); //设置异步任务的工作进程数量 $serv->set(array('task_worker_num' => 4)); $serv->on('receive', function($serv, $fd, $from_id, $data) { //投递异步任务 $task_id = $serv->task($data); echo "Dispath AsyncTask: id=$task_id\n"; }); //处理异步任务 $serv->on('task', function ($serv, $task_id, $from_id, $data) { echo "New AsyncTask[id=$task_id]".PHP_EOL; //返回任务执行的结果 $serv->finish("$data -> OK"); }); //处理异步任务的结果 $serv->on('finish', function ($serv, $task_id, $data) { echo "AsyncTask[$task_id] Finish: $data".PHP_EOL; }); $serv->start();
可以看到,官方是透過一個function匿名函數,將後續的執行邏輯傳了進去。這麼看,事情就變得簡單多了。
url = $url; } public function visitOneDegree() { $this->visit($this->url, function ($content) { $this->loadPage($content); $this->visitAll(); }); } private function loadPage($content) { $pattern = '#((http|ftp)://(\S*?\.\S*?))([\s)\[\]{},;"\':<]|\.\s|$)#i'; preg_match_all($pattern, $content, $matched); foreach ($matched[0] as $url) { if (in_array($url, $this->toVisit)) { continue; } $this->toVisit[] = $url; } } private function visitAll() { foreach ($this->toVisit as $url) { $this->visit($url); } } private function visit($url, $callBack = null) { $urlInfo = parse_url($url); Swoole\Async::dnsLookup($urlInfo['host'], function ($domainName, $ip) use($urlInfo, $callBack) { if (!$ip) { return; } $cli = new swoole_http_client($ip, 80); $cli->setHeaders([ 'Host' => $domainName, "User-Agent" => 'Chrome/49.0.2587.3', 'Accept' => 'text/html,application/xhtml+xml,application/xml', 'Accept-Encoding' => 'gzip', ]); $cli->get($urlInfo['path'], function ($cli) use ($callBack) { if ($callBack) { call_user_func($callBack, $cli->body); } $cli->close(); }); }); } }
看了這段程式碼,竟然有種似曾相識的感覺,在nodejs開發中,隨處可見的callback原來是有它的道理的。現在我才突然明白,原來callback的存在就是為了解決非同步問題的。
執行了一下程序,竟然只用0.0007s,還沒開始就已經結束了!異步的效率真的能提升這麼多嗎?答案當然是否定的,是我們的程式碼出問題了。
由於用了非同步,沒有等任務完全跑完,就已經執行了計算結束的時間的邏輯。看來又到了用callback的時候了。
/** Async/Crawler.php **/ public function visitOneDegree($callBack) { $this->visit($this->url, function ($content) use($callBack) { $this->loadPage($content); $this->visitAll(); call_user_func($callBack); }); }
<?php /** * crawler.php */ require_once 'Async/Crawler.php'; $start = microtime(true); $url = 'http://www.swoole.com/'; $ins = new Crawler($url); $ins->visitOneDegree(function () use($start) { $timeUsed = microtime(true) - $start; echo "time used: " . $timeUsed; }); /*output: time used: 0.068463802337646 */
現在來看,結果可信多了。
讓我們比較同步的非同步的差距,同步耗時6.26s,非同步耗時0.068秒,差了整整6.192s。不,更準確地表述,應該是差了將近10倍!
當然,從效率上講,非同步遠高於同步的程式碼,但是從邏輯上講,非同步的邏輯比同步更繞,程式碼中會帶來大量的callback,不便於理解。
Swoole官方裡有一段關於非同步與同步的選擇的描述,非常中肯,分享給大家:
我们不赞成用异步回调的方式去做功能开发,传统的PHP同步方式实现功能和逻辑是最简单的,也是最佳的方案。像node.js这样到处callback,只是牺牲可维护性和开发效率。
#相關閱讀:
以上就是這篇文章的所有內容,同學們如果有疑問可以在下方留言區討論喔~
以上是用Swoole異步抓取網頁實戰分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!