因为最近项目一直用到定向抓取各大网站的爬虫技术,加上自己对爬虫还算是比较熟悉,所以博客的第一篇就献给网络爬虫吧,内容主要是使用php来实现获取给定百科词条的所有相关词条的标题和简介。

由于只是展示爬虫的基本工作原理和实现过程,所以程序会设计得比较简单,只能称为一个简易爬虫。

爬虫简介

网络爬虫,也叫网页蜘蛛,网络机器人,是指能按照一定规则自动抓取互联网上的信息资源的程序。如果把互联网上的网页看作是一个一个的节点,把网页之间的超链接看作是节点的边,整个互联网就形成了一个Web有向图。

宽度优先遍历 深度优先遍历

爬虫的抓取就是对Web有向图的遍历,按照遍历顺序的不同,可以分为宽度优先(Breadth First)和深度优先(Depth First)两种遍历方式。

网络爬虫最主要的作用就是获取互联网上的数据和信息,广泛应用于搜索引擎、数据挖掘、信息采集、舆情监测等领域,还是广大站长和SEOer获取网站内容和数据的利器。

工作原理

网络爬虫架构

如图所示,首先选取一部分url作为种子url加入待抓取队列中,爬虫会依次读取待抓取队列中的url,并交给下载器将网页下载到本地,html解析器从网页中抽取所有链接,过滤出我们需要的链接。

为了避免爬虫对网页的重复抓取,需要维护一个已抓取集合,用来判断一个url是否已经被抓取过,过滤出的url经过去重后,加入到待抓取队列中,已抓取过的url加入到已抓取集合中,调度程序继续从待抓取队列中取出url进行抓取,如此循环,直到待抓取队列为空。

依赖组件

根据爬虫的架构,将其拆分为调度程序、URL管理器、下载器、html解析器和输出器。

  • 调度程序
    负责对爬虫各个组件进行调度。
  • URL管理器
    主要管理未抓取队列和已抓取集合,采用php数组来实现。
  • 下载器
    采用packgist上的curlphp-curl-class作为http客户端来实现。
  • html解析器
    采用didom库来实现对html的解析。
  • 输出器
    负责将词条的标题和简介保存到CSV文件。

使用composer进行依赖包管理,php环境为5.4以上。

安装didomphp-curl-class:

composer require imangazaliev/didom php-curl-class/php-curl-class 

调度程序

Spider.php

<?php
 
namespace Vanry\Spider;
 
class Spider
{
    private $seed;
    private $limit = 0;
 
    private $urlManager;
    private $downloader;
    private $parser;
    private $writer;
 
    public function __construct($seed)
    {   
        $this->seed = $seed;
 
        $this->urlManager = new UrlManager;
        $this->downloader = new Downloader;
        $this->parser = new Parser;
        $this->writer = new Writer;
    }
 
    /**
     * 设置最大抓取量
     *
     * @param  string  $limit
     * @return void
     */
    public function setLimit($limit)
    {
        $this->limit = $limit;
    }
 
    public function crawl()
    {
        //将种子url加入待抓取队列
        $this->urlManager->addUrl($this->seed);
 
        //取出待抓取的url开始抓取
        $count = 0;
        while ($url = $this->urlManager->next()) {
            if ($this->limit != 0 && $count >= $this->limit) {
                echo "抓取数量已达到设定值 \n";
                break;
            }
 
            echo "正在抓取: $url \n";
            $html = $this->downloader->download($url);
 
            //抓取过的url加入已抓取集合
            $this->urlManager->addVisted($url);
            $count++;
 
            //失败则跳过
            if (! $html) {
                continue;
            }
 
            //解析出新的链接和标题、简介
            $data = $this->parser->parse($html);
            extract($data);
 
            $this->urlManager->addUrls($urls);
            $this->writer->addItem($item);
            
            //避免触发反爬虫机制,延时1~3秒
            sleep(mt_rand(1, 3));
        }
 
        //保存到csv文件
        $this->writer->write();
    }
}

