离框开发


1、爬山虎业务开发分两种:「离框开发」和「基于爬山虎应用框架开发」。
2、所谓的离框开发指的是脱离爬山虎应用框架进行的自由定制开发。
3、注意:由于主机禁止出现敏感IP,请自行将 localhost 替换为 目标IP

将以下脚本保存到文件并命名为start.php,然后执行:php start.php start

<?php 
/**
 * @script   start.php
 * @brief    这是脱离爬山虎应用框架的自定义启动脚本:
 *
 * 1. 本示例脚本是脱离爬山虎应用框架的自定义启动脚本;
 * 2. 本示例脚本用于模拟抓取未来7天内北京的天气预报;
 * 3. 如果希望使用爬山虎应用框架开发,请参考开发手册:
 *
 *    >> http://www.phpcreeper.com/docs/
 *    >> http://www.blogdaren.com/docs/
 *
 * @author   blogdaren<blogdaren@163.com>
 * @link     http://www.phpcreeper.com
 * @create   2022-09-08
 */


//自动路由autoloader
$msg = PHP_EOL."找不到自动加载器autoloader, 请尝试运行: composer require blogdaren/phpcreeper".PHP_EOL.PHP_EOL;
false === ($files = @scandir(__DIR__, 1)) && exit($msg);
foreach($files as $k => $file){
    if('vendor' === $file && is_dir(__DIR__ . DIRECTORY_SEPARATOR . $file)){
        require_once dirname(__FILE__, 1) . "/vendor/autoload.php";break;
    }elseif(false !== strpos(__DIR__, 'vendor/blogdaren/phpcreeper/Examples')){
        require_once dirname(__FILE__, 5) . "/vendor/autoload.php";break;
    }elseif(false !== strpos(__DIR__, 'Examples')){
        require_once dirname(__FILE__, 2) . "/vendor/autoload.php";break;
    }else{
        (count($files) == ($k+1)) && exit($msg);
    }
}


use PHPCreeper\PHPCreeper;
use PHPCreeper\Producer;
use PHPCreeper\Downloader;
use PHPCreeper\Parser;
use PHPCreeper\Server;
use PHPCreeper\Tool;
use PHPCreeper\Timer;
use PHPCreeper\Crontab;
use Logger\Logger;


/**
 * enable the single worker mode so that we can run without redis, however, you should note 
 * it will be limited to run only all the downloader workers in this case【version >= 1.3.2】
 * and the default is Multi-Worker run mode.
 * 多worker运作模式开关,默认是多worker运作模式,支持两种运作模式【version >= 1.3.2】:
 *
 * 1、单worker运作模式:限定只能编写若干特定的downloader实例,即可完成所有的爬虫需求,
 *    好处是开箱即用,不依赖redis服务,使用PHP内置队列,缺点是只能对付简单的爬虫需求;
 * 2、多worker运作模式:支持自由编写任意多个业务worker实例,这是爬山虎默认的运作模式;
 */
//PHPCreeper::enableMultiWorkerMode(false);


/**
 * switch runtime language between `zh` and `en`, default is `zh`【version >= 1.3.7】
 * 多语言运行时环境开关:暂支持中文和英文,默认是中文【version >= 1.3.7】
 */
//PHPCreeper::setLang('en');


/**
 * redirect all stdandard out to file when run as daemonize【version >= 1.7.0】
 * 如果以守护进程方式运行,则所有向终端的输出(echo var_dump等)将会被重定向到指定的文件中;
 * 如果以守护进程方式运行并且不设置,则所有终端输出将被重定向到/dev/null,即丢弃所有输出;
 */
//PHPCreeper::setStdoutFile("/tmp/stdout.log");


/**
 * set the corresponding app log according to the component, 
 * and can also mask the log of the corresponding log level.
 * 根据组件保存相应的应用日志,也可以屏蔽掉相应日志级别的日志。
 */
//PHPCreeper::setLogFile('/tmp/runtime.log');
//PHPCreeper::setLogFile('/tmp/runtime.log', 'producer');
//PHPCreeper::setLogFile('/tmp/runtime.log', 'downloader');
//PHPCreeper::setLogFile('/tmp/runtime.log', 'parser');
//PHPCreeper::disableLogLevel(['crazy','debug','info']);
//PHPCreeper::disableLogLevel(['crazy','debug','info'], 'producer');
//PHPCreeper::disableLogLevel(['crazy','debug','info', 'warn'], 'downloader');


