首頁  >  文章  >  後端開發  >  php簡易爬蟲

php簡易爬蟲

大家讲道理
大家讲道理原創
2018-05-14 14:45:458818瀏覽

簡易爬蟲設計

引言

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

設計

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

以上是php簡易爬蟲的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
上一篇:PHP錯誤屏蔽法下一篇:PHP錯誤屏蔽法