ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
> 网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。 以上的定义来自 百度百科 。今天我就给大家是实现一个可以简易爬取新闻的小爬虫。当然,如果严格意义上讲,把它当成一个成熟的爬虫,那还相差很远,只能说算是一个小的试验。但是,它基本可以满足我们从一些网站上,采集一些有用的信息下来的目的了。 首先来介绍一下,我们需要准备哪些工具: 1. 可以启动多线程请求的 curl 类 2. 可以像 jquery 那样解析 dom 的 phpQuery 类 3. ThinkPHP5命令行工具 下面我们来一 一添加这些工具,并完成简单爬虫的制作。 ## 添加 curl 类 其实 php 的http请求类库有很多的,其中很优秀的 guzzle 。但是本教程不打算采用这个(因为我也不太熟悉这个类库)。当然,我们制作的是一个简单的小爬虫,可替代的方案有很多,甚至你可以直接使用 file\_get\_contents 。考虑到简答的并发抓取的问题,于是我在网上寻找了一个不是很复杂,但是很好用的 curl 类库。我们新建 extend\\curl\\MultiCurl.php ~~~ <?php namespace curl; /* * Multi curl in PHP * @author rainyluo * @date 2016-04-15 */ class MultiCurl { //urls needs to be fetched public $targets = []; //parallel running curl threads public $threads = 10; //curl options public $curlOpt = []; //callback function public $callback = null; //debug ,will show log using echo public $debug = true; //multi curl handler private $mh = null; //curl running signal private $runningSig = null; /** * 架构函数 */ public function __construct() { $this->mh = curl_multi_init(); $this->curlOpt = [ CURLOPT_HEADER => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 10, CURLOPT_AUTOREFERER => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, ]; $this->callback = function ($html) { echo md5($html); echo "fetched"; echo "\r\n"; }; } /** * 设置目标数 * @param unknown $urls * @return \extend\MultiCurl */ public function setTargets($urls) { $this->targets = $urls; return $this; } /** * 设置线程数 * @param unknown $threads * @return \extend\MultiCurl */ public function setThreads($threads) { $this->threads = intval($threads); return $this; } /** * 设置回调函数 * @param unknown $func * @return \extend\MultiCurl */ public function setCallback($func) { $this->callback = $func; return $this; } /* * start running */ public function run() { $this->initPool(); $this->runCurl(); } /* * run multi curl */ private function runCurl() { do { //start request thread and wait for return,if there's no return in 1s,continue add request thread do { curl_multi_exec($this->mh, $this->runningSig); //$this->log("exec results...running sig is" . $this->runningSig); $return = curl_multi_select($this->mh, 1.0); if ($return > 0) { //$this->log("there is a return...$return"); break; } unset($return); } while ($this->runningSig > 0); //if there is return,read it while ($returnInfo = curl_multi_info_read($this->mh)) { $handler = $returnInfo["handle"]; if ($returnInfo["result"] == CURLE_OK) { $url = curl_getinfo($handler, CURLINFO_EFFECTIVE_URL); //$this->log($url . "returns data"); $callback = $this->callback; $callback(curl_multi_getcontent($handler)); } else { $url = curl_getinfo($handler, CURLINFO_EFFECTIVE_URL); //$this->log("$url fetch error." . curl_error($handler)); } curl_multi_remove_handle($this->mh, $handler); curl_close($handler); unset($handler); //add new targets into curl thread if ($this->targets) { $threadsIdel = $this->threads - $this->runningSig; //$this->log("idel threads:" . $threadsIdel); if ($threadsIdel < 0) continue; for ($i = 0; $i < $threadsIdel; $i++) { $t = array_pop($this->targets); if (!$t) continue; $task = curl_init($t); curl_setopt_array($task, $this->curlOpt); curl_multi_add_handle($this->mh, $task); //$this->log("new task adds!" . $task); $this->runningSig += 1; unset($task); } } else { //$this->log("targets all finished"); } } } while ($this->runningSig); } /* * init multi curl pool */ private function initPool() { if (count($this->targets) < $this->threads) $this->threads = count($this->targets); //init curl handler pool ... for ($i = 1; $i <= $this->threads; $i++) { $task = curl_init(array_pop($this->targets)); curl_setopt_array($task, $this->curlOpt); curl_multi_add_handle($this->mh, $task); //$this->log("init pool thread one"); unset($task); } // $this->log("init pool done"); } /** * 日志函数 * @param unknown $log * @return boolean */ private function log($log) { if (!$this->debug) return false; ob_start(); echo "---------- " . date("Y-m-d H:i", time()) . "-------------"; if (is_array($log)) { echo json_encode($log); } else { echo $log; } $m = memory_get_usage(); echo "memory:" . intval($m / 1024) . "kb\r\n"; echo "\r\n"; flush(); ob_end_flush(); unset($log); } /** * 析构函数 */ public function __destruct() { //$this->log("curl ends."); curl_multi_close($this->mh); } } ~~~ ## 下载 phpquery 类库 我们到[packagist](https://packagist.org/)搜索 phpquery ,会看到 phpquery 包的详情。复制安装命令,打开 cmd,进入项目根目录 ~~~ composer require electrolinux/phpquery ~~~ 等待下载完成即可。 接下来,应该讲解 thinkphp5 命令行的用法,可是我感觉这不是重点。怎么使用,你可以参考[自定义命令行](http://www.kancloud.cn/manual/thinkphp5/235129)。下面我想给大家讲解一下 这个 curl 类库 和 如何通过 phpquery 解析数据,得到我们想要的数据。方便起见,我只演示 采集文章标题和地址。新建一个表记录这些数据: ~~~ DROP TABLE IF EXISTS `article_title`; CREATE TABLE `article_title` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(155) NOT NULL COMMENT '文章标题', `href` varchar(155) NOT NULL COMMENT '文章链接', PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; ~~~ 本文演示,以砍柴网为列。砍柴网的创频道[http://www.ikanchai.com/article/](http://www.ikanchai.com/article/)。采集一般我们选择列表页来进行,因为这里的内容集中,而且 url 有一定的规律性,我们点击下面的分页按钮,地址栏就会展示出有规律的列表地址。 ~~~ http://www.ikanchai.com/article/index_1.shtml http://www.ikanchai.com/article/index_2.shtml ~~~ 这样每一页的列表页,都是通过 index\_x 后的数字来表示,因此我们很容易构建出很多的采集 url。下面来讲解一下,那个curl 类的基本使用方法: ~~~ $mu = new MultiCurl(); // 需要采集的列表数据 $urls = [ 'http://www.ikanchai.com/article/index_1.shtml', 'http://www.ikanchai.com/article/index_2.shtml' ]; // 获取内容回调函数 $callback = function($html) { // do something }; $mu->setTargets($urls)->setCallback($callback)->setThreads(5)->run(); ~~~ > 1. 实例化 curl 类 > 2. 定义需要采集的 url 集合 > 3. 定义成功采集之后的回调函数 > 4. 设置采集集合,设置回调函数,设置启动线程数,启动采集 我们要做的重点就是,如何在回调函数中,解析出文章的标题和地址,并且存入数据看。 ![](https://box.kancloud.cn/d3c427cab264448b6f4ad17088c555d4_1496x867.jpg) 我们通过 F12 可以看出,他的文章内容都是在 ~~~ <div class="hlgd-content"></div> ~~~ 里面包裹的,而且所有的标题是以一个循环结构展现的。循环结构的 div 为 ~~~ <div class="hlgd-box"></div> ~~~ 因此,我们在 回调函数中的 phpquery 要这么写: ~~~ // 获取内容回调函数 $callback = function($html) { $res = \phpQuery::newDocument($html); // 所有标题区域的 div 对象 $div = $res['.hlgd-content .hlgd-box']; // 循环获取查看每一个 div 中的标题信息 foreach($div as $div){ $title = pq($div)->find('h3 a')->attr('title'); $href = pq($div)->find('h3 a')->attr('href'); db('article_title')->insert(compact('title', 'href')); } }; ~~~ > 如果你熟悉 jquery 的话,很容易理解这部分的写法。另外,具体的 phpquery 该如何使用,篇幅有限,水平有限,请自行百度。 ## 通过 command 进行采集 我们在 application\\command.php 中定义: ~~~ return [ 'app\index\command\Spider' ]; ~~~ 建立命令类文件,新建application/index/command/Spider.php ~~~ <?php namespace app\index\command; use think\console\Command; use think\console\Input; use think\console\Output; use curl\MultiCurl; class Spider extends Command { protected function configure() { $this->setName('spider')->setDescription('spider running '); } protected function execute(Input $input, Output $output) { $mu = new MultiCurl(); // 需要采集的列表数据 $urls = [ 'http://www.ikanchai.com/article/index_1.shtml', 'http://www.ikanchai.com/article/index_2.shtml' ]; // 获取内容回调函数 $callback = function($html) { $res = \phpQuery::newDocument($html); // 所有标题区域的 div 对象 $div = $res['.hlgd-content .hlgd-box']; // 循环获取查看每一个 div 中的标题信息 foreach($div as $div){ $title = pq($div)->find('h3 a')->attr('title'); $href = pq($div)->find('h3 a')->attr('href'); db('article_title')->insert(compact('title', 'href')); } }; $mu->setTargets($urls)->setCallback($callback)->setThreads(5)->run(); $output->writeln("complete"); } } ~~~ 打开 cmd 进入 系统根目录,执行 ~~~ php think spider ~~~ ![](https://box.kancloud.cn/620567b14d3310d2c08053dc5062527a_396x139.jpg) 看到 complete 则采集完成。打开表可见: ![](https://box.kancloud.cn/93b4df037565fd330719c8781c923748_613x893.jpg)