/**
 * set master pid file manually as needed【version >= 1.3.8】
 * 设置主进程PID文件【version >= 1.3.8】
 */
//PHPCreeper::setMasterPidFile('master.pid');


/**
 * note that `predis` will be the default redis client since【version >= 1.4.2】
 * but you could still switch it to be `redis` if you prefer to use ext-redis
 * 设置默认的redis客户端,默认为predis,也可切换为基于ext-redis的redis【version >= 1.4.2】
 */
//PHPCreeper::setDefaultRedisClient('redis');


/**
 * set default timezone, default is `Asia/Shanghai`【version >= 1.5.4】
 * 设置默认时区,默认为 Asia/Shanghai
 */
//PHPCreeper::setDefaultTimezone('Asia/Shanghai');


/**
 * set default max file size to download, default is `0`【version >= 1.8.3】
 * 设置默认最大下载文件大小, 默认为0, 如果大于0, 将会触发额外的HTTP.METHOD.HEAD请求.
 * 注意:大文件机制尚待优化,当下载大文件遇到不可预期的问题时,尝试设置本参数为20MB.
 */
//PHPCreeper::setDefaultMaxFileSizeToDownload(20 * 1048576);


/**
 * set default headless browser, default is `chrome`【version >= 1.8.7】
 * 设置默认的无头浏览器,默认为 chrome,后续可能陆续支持 puppeteer 和 phantomjs.
 */
//PHPCreeper::setDefaultHeadlessBrowser('chrome');


/**
 * if the child process don't exit within timeout, then force to kill it【version >= 2.0.0】
 * 如果在timeout时间内子进程还没有退出,多用于慢业务场景,则强制将其杀死,默认2秒.
 */
//PHPCreeper::setChildProcessStopTimeout(5);


/**
 * Global-Redis-Config: just leave it alone when run as Single-Worker mode
 * 仅单worker运作模式下不依赖redis,所以此时redis的配置可以忽略不管.
 * 特别注意:自v1.6.4起,redis锁机制已升级并默认使用官方推荐的更安全的分布式红锁,
 * 只有当所有的redis实例都显式的配置[use_red_lock === false]才会退化为旧版的锁机制.
 */
$config['redis'] = [
    [
        'host'      =>  'localhost',
        'port'      =>  6379,
        'database'  =>  '0',
        'auth'      =>  false,
        'pass'      =>  'guest',
        'prefix'    =>  'PHPCreeper', 
        'connection_timeout' => 5,
        'read_write_timeout' => 0,
        //'use_red_lock'     => true,   //默认使用更安全的分布式红锁 
    ],
    /*[
        'host'      =>  'localhost',
        'port'      =>  6380,
        'database'  =>  '0',
        'auth'      =>  false,
        'pass'      =>  'guest',
        'prefix'    =>  'PHPCreeper', 
        'connection_timeout' => 5,
        'read_write_timeout' => 0,
        //'use_red_lock'     => true,   //默认使用更安全的分布式红锁 
    ],*/
];


/**
 * Global-Task-Config: the context member configured here is a global context,
 * we can also set a private context for each task, finally the global context 
 * and task private context will adopt the strategy of merging and covering.
 * free to customize various context settings, including user-defined.
 *
 * 注意: 此处配置的context是全局context上下文,我们也可以为每条任务设置私有context上下文,
 * 其上下文成员完全相同,全局context与任务私有context最终采用合并覆盖的策略,具体参考手册。
 * http://www.phpcreeper.com/docs/DevelopmentGuide/ApplicationConfig.html
 * context上下文成员主要是针对任务设置的,但同时拥有很大灵活性,可以间接影响依赖性服务,
 * 比如可以通过设置context上下文成员来影响HTTP请求时的各种上下文参数 (可选项,默认为空)
 * HTTP引擎默认采用Guzzle客户端,兼容支持Guzzle所有的请求参数选项,具体参考Guzzle手册。
 * 特别注意:个别上下文成员的用法是和Guzzle官方不一致的,一方面主要就是屏蔽其技术性概念,
 * 另一方面面向开发者来说,关注点主要是能进行简单的配置即可,所以不一致的会注释特别说明。
 */
