簡易爬蟲


簡易爬蟲設計

引言

說這是一個爬蟲有點說大話了,但這個名字又恰到好處,所以在前面加了”簡易“兩個字,表明
這是一個閹割的爬蟲,簡單的使用或者玩玩兒還是可以的。
公司最近有新的業務要去抓取競品的數據,看了之前的同學寫的抓取系統,存在一定的問題,
規則性太強了,無論是擴展性還是通用性發面都稍微弱了點,之前的系統必須要你搞個列表,
然后從這個列表去爬取,沒有深度的概念,這對爬蟲來說簡直是硬傷。因此,我決定搞一個
稍微通用點的爬蟲,加入深度的概念,擴展性通用型方面也提升下。

設計

我們這里約定下,要處理的內容(可能是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";
	}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM