> 网络爬虫(又被称为网页蜘蛛,网络机器人,在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)