PHP 爬虫任务调度

wen PHP项目 4

本文目录导读:

PHP 爬虫任务调度

  1. 基础架构设计
  2. 定时调度实现
  3. 多进程调度
  4. Redis队列调度
  5. 数据库调度
  6. 完整使用示例

我来为您提供一个完整的PHP爬虫任务调度解决方案,包含多种调度方式和实战示例。

基础架构设计

1 任务调度器核心类

<?php
// TaskScheduler.php
namespace Crawler\Scheduler;
use Exception;
use Redis;
use PDO;
class TaskScheduler
{
    private $redis;
    private $pdo;
    private $config;
    public function __construct($config)
    {
        $this->config = $config;
        $this->initRedis();
        $this->initDatabase();
    }
    /**
     * 初始化Redis连接
     */
    private function initRedis()
    {
        $this->redis = new Redis();
        $this->redis->connect(
            $this->config['redis']['host'],
            $this->config['redis']['port']
        );
        if (!empty($this->config['redis']['password'])) {
            $this->redis->auth($this->config['redis']['password']);
        }
        $this->redis->select($this->config['redis']['db']);
    }
    /**
     * 初始化数据库连接
     */
    private function initDatabase()
    {
        $dsn = sprintf(
            "mysql:host=%s;dbname=%s;charset=utf8mb4",
            $this->config['database']['host'],
            $this->config['database']['dbname']
        );
        $this->pdo = new PDO($dsn, $this->config['database']['user'], $this->config['database']['password']);
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }
    /**
     * 添加任务
     */
    public function addTask($taskName, $callback, $interval = 60, $priority = 5)
    {
        $taskId = uniqid('task_', true);
        $taskData = [
            'id' => $taskId,
            'name' => $taskName,
            'callback' => $callback,
            'interval' => $interval,
            'priority' => $priority,
            'last_run' => 0,
            'status' => 'pending'
        ];
        // 保存到Redis队列
        $this->redis->hSet('crawler:tasks', $taskId, json_encode($taskData));
        return $taskId;
    }
    /**
     * 获取待执行的任务
     */
    public function getPendingTasks()
    {
        $tasks = $this->redis->hGetAll('crawler:tasks');
        $pendingTasks = [];
        foreach ($tasks as $taskId => $taskJson) {
            $task = json_decode($taskJson, true);
            if ($task['status'] === 'pending' || 
                (time() - $task['last_run']) >= $task['interval']) {
                $pendingTasks[] = $task;
            }
        }
        // 按优先级排序
        usort($pendingTasks, function($a, $b) {
            return $a['priority'] <=> $b['priority'];
        });
        return $pendingTasks;
    }
    /**
     * 运行任务
     */
    public function runTask($task)
    {
        $taskId = $task['id'];
        // 标记任务为运行中
        $this->markTaskRunning($taskId);
        try {
            // 执行任务回调
            $result = call_user_func($task['callback']);
            // 更新任务状态
            $this->markTaskCompleted($taskId, $result);
            return ['success' => true, 'data' => $result];
        } catch (Exception $e) {
            // 记录错误
            $this->markTaskFailed($taskId, $e->getMessage());
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }
    private function markTaskRunning($taskId)
    {
        $task = $this->getTask($taskId);
        $task['status'] = 'running';
        $this->redis->hSet('crawler:tasks', $taskId, json_encode($task));
    }
    private function markTaskCompleted($taskId, $result)
    {
        $task = $this->getTask($taskId);
        $task['status'] = 'completed';
        $task['last_run'] = time();
        $task['last_result'] = $result;
        $this->redis->hSet('crawler:tasks', $taskId, json_encode($task));
        // 保存执行日志
        $this->saveTaskLog($taskId, 'success', $result);
    }
    private function markTaskFailed($taskId, $error)
    {
        $task = $this->getTask($taskId);
        $task['status'] = 'failed';
        $task['last_error'] = $error;
        $this->redis->hSet('crawler:tasks', $taskId, json_encode($task));
        // 保存错误日志
        $this->saveTaskLog($taskId, 'error', $error);
    }
    public function getTask($taskId)
    {
        $taskJson = $this->redis->hGet('crawler:tasks', $taskId);
        return $taskJson ? json_decode($taskJson, true) : null;
    }
    private function saveTaskLog($taskId, $status, $message)
    {
        $sql = "INSERT INTO task_logs (task_id, status, message, created_at) 
                VALUES (?, ?, ?, NOW())";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$taskId, $status, $message]);
    }
}

定时调度实现

1 Cron定时调度

<?php
// cron_scheduler.php
require_once 'TaskScheduler.php';
class CronScheduler
{
    private $scheduler;
    private $taskConfigs = [];
    public function __construct($config)
    {
        $this->scheduler = new TaskScheduler($config);
    }
    /**
     * 注册定时任务配置
     */
    public function registerTask($taskName, $cronExpression, $callback)
    {
        $this->taskConfigs[] = [
            'name' => $taskName,
            'cron' => $cronExpression,
            'callback' => $callback
        ];
    }
    /**
     * 检查并运行到期任务
     */
    public function checkAndRun()
    {
        foreach ($this->taskConfigs as $taskConfig) {
            if ($this->isDue($taskConfig['cron'])) {
                $this->scheduler->runTask($taskConfig);
            }
        }
    }
    /**
     * 解析Cron表达式(简化版)
     */
    private function isDue($cronExpression)
    {
        $parts = explode(' ', trim($cronExpression));
        if (count($parts) != 5) {
            return false;
        }
        list($minute, $hour, $day, $month, $weekday) = $parts;
        $now = time();
        return $this->matchField($minute, date('i', $now)) &&
               $this->matchField($hour, date('H', $now)) &&
               $this->matchField($day, date('j', $now)) &&
               $this->matchField($month, date('n', $now)) &&
               $this->matchField($weekday, date('w', $now));
    }
    private function matchField($pattern, $value)
    {
        if ($pattern == '*') {
            return true;
        }
        if (strpos($pattern, '/') !== false) {
            list($range, $step) = explode('/', $pattern);
            if ($range == '*') {
                $range = '0-59';
            }
            list($start, $end) = explode('-', $range);
            return ($value >= $start && $value <= $end && ($value - $start) % $step == 0);
        }
        if (strpos($pattern, '-') !== false) {
            list($start, $end) = explode('-', $pattern);
            return ($value >= $start && $value <= $end);
        }
        if (strpos($pattern, ',') !== false) {
            $values = explode(',', $pattern);
            return in_array($value, $values);
        }
        return $value == $pattern;
    }
}
// 使用示例
$config = require 'config.php';
$scheduler = new CronScheduler($config);
// 注册定时爬虫任务
$scheduler->registerTask('crawl_news', '*/30 * * * *', function() {
    $crawler = new NewsCrawler();
    return $crawler->crawl();
});
$scheduler->registerTask('crawl_products', '0 */2 * * *', function() {
    $crawler = new ProductCrawler();
    return $crawler->crawl();
});
// 执行调度检查
$scheduler->checkAndRun();

2 PHP脚本无限循环调度

<?php
// loop_scheduler.php
class LoopScheduler
{
    private $scheduler;
    private $maxExecutionTime;
    private $delayBetweenTasks;
    public function __construct($config)
    {
        $this->scheduler = new TaskScheduler($config);
        $this->maxExecutionTime = $config['scheduler']['max_execution_time'] ?? 60;
        $this->delayBetweenTasks = $config['scheduler']['delay_between_tasks'] ?? 5;
    }
    /**
     * 无限循环执行
     */
    public function runForever()
    {
        $startTime = time();
        while (true) {
            // 检查最大执行时间
            if ((time() - $startTime) > $this->maxExecutionTime) {
                log_message("达到最大执行时间,退出循环");
                break;
            }
            // 获取待执行任务
            $tasks = $this->scheduler->getPendingTasks();
            foreach ($tasks as $task) {
                $this->scheduler->runTask($task);
                sleep($this->delayBetweenTasks);
            }
            // 防止占用过多CPU
            usleep(100000); // 0.1秒
        }
    }
    /**
     * 带退避重试的无限循环
     */
    public function runWithBackoff()
    {
        $retryCount = 0;
        $maxRetries = 5;
        while (true) {
            try {
                $this->runForever();
                $retryCount = 0; // 重置重试计数
            } catch (Exception $e) {
                $retryCount++;
                if ($retryCount > $maxRetries) {
                    log_error("连续失败{$maxRetries}次,停止调度器");
                    break;
                }
                // 指数退避
                $backoffTime = pow(2, $retryCount) * 5;
                log_warning("调度器异常,{$backoffTime}秒后重试");
                sleep($backoffTime);
            }
        }
    }
}

多进程调度

1 基于PCNTL的多进程调度

<?php
// multi_process_scheduler.php
class MultiProcessScheduler
{
    private $scheduler;
    private $processLimit;
    private $childProcesses = [];
    public function __construct($config)
    {
        $this->scheduler = new TaskScheduler($config);
        $this->processLimit = $config['scheduler']['process_limit'] ?? 4;
    }
    /**
     * 多进程执行任务
     */
    public function runConcurrently($tasks)
    {
        if (function_exists('pcntl_fork') === false) {
            throw new Exception('PCNTL扩展未安装');
        }
        $taskChunks = array_chunk($tasks, $this->processLimit);
        foreach ($taskChunks as $chunk) {
            $this->forkAndExecute($chunk);
            $this->waitForChildren();
        }
    }
    private function forkAndExecute($tasks)
    {
        $pid = pcntl_fork();
        if ($pid == -1) {
            throw new Exception('无法创建子进程');
        }
        if ($pid) {
            // 父进程
            $this->childProcesses[] = $pid;
        } else {
            // 子进程
            foreach ($tasks as $task) {
                $result = $this->scheduler->runTask($task);
                echo "Task {$task['name']} result: " . json_encode($result) . "\n";
            }
            exit(0);
        }
    }
    private function waitForChildren()
    {
        while (count($this->childProcesses) > 0) {
            $pid = pcntl_waitpid(-1, $status, WNOHANG);
            if ($pid > 0) {
                if (($key = array_search($pid, $this->childProcesses)) !== false) {
                    unset($this->childProcesses[$key]);
                }
            }
            usleep(100000); // 0.1秒检查一次
        }
    }
}

Redis队列调度

1 基于Redis的队列调度器

<?php
// queue_scheduler.php
class QueueScheduler
{
    private $redis;
    private $queueKey = 'crawler:queue';
    private $retryKey = 'crawler:retry';
    public function __construct($config)
    {
        $this->redis = new Redis();
        $this->redis->connect($config['redis']['host'], $config['redis']['port']);
    }
    /**
     * 添加任务到队列
     */
    public function enqueue($task, $priority = 0, $delay = 0)
    {
        $taskData = [
            'data' => $task,
            'priority' => $priority,
            'available_at' => time() + $delay
        ];
        // 使用有序集合实现优先级队列
        $this->redis->zAdd($this->queueKey, $priority + ($taskData['available_at'] / 1000000), json_encode($taskData));
    }
    /**
     * 从队列获取任务
     */
    public function dequeue()
    {
        $options = [
            'count' => 1,
            'withscores' => true
        ];
        $tasks = $this->redis->zRangeByScore($this->queueKey, 0, time(), $options);
        if (empty($tasks)) {
            return null;
        }
        $taskKey = key($tasks);
        $this->redis->zRem($this->queueKey, $taskKey);
        return json_decode($taskKey, true);
    }
    /**
     * 任务重试
     */
    public function retryTask($task, $retryCount = 0)
    {
        $this->enqueue($task, 5, 2^$retryCount * 5);
        if ($retryCount > 3) {
            $this->redis->hSet($this->retryKey, uniqid(), json_encode($task));
        }
    }
    /**
     * 消费者工作进程
     */
    public function worker()
    {
        while (true) {
            $task = $this->dequeue();
            if ($task) {
                try {
                    $this->processTask($task);
                } catch (Exception $e) {
                    $this->handleFailure($task, $e);
                }
            } else {
                usleep(1000000); // 1秒后重试
            }
        }
    }
    private function processTask($task)
    {
        echo "Processing task: " . json_encode($task) . "\n";
        // 实际处理逻辑
    }
    private function handleFailure($task, Exception $e)
    {
        $task['error'] = $e->getMessage();
        $this->retryTask($task);
    }
}

数据库调度

1 基于数据库的任务调度

<?php
// database_scheduler.php
class DatabaseScheduler
{
    private $pdo;
    public function __construct($config)
    {
        $dsn = sprintf(
            "mysql:host=%s;dbname=%s;charset=utf8mb4",
            $config['database']['host'],
            $config['database']['dbname']
        );
        $this->pdo = new PDO($dsn, $config['database']['user'], $config['database']['password']);
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->createTables();
    }
    /**
     * 创建必要的表
     */
    private function createTables()
    {
        $sql = "
            CREATE TABLE IF NOT EXISTS scheduled_tasks (
                id INT AUTO_INCREMENT PRIMARY KEY,
                task_name VARCHAR(100) NOT NULL,
                task_type VARCHAR(50) NOT NULL,
                task_params TEXT,
                schedule_expression VARCHAR(100),
                is_active BOOLEAN DEFAULT TRUE,
                last_run_at DATETIME,
                next_run_at DATETIME,
                status VARCHAR(20) DEFAULT 'pending',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                INDEX idx_next_run (next_run_at)
            ) ENGINE=InnoDB;
            CREATE TABLE IF NOT EXISTS task_executions (
                id INT AUTO_INCREMENT PRIMARY KEY,
                task_id INT NOT NULL,
                execution_time DATETIME NOT NULL,
                status VARCHAR(20) NOT NULL,
                output TEXT,
                error TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (task_id) REFERENCES scheduled_tasks(id)
            ) ENGINE=InnoDB;
        ";
        $this->pdo->exec($sql);
    }
    /**
     * 添加定时任务
     */
    public function createScheduledTask($name, $type, $params, $schedule)
    {
        $sql = "INSERT INTO scheduled_tasks (task_name, task_type, task_params, schedule_expression, next_run_at)
                VALUES (?, ?, ?, ?, NOW())";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$name, $type, json_encode($params), $schedule]);
        return $this->pdo->lastInsertId();
    }
    /**
     * 获取到期任务
     */
    public function getDueTasks()
    {
        $sql = "SELECT * FROM scheduled_tasks 
                WHERE is_active = TRUE 
                AND next_run_at <= NOW()
                AND status = 'pending'
                ORDER BY next_run_at ASC
                LIMIT 10";
        $stmt = $this->pdo->query($sql);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    /**
     * 更新任务状态和下一次执行时间
     */
    public function updateTaskAfterRun($taskId, $status, $output = null, $error = null)
    {
        try {
            // 记录执行历史
            $sql = "INSERT INTO task_executions (task_id, execution_time, status, output, error)
                    VALUES (?, NOW(), ?, ?, ?)";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([$taskId, $status, $output, $error]);
            // 更新任务信息
            if ($status == 'success') {
                $sql = "UPDATE scheduled_tasks 
                        SET last_run_at = NOW(), 
                            next_run_at = DATE_ADD(NOW(), INTERVAL 1 DAY),
                            status = 'pending'
                        WHERE id = ?";
            } else {
                $sql = "UPDATE scheduled_tasks 
                        SET last_run_at = NOW(),
                            next_run_at = DATE_ADD(NOW(), INTERVAL 10 MINUTE),
                            status = 'retry'
                        WHERE id = ?";
            }
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([$taskId]);
        } catch (Exception $e) {
            error_log("Error updating task: " . $e->getMessage());
        }
    }
    /**
     * 运行调度器
     */
    public function runScheduler()
    {
        $tasks = $this->getDueTasks();
        foreach ($tasks as $task) {
            try {
                $result = $this->executeTask($task);
                $this->updateTaskAfterRun($task['id'], 'success', json_encode($result));
            } catch (Exception $e) {
                $this->updateTaskAfterRun($task['id'], 'error', null, $e->getMessage());
            }
        }
    }
    private function executeTask($task)
    {
        $params = json_decode($task['task_params'], true);
        switch ($task['task_type']) {
            case 'crawler':
                return $this->executeCrawler($params);
            case 'parser':
                return $this->executeParser($params);
            case 'notification':
                return $this->executeNotification($params);
            default:
                throw new Exception("Unknown task type: {$task['task_type']}");
        }
    }
    private function executeCrawler($params)
    {
        $url = $params['url'];
        $html = file_get_contents($url);
        // 示例:使用Goutte或Symfony DomCrawler
        $crawler = new GoutteClient();
        $crawler->request('GET', $url);
        return [
            'url' => $url,
            'title' => $crawler->filter('title')->text(),
            'content_length' => strlen($html)
        ];
    }
    private function executeParser($params)
    {
        // 解析逻辑
        return ['status' => 'parsed'];
    }
    private function executeNotification($params)
    {
        // 通知逻辑
        return ['status' => 'notified'];
    }
}

完整使用示例

1 配置文件

<?php
// config.php
return [
    'redis' => [
        'host' => '127.0.0.1',
        'port' => 6379,
        'password' => '',
        'db' => 0
    ],
    'database' => [
        'host' => '127.0.0.1',
        'dbname' => 'crawler',
        'user' => 'root',
        'password' => 'password'
    ],
    'scheduler' => [
        'max_execution_time' => 3600,
        'delay_between_tasks' => 5,
        'process_limit' => 4,
        'retry_count' => 3,
        'retry_delay' => 60
    ],
    'crawler' => [
        'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'timeout' => 30,
        'max_depth' => 3,
        'respect_robots' => true
    ]
];

2 主调度入口

<?php
// main_scheduler.php
require_once 'TaskScheduler.php';
require_once 'CronScheduler.php';
require_once 'LoopScheduler.php';
require_once 'MultiProcessScheduler.php';
require_once 'QueueScheduler.php';
class MainScheduler
{
    private $config;
    private $scheduler;
    public function __construct($config)
    {
        $this->config = $config;
        $this->scheduler = new TaskScheduler($config);
    }
    /**
     * 启动调度系统
     */
    public function start()
    {
        $mode = $this->config['scheduler']['mode'] ?? 'cron';
        switch ($mode) {
            case 'cron':
                $this->runCronMode();
                break;
            case 'loop':
                $this->runLoopMode();
                break;
            case 'multiprocess':
                $this->runMultiProcessMode();
                break;
            case 'queue':
                $this->runQueueMode();
                break;
            default:
                throw new Exception("Unsupported scheduler mode: $mode");
        }
    }
    private function runCronMode()
    {
        $scheduler = new CronScheduler($this->config);
        // 注册任务
        $scheduler->registerTask('crawl_news', '*/5 * * * *', function() {
            $crawler = new NewsCrawler();
            return $crawler->crawlLatestNews();
        });
        $scheduler->registerTask('crawl_products', '0 */1 * * *', function() {
            $crawler = new ProductCrawler();
            return $crawler->crawlProducts();
        });
        $scheduler->checkAndRun();
    }
    private function runLoopMode()
    {
        $scheduler = new LoopScheduler($this->config);
        $scheduler->runWithBackoff();
    }
    private function runMultiProcessMode()
    {
        $scheduler = new MultiProcessScheduler($this->config);
        while (true) {
            $tasks = $this->scheduler->getPendingTasks();
            if (!empty($tasks)) {
                $scheduler->runConcurrently($tasks);
            }
            sleep(10);
        }
    }
    private function runQueueMode()
    {
        $scheduler = new QueueScheduler($this->config);
        $scheduler->worker();
    }
}
// 启动调度器
$config = require 'config.php';
$mainScheduler = new MainScheduler($config);
$mainScheduler->start();

3 监控脚本

<?php
// monitor.php
class SchedulerMonitor
{
    private $redis;
    private $pdo;
    public function __construct($config)
    {
        $this->redis = new Redis();
        $this->redis->connect($config['redis']['host'], $config['redis']['port']);
        $dsn = "mysql:host=" . $config['database']['host'] . ";dbname=" . $config['database']['dbname'];
        $this->pdo = new PDO($dsn, $config['database']['user'], $config['database']['password']);
    }
    /**
     * 获取系统状态
     */
    public function getStatus()
    {
        return [
            'queue_size' => $this->redis->lLen('crawler:queue'),
            'active_tasks' => $this->redis->hLen('crawler:tasks'),
            'last_executions' => $this->getLastExecutions(),
            'error_rates' => $this->getErrorRate()
        ];
    }
    private function getLastExecutions($limit = 10)
    {
        $sql = "SELECT * FROM task_executions ORDER BY created_at DESC LIMIT ?";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    private function getErrorRate()
    {
        $sql = "SELECT 
                    COUNT(*) as total,
                    SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errors
                FROM task_executions
                WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)";
        $stmt = $this->pdo->query($sql);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result['total'] > 0 ? 
            ($result['errors'] / $result['total']) * 100 : 0;
    }
    /**
     * 发送警报
     */
    public function sendAlert($message, $severity = 'warning')
    {
        // 实现邮件、短信或Webhook通知
        // ...
    }
}
// 定期执行监控
$monitor = new SchedulerMonitor($config);
$status = $monitor->getStatus();
if ($status['error_rates'] > 20) {
    $monitor->sendAlert("错误率过高: " . $status['error_rates'] . "%");
}

这套PHP爬虫任务调度系统包含:

  1. 多种调度模式:Cron定时、无限循环、多进程、Redis队列
  2. 任务管理:添加、删除、查询、暂停/恢复
  3. 错误处理:重试机制、错误日志、告警通知
  4. 高可用性:多进程并发、队列处理、数据库持久化
  5. 监控支持:执行日志、状态查询、错误率统计

使用建议:

  • 简单任务使用Cron调度
  • 需要快速响应用Loop模式
  • 大规模爬取使用多进程或队列模式
  • 生产环境推荐使用Redis队列+ Workers模式

根据实际需求选择合适的调度方式,并根据系统负载调整配置参数。

抱歉,评论功能暂时关闭!