$config['task'] = array( 
    //任务爬取间隔,单位秒,最小支持0.001秒 (可选项,默认1秒)
    //'crawl_interval'  => 1,

    //任务队列最大task数量, 0代表无限制 (可选项,默认0)
    //'max_number'      => 1000,

    //特指每个下载器进程可以建立到解析器的最大连接数 (可选项,默认1,最小值为1,最大值为1000)
    //'max_connections' => 1,

    //当前Socket连接累计最大请求数,0代表无限制 (可选项,默认0)
    //如果当前Socket连接的累计请求数超过最大请求数时,
    //parser端会主动关闭连接,同时客户端会自动尝试重连
    //'max_request'     => 1000,

    //限定爬取站点域,留空表示不受限
    'limit_domains' => [],

    //根据预期任务总量和误判率引擎会自动计算布隆过滤器最优的bitmap长度以及hash函数的个数
    //'bloomfilter' => [
        //'expected_insertions' => 10000,  //预期任务总量
        //'expected_falseratio' => 0.01,   //预期误判率
    //],

    //全局任务context上下文 [注意每条任务都有各自的私有context上下文,最终采用合并覆盖策略]
    'context' => [
        //要不要缓存下载文件 [默认false]
        'cache_enabled'   => true,
        'cache_directory' => sys_get_temp_dir() . '/DownloadCache4PHPCreeper/',

        //在特定的生命周期内是否允许重复抓取同一个URL资源 [默认false]
        'allow_url_repeat'   => true,

        //要不要跟踪完整的HTTP请求参数,开启后终端会显示完整的请求参数 [默认false]
        'track_request_args' => true,

        //要不要跟踪完整的TASK数据包,开启后终端会显示完整的任务数据包 [默认false]
        'track_task_package' => true,

        //在v1.6.0之前,如果rulename留空,默认会使用 md5($task_url)作为rulename
        //自v1.6.0开始,如果rulename留空,默认会使用 md5($task_id) 作为rulename
        //所以这个配置参数是仅仅为了保持向下兼容,但是不推荐使用,因为有潜在隐患
        //换句话如果使用的是v1.6.0之前旧版本,那么才有可能需要激活本参数 [默认false]
        'force_use_md5url_if_rulename_empty' => false,

        //强制使用多任务创建API的旧版本参数风格,保持向下兼容,不再推荐使用 [默认false]
        'force_use_old_style_multitask_args' => false,

        //cookies成员的配置格式和guzzle官方不大一样,屏蔽了cookieJar,取值[false|array]
        'cookies' => [
            //'domain' => 'domain.com',
            //'k1' => 'v1',
            //'k2' => 'v2',
        ],

        //除了内置参数之外,还可以自由配置自定义参数,在上下游业务链应用场景中十分有用
        'user_define_key1' => 'user_define_value1',
        'user_define_key2' => 'user_define_value2',

        //无头浏览器,如果是动态页面考虑启用,否则应当禁用 [默认使用chrome且为禁用状态]
        //更多其他无头参数详见手册[常见问题]章节
        'headless_browser' => ['headless' => false],

        //更多其他上下文参数详见官方手册
    ],
); 


/**
 * all components support distributed or separated deployment
 * 所有组件支持分布式或分离式部署
 */
