用Swoole異步抓取網頁實戰分享

*文
發布: 2023-03-18 08:02:01
原創
5263 人瀏覽過

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 = &#39;#((http|ftp)://(\S*?\.\S*?))([\s)\[\]{},;"\&#39;:<]|\.\s|$)#i&#39;;
        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 &#39;Sync/Crawler.php&#39;;
$start = microtime(true);
$url = &#39;http://www.swoole.com/&#39;;
$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([
        &#39;Host&#39; => $domainName,
        "User-Agent" => &#39;Chrome/49.0.2587.3&#39;,
        &#39;Accept&#39; => &#39;text/html,application/xhtml+xml,application/xml&#39;,
        &#39;Accept-Encoding&#39; => &#39;gzip&#39;,
    ]);
    $cli->get(&#39;/index.html&#39;, 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 = &#39;#((http|ftp)://(\S*?\.\S*?))([\s)\[\]{},;"\&#39;:<]|\.\s|$)#i&#39;;
        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[&#39;host&#39;], function ($domainName, $ip) use($urlInfo, $root) {
            $cli = new swoole_http_client($ip, 80);
            $cli->setHeaders([
                &#39;Host&#39; => $domainName,
                "User-Agent" => &#39;Chrome/49.0.2587.3&#39;,
                &#39;Accept&#39; => &#39;text/html,application/xhtml+xml,application/xml&#39;,
                &#39;Accept-Encoding&#39; => &#39;gzip&#39;,
            ]);
            $cli->get($urlInfo[&#39;path&#39;], function ($cli) use ($root) {
                if ($root) {
                    $this->loadPage($cli->body);
                    $this->loaded = true;
                }
            });
        });
    }
}
登入後複製
<?php
/**
 * crawler.php
 */
require_once &#39;Async/CrawlerV1.php&#39;;
$start = microtime(true);
$url = &#39;http://www.swoole.com/&#39;;
$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異步抓取網頁實戰分享

#然而真實的場景不是這樣的。透過調試,我大致了解到實際的模型應該是這樣的:


用Swoole異步抓取網頁實戰分享

也就是說,無論我怎麼提高重試次數,數據永遠不會準備好,數據只有在當前函數準備好以後,才會開始執行,這裡的異步,只是減少了準備連接的時間。
那麼問題來了,我該如何讓程式在準備資料之後執行我期望的功能。
先來看看Swoole官方執行非同步任務的程式碼是如何寫的

$serv = new swoole_server("127.0.0.1", 9501);
//设置异步任务的工作进程数量
$serv->set(array(&#39;task_worker_num&#39; => 4));
$serv->on(&#39;receive&#39;, function($serv, $fd, $from_id, $data) {
    //投递异步任务
    $task_id = $serv->task($data);
    echo "Dispath AsyncTask: id=$task_id\n";
});
//处理异步任务
$serv->on(&#39;task&#39;, function ($serv, $task_id, $from_id, $data) {
    echo "New AsyncTask[id=$task_id]".PHP_EOL;
    //返回任务执行的结果
    $serv->finish("$data -> OK");
});
//处理异步任务的结果
$serv->on(&#39;finish&#39;, 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 &#39;Async/Crawler.php&#39;;
$start = microtime(true);
$url = &#39;http://www.swoole.com/&#39;;
$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,只是牺牲可维护性和开发效率。
登入後複製

#相關閱讀:

如何用php7安裝swoole擴充功能

Swoole開發重點介紹

php異步多執行緒swoole用法實例

以上就是這篇文章的所有內容,同學們如果有疑問可以在下方留言區討論喔~

以上是用Swoole異步抓取網頁實戰分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!