简易爬虫设计
引言
说这是一个爬虫有点说大话了,但这个名字又恰到好处,所以在前面加了”简易“两个字,表明
这是一个阉割的爬虫,简单的使用或者玩玩儿还是可以的。
公司最近有新的业务要去抓取竞品的数据,看了之前的同学写的抓取系统,存在一定的问题,
规则性太强了,无论是扩展性还是通用性发面都稍微弱了点,之前的系统必须要你搞个列表,
然后从这个列表去爬取,没有深度的概念,这对爬虫来说简直是硬伤。因此,我决定搞一个
稍微通用点的爬虫,加入深度的概念,扩展性通用型方面也提升下。
设计
我们这里约定下,要处理的内容(可能是url,用户名之类的)我们都叫他实体(entity)。
考虑到扩展性这里采用了队列的概念,待处理的实体全部存储在队列中,每次处理的时候,
从队列中拿出一个实体,处理完成之后存储,并将新抓取到的实体存入队列中。当然了这里
还需要做存储去重处理,入队去重处理,防止处理程序做无用功。
+--------+ +-----------+ +----------+
| entity | | enqueue | | result |
| list | | uniq list | | uniq list|
| | | | | |
| | | | | |
| | | | | |
| | | | | |
+--------+ +-----------+ +----------+
当每个实体进入队列的时候入队排重队列
设置入队实体标志为一后边不再入队,当处理完
实体,得到结果数据,处理完结果数据之后将结果诗句标志如结果数据排重list
,当然了
,这里你也可以做更新处理,代码中可以做到兼容。
+-------+
| 开始 |
+---+---+
|
v
+-------+ enqueue deep为1的实体
| init |-------------------------------->
+---+---+ set 已经入过队列 flag
|
v
+---------+ empty queue +------+
+------>| dequeue +------------->| 结束 |
| +----+----+ +------+
| |
| |
| |
| v
| +---------------+ enqueue deep为deep+1的实体
| | handle entity |------------------------------>
| +-------+-------+ set 已经入过队列 flag
| |
| |
| v
| +---------------+ set 已经处理过结果 flag
| | handle result |-------------------------->
| +-------+-------+
| |
+------------+
爬取策略(反作弊应对)
为了爬取某些网站,最怕的就是封ip,封了ip入过没有代理就只能呵呵呵了。因此,爬取
策略还是很重要的。
爬取之前可以先在网上搜搜待爬取网站的相关信息,看看之前有没有前辈爬取过,吸收他
门的经验。然后就是是自己仔细分析网站请求了,看看他们网站请求的时候会不会带上特
定的参数?未登录状态会不会有相关的cookie?最后就是尝试了,制定一个尽可能高的抓
取频率。
如果待爬取网站必须要登录的话,可以注册一批账号,然后模拟登陆成功,轮流去请求,
如果登录需要验证码的话就更麻烦了,可以尝试手动登录,然后保存cookie的方式(当然
,有能力可以试试ocr识别)。当然登陆了还是需要考虑上一段说的问题,不是说登陆了就
万事大吉,有些网站登录之后抓取频率过快会封掉账号。
所以,尽可能还是找个不需要登录的方法,登录被封账号,申请账号、换账号比较麻烦。
抓取数据源和深度
初始数据源选择也很重要。我要做的是一个每天抓取一次,所以我找的是带抓取网站每日
更新的地方,这样初始化的动作就可以作为全自动的,基本不用我去管理,爬取会从每日
更新的地方自动进行。
抓取深度也很重要,这个要根据具体的网站、需求、及已经抓取到的内容确定,尽可能全
的将网站的数据抓过来。
优化
在生产环境运行之后又改了几个地方。
第一就是队列这里,改为了类似栈的结构。因为之前的队列,deep小的实体总是先执行,
这样会导致队列中内容越来越多,内存占用很大,现在改为栈的结构,递归的先处理完一个
实体的所以深度,然后在处理下一个实体。比如说初始10个实体(deep=1),最大爬取深度
是3,每一个实体下面有10个子实体,然后他们队列最大长度分别是:
队列(lpush,rpop) => 1000个
修改之后的队列(lpush,lpop) => 28个
上面的两种方式可以达到同样的效果,但是可以看到队列中的长度差了很多,所以改为第二
中方式了。
最大深度限制是在入队的时候处理的,如果超过最大深度,直接丢弃。另外对队列最大长度
也做了限制,让制意外情况出现问题。
代码
下面就是又长又无聊的代码了,本来想发在github,又觉得项目有点小,想想还是直接贴出来吧,不好的地方还望看朋友们直言不讳,不管是代码还是设计。
abstract class SpiderBase
{
/**
* @var 处理队列中数据的休息时间开始区间
*/
public $startMS = 1000000;
/**
* @var 处理队列中数据的休息时间结束区间
*/
public $endMS = 3000000;
/**
* @var 最大爬取深度
*/
public $maxDeep = 1;
/**
* @var 队列最大长度,默认1w
*/
public $maxQueueLen = 10000;
/**
* @desc 给队列中插入一个待处理的实体
* 插入之前调用 @see isEnqueu 判断是否已经如果队列
* 直插入没如果队列的
*
* @param $deep 插入实体在爬虫中的深度
* @param $entity 插入的实体内容
* @return bool 是否插入成功
*/
abstract public function enqueue($deep, $entity);
/**
* @desc 从队列中取出一个待处理的实体
* 返回值示例,实体内容格式可自行定义
* [
* "deep" => 3,
* "entity" => "balabala"
* ]
*
* @return array
*/
abstract public function dequeue();
/**
* @desc 获取待处理队列长度
*
* @return int
*/
abstract public function queueLen();
/**
* @desc 判断队列是否可以继续入队
*
* @param $params mixed
* @return bool
*/
abstract public function canEnqueue($params);
/**
* @desc 判断一个待处理实体是否已经进入队列
*
* @param $entity 实体
* @return bool 是否已经进入队列
*/
abstract public function isEnqueue($entity);
/**
* @desc 设置一个实体已经进入队列标志
*
* @param $entity 实体
* @return bool 是否插入成功
*/
abstract public function setEnqueue($entity);
/**
* @desc 判断一个唯一的抓取到的信息是否已经保存过
*
* @param $entity mixed 用于判断的信息
* @return bool 是否已经保存过
*/
abstract public function isSaved($entity);
/**
* @desc 设置一个对象已经保存
*
* @param $entity mixed 是否保存的一句
* @return bool 是否设置成功
*/
abstract public function setSaved($entity);
/**
* @desc 保存抓取到的内容
* 这里保存之前会判断是否保存过,如果保存过就不保存了
* 如果设置了更新,则会更新
*
* @param $uniqInfo mixed 抓取到的要保存的信息
* @param $update bool 保存过的话是否更新
* @return bool
*/
abstract public function save($uniqInfo, $update);
/**
* @desc 处理实体的内容
* 这里会调用enqueue
*
* @param $item 实体数组,@see dequeue 的返回值
* @return
*/
abstract public function handle($item);
/**
* @desc 随机停顿时间
*
* @param $startMs 随机区间开始微妙
* @param $endMs 随机区间结束微妙
* @return bool
*/
public function randomSleep($startMS, $endMS)
{
$rand = rand($startMS, $endMS);
usleep($rand);
return true;
}
/**
* @desc 修改默认停顿时间开始区间值
*
* @param $ms int 微妙
* @return obj $this
*/
public function setStartMS($ms)
{
$this->startMS = $ms;
return $this;
}
/**
* @desc 修改默认停顿时间结束区间值
*
* @param $ms int 微妙
* @return obj $this
*/
public function setEndMS($ms)
{
$this->endMS = $ms;
return $this;
}
/**
* @desc 设置队列最长长度,溢出后丢弃
*
* @param $len int 队列最大长度
*/
public function setMaxQueueLen($len)
{
$this->maxQueueLen = $len;
return $this;
}
/**
* @desc 设置爬取最深层级
* 入队列的时候判断层级,如果超过层级不做入队操作
*
* @param $maxDeep 爬取最深层级
* @return obj
*/
public function setMaxDeep($maxDeep)
{
$this->maxDeep = $maxDeep;
return $this;
}
public function run()
{
while ($this->queueLen()) {
$item = $this->dequeue();
if (empty($item))
continue;
$item = json_decode($item, true);
if (empty($item) || empty($item["deep"]) || empty($item["entity"]))
continue;
$this->handle($item);
$this->randomSleep($this->startMS, $this->endMS);
}
}
/**
* @desc 通过curl获取链接内容
*
* @param $url string 链接地址
* @param $curlOptions array curl配置信息
* @return mixed
*/
public function getContent($url, $curlOptions = [])
{
$ch = curl_init();
curl_setopt_array($ch, $curlOptions);
curl_setopt($ch, CURLOPT_URL, $url);
if (!isset($curlOptions[CURLOPT_HEADER]))
curl_setopt($ch, CURLOPT_HEADER, 0);
if (!isset($curlOptions[CURLOPT_RETURNTRANSFER]))
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if (!isset($curlOptions[CURLOPT_USERAGENT]))
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Macintosh; Intel Mac");
$content = curl_exec($ch);
if ($errorNo = curl_errno($ch)) {
$errorInfo = curl_error($ch);
echo "curl error : errorNo[{$errorNo}], errorInfo[{$errorInfo}]\n";
curl_close($ch);
return false;
}
$httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);
curl_close($ch);
if (200 != $httpCode) {
echo "http code error : {$httpCode}, $url, [$content]\n";
return false;
}
return $content;
}
}
abstract class RedisDbSpider extends SpiderBase
{
protected $queueName = "";
protected $isQueueName = "";
protected $isSaved = "";
public function __construct($objRedis = null, $objDb = null, $configs = [])
{
$this->objRedis = $objRedis;
$this->objDb = $objDb;
foreach ($configs as $name => $value) {
if (isset($this->$name)) {
$this->$name = $value;
}
}
}
public function enqueue($deep, $entities)
{
if (!$this->canEnqueue(["deep"=>$deep]))
return true;
if (is_string($entities)) {
if ($this->isEnqueue($entities))
return true;
$item = [
"deep" => $deep,
"entity" => $entities
];
$this->objRedis->lpush($this->queueName, json_encode($item));
$this->setEnqueue($entities);
} else if(is_array($entities)) {
foreach ($entities as $key => $entity) {
if ($this->isEnqueue($entity))
continue;
$item = [
"deep" => $deep,
"entity" => $entity
];
$this->objRedis->lpush($this->queueName, json_encode($item));
$this->setEnqueue($entity);
}
}
return true;
}
public function dequeue()
{
$item = $this->objRedis->lpop($this->queueName);
return $item;
}
public function isEnqueue($entity)
{
$ret = $this->objRedis->hexists($this->isQueueName, $entity);
return $ret ? true : false;
}
public function canEnqueue($params)
{
$deep = $params["deep"];
if ($deep > $this->maxDeep) {
return false;
}
$len = $this->objRedis->llen($this->queueName);
return $len < $this->maxQueueLen ? true : false;
}
public function setEnqueue($entity)
{
$ret = $this->objRedis->hset($this->isQueueName, $entity, 1);
return $ret ? true : false;
}
public function queueLen()
{
$ret = $this->objRedis->llen($this->queueName);
return intval($ret);
}
public function isSaved($entity)
{
$ret = $this->objRedis->hexists($this->isSaved, $entity);
return $ret ? true : false;
}
public function setSaved($entity)
{
$ret = $this->objRedis->hset($this->isSaved, $entity, 1);
return $ret ? true : false;
}
}
class Test extends RedisDbSpider
{
/**
* @desc 构造函数,设置redis、db实例,以及队列相关参数
*/
public function __construct($redis, $db)
{
$configs = [
"queueName" => "spider_queue:zhihu",
"isQueueName" => "spider_is_queue:zhihu",
"isSaved" => "spider_is_saved:zhihu",
"maxQueueLen" => 10000
];
parent::__construct($redis, $db, $configs);
}
public function handle($item)
{
$deep = $item["deep"];
$entity = $item["entity"];
echo "开始抓取用户[{$entity}]\n";
echo "数据内容入库\n";
echo "下一层深度如队列\n";
echo "抓取用户[{$entity}]结束\n";
}
public function save($addUsers, $update)
{
echo "保存成功\n";
}
}