URL管理器

UrlManager.php

<?php
 
namespace Vanry\Spider;
 
class UrlManager
{
    private $queue = [];
    private $visited = [];
    
    public function addUrl($url)
    {
        //每个url只抓取一遍
        if (! in_array($url, $this->queue) && ! in_array($url, $this->visited)) {
            array_push($this->queue, $url);
        }
    }
 
    public function addUrls($urls)
    {
        foreach ($urls as $url) {
            $this->addUrl($url);
        }
    }
 
    public function next()
    {
        return array_shift($this->queue);
    }
 
    public function addVisted($url)
    {
        array_push($this->visited, $url);
    }
}

下载器

Downloader.php

<?php
 
namespace Vanry\Spider;
 
use Curl\Curl;
 
class Downloader
{
    const USER_AGENT = 'Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0';
 
    private $client;
 
    /**
     * 可传入自定义设置后的Curl\Curl实例
     *
     * @param  Curl\Curl  $curl
     * @return void
     */
    public function __construct(Curl $curl = null)
    {
        if (is_null($curl)) {
            $curl = new Curl;
            $curl->setUserAgent(self::USER_AGENT);
 
            //抓取跳转后的页面
            $curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
        }
 
        $this->client = $curl;
    }
    
    public function download($url)
    {
        //GET方式发送请求
        $response = $this->client->get($url);
 
        //过滤不可访问的链接
        if ($this->client->httpStatusCode >= 400) {
            return false;
        }
 
        return $response;
    }
}

解析器

Parser.php

<?php
 
namespace Vanry\Spider;
 
use DiDom\Document;
 
class Parser
{
    private $dom;
    
    public function __construct()
    {
        $this->dom = new Document;
    }
 
    public function parse($html)
    {
        $this->dom->load($html);
 
        $urls = $this->extractLinks();
        $item = $this->getItem();
 
        return compact('urls', 'item');
    }
 
    public function extractLinks()
    {
        $urls = [];
        $nodes = $this->dom->find('div.main-content a');
 
        foreach ($nodes as $node) {
            //只保留包含viewurl, 忽略锁定词条
            if (strpos($node->href, 'view/') !== false && $node->title != '锁定') {
                $urls[] = 'http://baike.baidu.com'.$node->href;
            }
        }
 
        return array_unique($urls);
    }
 
    public function getItem()
    {
        $title = $this->getTitle();
        $intro = $this->getIntro();
 
        return compact('title', 'intro');
    }
 
    private function getTitle()
    {
        return $this->dom->find('dd.lemmaWgt-lemmaTitle-title h1')[0]->text();
    }
 
    private function getIntro()
    {
        $intro = $this->dom->find('div.lemma-summary')[0]->text();
 
        return trim($intro);
    }
}

输出器

Writer.php

<?php
 
namespace Vanry\Spider;
 
class Writer
{
    private $items = [];
    private $filename;
 
    public function __construct($filename = 'result.csv')
    {
        $this->filename = $filename;
    }
    
    public function addItem($item)
    {
        $this->items[] = $item;
    }
 
    public function write()
    {   
        $fp = fopen($this->filename, 'w');
        fputcsv($fp, ['标题', '摘要']);
 
        foreach ($this->items as $item) {
            echo "正在保存: {$item['title']} \n";
            fputcsv($fp, $item);
        }
 
        fclose($fp);
    }
}

演示

example.php

<?php
 
require __DIR__.'/../vendor/autoload.php';
 
use Vanry\Spider\Spider;
 
$url = 'http://baike.baidu.com/view/284853.htm';
 
$spider = new Spider($url);
 
//设置最大抓取量
$spider->setLimit(10);
 
//开始抓取..
$spider->crawl();

爬虫抓取状态图

爬虫抓取结果图

github地址: https://github.com/vanry/baike-spider

packgist包地址: https://packagist.org/packages/vanry/baike-spider

composer require vanry/baike-spider dev-master