function startAppProducer()
{
    global $config;
    $producer = new Producer($config);
    $producer->setName('AppProducer')->setCount(1);

    //模拟抓取未来7天内北京的天气预报
    $producer->onProducerStart = function($producer){
        //任务私有context,其上下文成员与全局context完全相同,最终会采用合并覆盖策略
        $private_task_context = [];

        //在v1.6.0之前,爬山虎主要使用OOP风格的API来创建任务:
        //$producer->newTaskMan()->setXXX()->setXXX()->createTask()
        //$producer->newTaskMan()->setXXX()->setXXX()->createTask($task)
        //$producer->newTaskMan()->setXXX()->setXXX()->createMultiTask()
        //$producer->newTaskMan()->setXXX()->setXXX()->createMultiTask($task)

        //自v1.6.0开始,爬山虎提供了更加短小便捷的API来创建任务, 而且参数类型更加丰富:
        //注意:仅仅只是扩展,原有的API依然可以正常使用,提倡扩展就是为了保持向下兼容。
        //1. 单任务API:$task参数类型可支持:[字符串 | 一维数组]
        //2. 单任务API:$producer->createTask($task);
        //3. 多任务API:$task参数类型可支持:[字符串 | 一维数组 | 二维数组]
        //4. 多任务API:$producer->createMultiTask($task);

        //使用字符串:不推荐使用,配置受限,需要自行处理抓取结果
        //$task = "http://www.weather.com.cn/weather/101010100.shtml";
        //$producer->createTask($task);
        //$producer->createMultiTask($task);

        //使用一维数组:推荐使用,配置丰富,引擎内置处理抓取结果
        $task = array(
            'active' => true,       //是否激活当前任务,只有配置为false才会冻结任务,默认true
            'url' => "http://www.weather.com.cn/weather/101010100.shtml",
            'rule' => array(        //如果该字段留空默认将返回原始下载数据
                'time' => ['div#7d ul.t.clearfix h1',      'text', [], 'function($field_name, $data){
                    return "具体日子: " . $data;
                }'],                //关于回调字符串的用法务必详看官方手册
                'wea'  => ['div#7d ul.t.clearfix p.wea',   'text'],
                'tem'  => ['div#7d ul.t.clearfix p.tem',   'text'],
            ), 
            'rule_name' =>  '',     //如果留空将使用md5($task_id)作为规则名
            'refer'     =>  '',
            'type'      =>  'text', //已丧失原本的概念设定,可以自由设定类型
            'method'    =>  'get',
            'parser'    =>  '',     //如果留空将路由至一台随机的目标parser服务器[ip:port]
            'context'   =>  $private_task_context,
        );
        $producer->createTask($task);
        $producer->createMultiTask($task);

        //使用二维数组: 推荐使用,配置丰富,因为是多任务,所以只能调用createMultiTask()接口
        $task = array(
            array(
                "url" => "http://www.weather.com.cn/weather/101010100.shtml",
                'rule' => array(
                    'time' => ['div#7d ul.t.clearfix h1',      'text'],
                    'wea'  => ['div#7d ul.t.clearfix p.wea',   'text'],
                    'tem'  => ['div#7d ul.t.clearfix p.tem',   'text'],
                ), 
                'rule_name' => 'r1', //如果留空将使用md5($task_id)作为规则名
                "context" => $private_task_context,
            ),
            array(
                "url" => "http://www.weather.com.cn/weather/201010100.shtml",
                'rule' => array(
                    'time' => ['div#7d ul.t.clearfix h1',      'text'],
                    'wea'  => ['div#7d ul.t.clearfix p.wea',   'text'],
                    'tem'  => ['div#7d ul.t.clearfix p.tem',   'text'],
                ), 
                'rule_name' => 'r2', //如果留空将使用md5($task_id)作为规则名
                "context" => $private_task_context,
            ),
        );
        $producer->createMultiTask($task);

        //使用无头浏览器爬取动态页面
        $private_task_context['headless_browser']['headless'] = true;
        $dynamic_task = array(
            'url'  => 'https://www.toutiao.com',
            'rule' => array(
                'title' => ['div.show-monitor ol li a', 'aria-label'],
                'link'  => ['div.show-monitor ol li a', 'href'],
            ), 
            'context' => $private_task_context,
        );
        $producer->createTask($dynamic_task);
    };
}


/**
 * all components support distributed or separated deployment
 * 所有组件支持分布式或分离式部署
 */
