PHPCreeper,中文名《爬山虎》,是一个专注于高效敏捷开发的爬虫引擎。它不仅简化了爬取工作的复杂性,还解决了传统PHP爬虫框架在性能和扩展性上的瓶颈问题。通过充分利用多进程、分布式和分离式部署环境,PHPCreeper能够支持无头浏览器,执行JavaScript代码以爬取动态页面,极大地提升了爬取效率和灵活性。
PHPCreeper的核心技术架构基于workerman,这是一个高性能的PHP socket服务器框架。PHPCreeper继承了Workerman的所有特性,并在此基础上增加了对无头浏览器的支持,使得爬取动态页面成为可能。此外,它还支持类似Linux Crontab的定时任务、分布式部署以及灵活的回调和第三方中间件定制。
传统的PHP爬虫框架普遍有两大不足:一个是大多为单进程工作模型;另一个是大多为单机或同步工作模式,换句话即看不到 socket 的身影, 因此无法做到分布式及分离式部署,因此无法最大化发挥爬取性能,而爬山虎是基于 workerman 开发的, 能够轻松支持如下架构:异步IO + 多进程 + 分布/分离式部署 + 事件驱动
模型, 从而保证爬山虎发挥最大化性能【不过作者认为:性能其实在爬虫领域内没有突出意义,而且有限的政策环境也使性能蒙上了物理阴影】;此外爬山虎采用了微内核 + 插件 + 分离式部署
的设计理念,所以具备强扩展性。
该插件依赖与webman框架,如果已安装跳过即可。
安装文档:
https://www.workerman.net/doc/webman/install.html
爬山虎插件安装
composer require blogdaren/webman-phpcreeper
插件地址:https://www.workerman.net/plugin/39
案例场景:
模拟抓取未来3天内北京的天气预报
创建爬虫目录
mkdir app/spider
这里的app
即webman的应用目录
句柄类文件app/spider/TinywanProducer.php
<?php
/**
* @desc TinywanProducer.php 描述信息
* @author Tinywan(ShaoBo Wan)
* @date 2024/10/5 10:02
*/
declare(strict_types=1);
namespace app\spider;
use PHPCreeper\Kernel\PHPCreeper;
use Webman\PHPCreeper\Producer;
class TinywanProducer extends Producer
{
/**
* @brief 生产任务
*/
public function makeTask()
{
//任务私有context,其上下文成员与全局context完全相同,最终会采用合并覆盖策略
$private_task_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,
//设置http请求头:默认引擎会自动伪装成常见的各种随机User-Agent
'headers' => [
//'User-Agent' => 'Mozilla/5.0 Chrome/124.0.0.0 Safari/537.36',
//'Accept' => 'text/html,*/*',
],
//cookies成员的配置格式和guzzle官方不大一样,屏蔽了cookieJar,取值[false|array]
'cookies' => [
//'domain' => 'domain.com',
//'k1' => 'v1',
//'k2' => 'v2',
],
//无头浏览器,如果是动态页面考虑启用,否则应当禁用 [默认使用chrome且为禁用状态]
'headless_browser' => ['headless' => false, /*更多其他无头参数*/],
//除了内置参数之外,还可以自由配置自定义参数,在上下游业务链应用场景中十分有用
'user_define_arg1' => 'user_define_value1',
'user_define_arg2' => 'user_define_value2',
//更多其他上下文参数详见官方手册
];
$task = [
'active' => true, //是否激活当前任务,只有配置为false才会冻结任务,默认true
'url' => 'http://www.weather.com.cn/weather/101010100.shtml',
"rule" => array( //如果该字段留空默认将返回原始下载数据
'日子' => ['div#7d ul.t.clearfix h1', 'text', [], 'function($field_name, $data){
return date("Y-m-d") . " | " . $data;
}'], //关于回调字符串的用法务必详看官方手册
'天气' => ['div#7d ul.t.clearfix p.wea', 'text'],
'温度' => ['div#7d ul.t.clearfix p.tem', 'text'],
),
'rule_name' => '', //如果留空将使用md5($task_id)作为规则名
'refer' => '',
'type' => 'text', //已丧失原本的概念设定,可以自由设定类型
'method' => 'get',
'context' => $private_task_context,
];
$this->createTask($task);
}
/**
* @brief 使用无头浏览器爬JavaScript渲染的取动态页面
*
* @return mixed
*/
public function makeDynamicTask()
{
$private_task_context = [
//是否缓存下载数据(可选项,默认false)
'cache_enabled' => true,
//缓存下载数据存放目录 (可选项,默认位于系统临时目录下)
'cache_directory' => sys_get_temp_dir() . '/DownloadCache4PHPCreeper/',
//无头浏览器,如果是动态页面考虑启用,否则应当禁用 [默认使用chrome且为禁用状态]
'headless_browser' => ['headless' => true, /*更多其他无头参数*/],
];
$dynamic_task = [
'url' => "https://www.toutiao.com",
'rule' => [
'今日头条热榜标题' => ['div.show-monitor ol li a', 'aria-label'],
'今日头条热榜链接' => ['div.show-monitor ol li a', 'href'],
],
'context' => $private_task_context,
];
$this->createTask($dynamic_task);
}
/**
* @brief onProducerStart
* @param PHPCreeper $producer
* @return void
*/
public function onProducerStart(PHPCreeper $producer)
{
$this->makeTask();
$this->makeDynamicTask();
//使用Timer定时器创建任务
//Timer::add(5, [$this, "makeTask"], [], true);
//使用Crontab定时器创建任务
//new Crontab('*/5 * * * * *', function(){
//$this->makeTask();
//});
}
/**
* @brief onProducerStop
* @param PHPCreeper $producer
* @return void
*/
public function onProducerStop(PHPCreeper $producer)
{
}
/**
* @brief onProducerReload
* @param PHPCreeper $producer
* @return void
*/
public function onProducerReload(PHPCreeper $producer)
{
}
}
句柄类文件app/spider/TinywanDownloader.php
<?php
/**
* @desc TinywanDownloader.php 描述信息
* @author Tinywan(ShaoBo Wan)
* @date 2024/10/5 10:06
*/
declare(strict_types=1);
namespace app\spider;
use PHPCreeper\Kernel\PHPCreeper;
use Webman\PHPCreeper\Downloader;
class TinywanDownloader extends Downloader
{
/**
* @brief onDownloaderStart
*
* @param PHPCreeper $downloader
*
* @return void
*/
public function onDownloaderStart(PHPCreeper $downloader)
{
$downloader->setClientSocketAddress([
'ws://127.0.0.1:8888',
]);
}
/**
* @brief onDownloaderStop
* @param PHPCreeper $downloader
* @return void
*/
public function onDownloaderStop(PHPCreeper $downloader)
{
}
/**
* @brief onDownloaderReload
* @param PHPCreeper $downloader
*/
public function onDownloaderReload(PHPCreeper $downloader)
{
}
/**
* @brief onDownloaderMessage
*
* @param PHPCreeper $downloader
* @param string $parser_reply
*/
public function onDownloaderMessage(PHPCreeper $downloader, string $parser_reply)
{
//pprint($parser_reply, __METHOD__);
}
/**
* @brief onBeforeDownload
* @param PHPCreeper $downloader
* @param array $task
* @return mixed
*/
public function onBeforeDownload(PHPCreeper $downloader, array $task)
{
//$downloader->httpClient->setConnectTimeout(3);
//$downloader->httpClient->setTransferTimeout(10);
//$downloader->httpClient->setHeaders(array());
//$downloader->httpClient->setProxy('http://127.0.0.1:8800');
}
/**
* @brief onStartDownload
*
* @param PHPCreeper $downloader
* @param array $task
* @return void
*/
public function onStartDownload(PHPCreeper $downloader, array $task)
{
}
/**
* @brief onAfterDownload
*
* @param PHPCreeper $downloader
* @param array $download_data
* @param array $task
*
* @return mixed
*/
public function onAfterDownload(PHPCreeper $downloader, array $download_data, array $task)
{
//pprint($downloader->getDbo('test'), __METHOD__);
}
/**
* @brief onTaskEmpty
* @param PHPCreeper $downloader
*/
public function onTaskEmpty(PHPCreeper $downloader)
{
//$downloader->createTask($task);
}
/**
* @brief onHeadlessBrowserOpenPage
*
* @param PHPCreeper $downloader
* @param object $browser
* @param object $page
* @param string $url
*
* @return mixed
*/
public function onHeadlessBrowserOpenPage(PHPCreeper $downloader, $browser, $page, $url)
{
//注意:灵活设计特定类型的返回值有助于对付各种复杂的应用场景
//1. 返回false, 会触发中断后续的业务逻辑;
//2. 返回string,会触发中断后续的业务逻辑,一般多用于返回页面的HTML;
//3. 返回array, 会继续执行后续的业务逻辑,一般多用于返回无头浏览器选项参数;
//4. 返回其他, 会继续执行后续的业务逻辑,相当于是什么也没有发生;
//注意:一般无需调用如下几行代码,因为爬山虎内部默认会自动调用无头API做同样的工作.
//$page->navigate($url)->waitForNavigation('firstMeaningfulPaint');
//$html = $page->getHtml();
//return $html;
}
}
句柄类文件app/spider/TinywanParser.php
<?php
/**
* @desc TinywanParser.php 描述信息
* @author Tinywan(ShaoBo Wan)
* @date 2024/10/5 10:06
*/
declare(strict_types=1);
namespace app\spider;
use Webman\PHPCreeper\Parser;
class TinywanParser extends Parser
{
/**
* @brief onParserStart
*
* @param object $parser
*
* @return mixed
*/
public function onParserStart($parser)
{
}
/**
* @brief onParserStop
*
* @param object $parser
*
* @return mixed
*/
public function onParserStop($parser)
{
}
/**
* @brief onParserReload
*
* @param object $parser
*
* @return mixed
*/
public function onParserReload($parser)
{
}
/**
* @brief onParserMessage
*
* @param object $parser
* @param object $connection
* @param string $download_data
*
* @return mixed
*/
public function onParserMessage($parser, $connection, $download_data)
{
//pprint(strlen($download_data), __METHOD__);
}
/**
* @brief onParserFindUrl
*
* @param object $parser
* @param string $url
*
* @return string
*/
public function onParserFindUrl($parser, string $url)
{
return $url;
}
/**
* @brief onParserExtractField
*
* @param object $parser
* @param string $download_data
* @param array $fields
*
* @return mixed
*/
public function onParserExtractField($parser, $download_data, $fields)
{
!empty($fields) && pprint($fields[$parser->task['rule_name']]);
}
}
配置文件路径config/plugin/blogdaren/webman-phpcreeper/process.php
修改为以下配置
<?php
return [
'myproducer' => [
'handler' => \app\spider\TinywanProducer::class,
'listen' => '',
'count' => 1,
'constructor' => ['config' =>
include('spider/global.php')
],
],
'mydownloader' => [
'handler' => \app\spider\TinywanDownloader::class,
'listen' => '',
'count' => 1,
'constructor' => ['config' =>
include('spider/global.php')
],
],
'myparser' => [
'handler' => \app\spider\TinywanParser::class,
'listen' => 'websocket://0.0.0.0:8888',
'count' => 1,
'constructor' => ['config' =>
include('spider/global.php')
],
],
];
配置文件路径config/plugin/blogdaren/webman-phpcreeper/spider/database.php
修改为自己的Redis配置就可以啦
<?php
return array(
'redis' => array(
'host' => 'dnmp-redis',
'port' => 63789,
'database' => '0',
'auth' => false,
'pass' => 'guest',
'prefix' => 'PHPCreeper',
'connection_timeout' => 5,
'read_write_timeout' => 0,
),
);
由于以多worker模式运行时依赖Redis服务,所以需要配置Redis服务,否则会有以下错误提示
| ERROR | 服务巡检:Connection refused [tcp://dnmp-redis:63798],
以多worker模式运行时依赖[redis-server]服务,
请检查[redis-server]服务有没有启动 或 防火墙有没有放行服务端口, 当前进程将在休眠10秒之后自动退出并重启后继续运行.
确保以上配置都没问题,就可以开启webman服务进行爬虫了
开启服务
爬虫数据
Array
(
[0] => Array
(
[日子] => 2024-10-05 | 5日(今天)
[天气] => 多云转阴
[温度] => 21/13℃
)
[1] => Array
(
[日子] => 2024-10-05 | 6日(明天)
[天气] => 小雨
[温度] => 18/10℃
)
[2] => Array
(
[日子] => 2024-10-05 | 7日(后天)
[天气] => 晴
[温度] => 21/8℃
)
[3] => Array
(
[日子] => 2024-10-05 | 8日(周二)
[天气] => 晴转多云
[温度] => 21/9℃
)
[4] => Array
(
[日子] => 2024-10-05 | 9日(周三)
[天气] => 多云转晴
[温度] => 22/12℃
)
[5] => Array
(
[日子] => 2024-10-05 | 10日(周四)
[天气] => 晴
[温度] => 21/10℃
)
[6] => Array
(
[日子] => 2024-10-05 | 11日(周五)
[天气] => 晴
[温度] => 22/10℃
)
)
其他下载