function startAppDownloader()
{
    global $config;

    $downloader = new Downloader($config);
    //$downloader->setTaskCrawlInterval(5);
    $downloader->setName('AppDownloader')->setCount(1)->setClientSocketAddress([
        'ws://localhost:8888',
    ]);

    $downloader->onDownloaderStart = function($downloader){
    };

    $downloader->onDownloaderConnectToParser = function($connection){
        //$connection->bufferFull = true;
    };

    //回调【onBeforeDownload】的新增别名是【onDownloadBefore】
    $downloader->onDownloadBefore = function($downloader, $task){
        //disable http ssl verify in any of the following two ways 
        //$downloader->httpClient->disableSSL();
        //$downloader->httpClient->setOptions(['verify' => false]);
    }; 

    //回调【onStartDownload】的新增别名是【onDownloadStart】
    $downloader->onDownloadStart = function($downloader, $task){
    };

    //回调【onAfterDownload】的新增别名是【onDownloadAfter】
    $downloader->onDownloadAfter = function($downloader, $data, $task){
        //Tool::debug($content, $json = true, $append = true, $filename = "debug", $base_dir = "")
        //Tool::debug($task);
    };

    //回调【onFailDownload】的新增别名是【onDownloadFail】
    $downloader->onDownloadFail = function($downloader, $error, $task){
        //pprint($error, $task);
    };

    //回调【onTaskEmpty】的新增别名是【onDownloadTaskEmpty】
    $downloader->onDownloadTaskEmpty= function($downloader){
        //$downloader->removeTimer();
    };

    //使用无头浏览器回调或者直接使用无头浏览器相关API
    $downloader->onHeadlessBrowserOpenPage = function($downloader, $browser, $page, $url){
        //注意:灵活设计特定类型的返回值有助于对付各种复杂的应用场景
        //1. 返回false, 会触发中断后续的业务逻辑;
        //2. 返回string,会触发中断后续的业务逻辑,一般多用于返回页面的HTML;
        //3. 返回array, 会继续执行后续的业务逻辑,一般多用于返回无头浏览器选项参数;
        //4. 返回其他,  会继续执行后续的业务逻辑,相当于是什么也没有发生;

        //注意:一般无需调用如下几行代码,因为爬山虎内部默认会自动调用无头API做同样的工作.
        //$page->navigate($url)->waitForNavigation('firstMeaningfulPaint');
        //$html = $page->getHtml();
        //return $html;
    };
}


/**
 * all components support distributed or separated deployment
 * 所有组件支持分布式或分离式部署
 */
function startAppParser()
{
    $parser = new Parser();
    $parser->setName('AppParser')->setCount(1);
    $parser->setServerSocketAddress('websocket://0.0.0.0:8888');
    $parser->onParserExtractField = function($parser, $download_data, $fields){
        pprint($fields);
    };
}

/**
 * General Server independ on [Producer|Downloader|Parser]
 * 通用型服务器组件,完全独立于[Producer|Downloader|Parser]
 */
function startAppServer()
{
    $server = new Server();
    $server->onServerStart = function(){
        /*
         * just show how to use Linux-Style Crontab: 
         *
         * (1) the only difference is that support the second-level;
         * (2) the minimum time granularity is minutes if the second bit is omitted;
         *
         * the formatter looks like as below:
         * 
         *  0   1   2   3   4   5
         *  |   |   |   |   |   |
         *  |   |   |   |   |   +------ day of week (0 - 6) (Sunday=0)
         *  |   |   |   |   +------ month (1 - 12)
         *  |   |   |   +-------- day of month (1 - 31)
         *  |   |   +---------- hour (0 - 23)
         *  |   +------------ min (0 - 59)
         *  +-------------- sec (0-59)[可省略,如果没有0位,则最小时间粒度是分钟]
         *
         * 防止重复造轮子且图省事完全照搬了walkor大大的workerman-crontab而来, 
         * 很小巧且为了方便所以将此库脱离了composer库并揉进了PHPCreeper内核,
         * 高仿Linux风格的Crontab,语法层面除了支持秒级以外,其余用法基本一致,
         * 所以平时crontab怎么用现在就怎么用,具体用法请参照workerman官方手册:
         * https://www.workerman.net/doc/webman/components/crontab.html
         */

        //每隔1秒执行一次任务
        new Crontab('*/1 * * * * *', function(){
            pprint("模拟每隔1秒打印一下当前时间:" . Tool::getHumanTime());
        });

        //每隔2分钟执行一次任务
        new Crontab('*/2 * * * *', function(){
            pprint("模拟每隔2分钟打印一下当前时间:" . Tool::getHumanTime());
        });
    };
}


//启动生产器组件
startAppProducer();

//启动下载器组件
startAppDownloader();

//启动解析器组件
startAppParser();

//启动通用型服务器组件,可按需自由定制一些服务,
//完全独立于 [Producer|Downloader|Parser] 组件.
startAppServer();

//启动爬山虎引擎
PHPCreeper::start();
Free Web Hosting