本文目录导读:

我来详细介绍如何使用PHP构建爬虫管理系统。
基础爬虫框架
简单的爬虫类
<?php
class SimpleCrawler {
private $ch;
private $userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36';
private $cookies = [];
private $timeout = 30;
private $maxRetries = 3;
private $proxy = null;
public function __construct($config = []) {
if (isset($config['userAgent'])) $this->userAgent = $config['userAgent'];
if (isset($config['timeout'])) $this->timeout = $config['timeout'];
if (isset($config['proxy'])) $this->proxy = $config['proxy'];
}
/**
* 发送GET请求
*/
public function get($url, $headers = []) {
return $this->request('GET', $url, null, $headers);
}
/**
* 发送POST请求
*/
public function post($url, $data = [], $headers = []) {
return $this->request('POST', $url, $data, $headers);
}
/**
* 核心请求方法
*/
private function request($method, $url, $data = null, $headers = []) {
$attempts = 0;
while ($attempts < $this->maxRetries) {
try {
$this->ch = curl_init();
// 基础配置
curl_setopt_array($this->ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_USERAGENT => $this->userAgent,
CURLOPT_HTTPHEADER => $this->buildHeaders($headers),
]);
// 请求类型处理
if ($method === 'POST') {
curl_setopt($this->ch, CURLOPT_POST, true);
if ($data) {
curl_setopt($this->ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
}
}
// Cookie处理
if (!empty($this->cookies)) {
curl_setopt($this->ch, CURLOPT_COOKIE, $this->buildCookieString());
}
// 代理设置(可选)
if ($this->proxy) {
curl_setopt($this->ch, CURLOPT_PROXY, $this->proxy);
}
// 执行请求
$response = curl_exec($this->ch);
$httpCode = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
// 处理Cookie
preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $response, $matches);
foreach ($matches[1] as $cookie) {
$parts = explode('=', $cookie, 2);
if (count($parts) == 2) {
$this->cookies[$parts[0]] = $parts[1];
}
}
curl_close($this->ch);
if ($httpCode === 200) {
return [
'success' => true,
'http_code' => $httpCode,
'content' => $response,
'cookies' => $this->cookies
];
}
// 处理限流
if ($httpCode === 429 || $httpCode === 503) {
sleep(5); // 等待5秒
$attempts++;
continue;
}
return [
'success' => false,
'http_code' => $httpCode,
'content' => $response
];
} catch (Exception $e) {
$attempts++;
if ($attempts >= $this->maxRetries) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
sleep(1);
}
}
return ['success' => false, 'error' => 'Max retries exceeded'];
}
/**
* 构建请求头
*/
private function buildHeaders($customHeaders) {
$defaultHeaders = [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: en-US,en;q=0.5',
'Connection: keep-alive',
'Upgrade-Insecure-Requests: 1',
];
return array_merge($defaultHeaders, $customHeaders);
}
/**
* 构建Cookie字符串
*/
private function buildCookieString() {
$cookieStr = '';
foreach ($this->cookies as $key => $value) {
$cookieStr .= $key . '=' . $value . '; ';
}
return rtrim($cookieStr, '; ');
}
/**
* 添加Cookie
*/
public function addCookie($name, $value) {
$this->cookies[$name] = $value;
}
/**
* 清除Cookie
*/
public function clearCookies() {
$this->cookies = [];
}
/**
* 获取页面链接
*/
public function extractLinks($html, $baseUrl) {
$links = [];
// 提取HTML中的链接
preg_match_all('/<a[^>]*href=["\']([^"\']+)["\']/i', $html, $matches);
foreach ($matches[1] as $link) {
// 处理相对路径和绝对路径
$absoluteLink = $this->resolveUrl($baseUrl, $link);
if ($absoluteLink && !in_array($absoluteLink, $links)) {
$links[] = $absoluteLink;
}
}
return $links;
}
/**
* URL解析
*/
private function resolveUrl($baseUrl, $url) {
// 如果是绝对URL,直接返回
if (filter_var($url, FILTER_VALIDATE_URL)) {
return $url;
}
// 处理相对URL
$parsed = parse_url($baseUrl);
// 处理协议相对URL
if (strpos($url, '//') === 0) {
return $parsed['scheme'] . ':' . $url;
}
// 处理锚点和JavaScript链接
if (strpos($url, '#') === 0 || strpos($url, 'javascript:') === 0) {
return null;
}
// 处理相对路径
$base = $parsed['scheme'] . '://' . $parsed['host'];
if (isset($parsed['port'])) {
$base .= ':' . $parsed['port'];
}
// 处理路径
$path = isset($parsed['path']) ? $parsed['path'] : '/';
$path = str_replace('\\', '/', $path);
if (strpos($url, '/') === 0) {
// 绝对路径
return $base . $url;
} else {
// 相对路径
$dir = dirname($path);
return $base . $dir . '/' . $url;
}
}
}
爬虫管理系统
数据库设计
-- 任务表
CREATE TABLE `crawler_tasks` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL COMMENT '任务名称',
`target_url` VARCHAR(500) NOT NULL COMMENT '目标URL',
`max_pages` INT(11) DEFAULT 100 COMMENT '最大爬取页数',
`interval_time` INT(11) DEFAULT 2 COMMENT '请求间隔(秒)',
`status` ENUM('pending','running','paused','completed','failed') DEFAULT 'pending',
`last_run_at` TIMESTAMP NULL DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_status` (`status`)
) ENGINE=InnoDB;
-- URL队列表
CREATE TABLE `crawler_urls` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`task_id` INT(11) NOT NULL,
`url` VARCHAR(500) NOT NULL,
`status` ENUM('pending','processing','completed','failed') DEFAULT 'pending',
`retry_count` INT(11) DEFAULT 0,
`error_message` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_url` (`task_id`, `url`),
INDEX `idx_task_status` (`task_id`, `status`)
) ENGINE=InnoDB;
-- 爬取数据表
CREATE TABLE `crawler_data` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`task_id` INT(11) NOT NULL,
`url` VARCHAR(500) NOT NULL, VARCHAR(500),
`content` LONGTEXT,
`meta` TEXT COMMENT '元数据',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_task_id` (`task_id`)
) ENGINE=InnoDB;
-- 代理池表
CREATE TABLE `proxies` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`ip` VARCHAR(50) NOT NULL,
`port` INT(11) NOT NULL,
`type` ENUM('http','https') DEFAULT 'http',
`status` ENUM('active','inactive') DEFAULT 'active',
`speed` INT(11) DEFAULT 0 COMMENT '响应速度ms',
`last_checked_at` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
任务管理器类
<?php
class CrawlerManager {
private $db;
private $logger;
public function __construct($db, $logger = null) {
$this->db = $db;
$this->logger = $logger;
}
/**
* 创建爬虫任务
*/
public function createTask($data) {
$sql = "INSERT INTO crawler_tasks (name, target_url, max_pages, interval_time)
VALUES (?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('ssii', $data['name'], $data['target_url'], $data['max_pages'], $data['interval_time']);
$stmt->execute();
return $this->db->insert_id;
}
/**
* 启动任务
*/
public function startTask($taskId) {
// 更新任务状态
$this->updateTaskStatus($taskId, 'running');
// 获取任务信息
$task = $this->getTask($taskId);
if (!$task) return false;
// 初始化URL队列
$this->initUrlQueue($taskId, $task);
// 启动爬虫进程
return $this->startCrawlerProcess($taskId);
}
/**
* 暂停任务
*/
public function pauseTask($taskId) {
$this->updateTaskStatus($taskId, 'paused');
// 停止爬虫进程
$this->stopCrawlerProcess($taskId);
// 保存进度
$this->saveTaskProgress($taskId);
}
/**
* 初始化URL队列
*/
private function initUrlQueue($taskId, $task) {
// 检查是否已有队列
$count = $this->getQueueCount($taskId);
if ($count == 0) {
$sql = "INSERT INTO crawler_urls (task_id, url) VALUES (?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('is', $taskId, $task['target_url']);
$stmt->execute();
}
}
/**
* 开始爬虫进程
*/
private function startCrawlerProcess($taskId) {
// 使用pcntl_fork创建子进程(如果可用)
if (extension_loaded('pcntl')) {
$pid = pcntl_fork();
if ($pid == -1) {
// 无法创建子进程
return false;
} elseif ($pid) {
// 父进程
return $pid;
} else {
// 子进程执行爬虫
$this->runCrawlerInSubprocess($taskId);
exit();
}
} else {
// 使用curl_multi并发爬取
$this->runCrawlerInMultithread($taskId);
}
return true;
}
/**
* 子进程爬虫执行
*/
private function runCrawlerInSubprocess($taskId) {
$crawler = new SimpleCrawler();
$task = $this->getTask($taskId);
// 获取待处理的URL
$urls = $this->getPendingUrls($taskId, 10);
while (count($urls) > 0 && $this->getTaskStatus($taskId) == 'running') {
foreach ($urls as $url) {
// 标记为处理中
$this->updateUrlStatus($url['id'], 'processing');
try {
// 抓取页面
$result = $crawler->get($url['url']);
if ($result['success']) {
// 保存数据
$this->saveCrawledData($taskId, $url['url'], $result['content']);
// 提取新链接
$links = $crawler->extractLinks($result['content'], $url['url']);
$this->addUrlsToQueue($taskId, $links);
// 更新状态
$this->updateUrlStatus($url['id'], 'completed');
} else {
// 处理失败
$this->handleUrlFailure($url['id'], $result['error']);
}
} catch (Exception $e) {
$this->handleUrlFailure($url['id'], $e->getMessage());
}
// 间隔等待
sleep($task['interval_time']);
}
// 获取下一批URL
$urls = $this->getPendingUrls($taskId, 10);
}
// 完成或暂停任务
$this->updateTaskStatus($taskId, array_key_exists('urls', $urls) ? 'completed' : 'paused');
}
/**
* 使用curl_multi并发爬取
*/
private function runCrawlerInMultithread($taskId) {
$task = $this->getTask($taskId);
// 获取待处理的URL
$urls = $this->getPendingUrls($taskId, 10);
while (count($urls) > 0 && $this->getTaskStatus($taskId) == 'running') {
// 创建多个curl句柄
$handles = [];
$multiHandle = curl_multi_init();
foreach ($urls as $url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url['url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_multi_add_handle($multiHandle, $ch);
$handles[$url['id']] = $ch;
}
// 执行并发请求
$running = null;
do {
curl_multi_exec($multiHandle, $running);
usleep(100000); // 0.1秒
} while ($running > 0);
// 处理响应
foreach ($handles as $urlId => $ch) {
$content = curl_multi_getcontent($ch);
$info = curl_getinfo($ch);
if ($info['http_code'] == 200) {
// 保存数据
$this->saveCrawledData($taskId, $info['url'], $content);
$this->updateUrlStatus($urlId, 'completed');
} else {
$this->handleUrlFailure($urlId, "HTTP Code: " . $info['http_code']);
}
curl_multi_remove_handle($multiHandle, $ch);
curl_close($ch);
}
curl_multi_close($multiHandle);
// 间隔等待
sleep($task['interval_time']);
// 获取下一批URL
$urls = $this->getPendingUrls($taskId, 10);
}
$remaining = $this->getPendingCount($taskId);
if ($remaining == 0) {
$this->updateTaskStatus($taskId, 'completed');
} else {
$this->updateTaskStatus($taskId, 'paused');
}
}
/**
* 保存爬取数据
*/
private function saveCrawledData($taskId, $url, $content) {
// 解析HTML内容
$title = $this->extractTitle($content);
$text = $this->extractText($content);
$sql = "INSERT INTO crawler_data (task_id, url, title, content) VALUES (?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('isss', $taskId, $url, $title, $text);
$stmt->execute();
}
/**
* 提取标题
*/
private function extractTitle($html) {
preg_match('/<title[^>]*>(.*?)<\/title>/i', $html, $matches);
return isset($matches[1]) ? trim($matches[1]) : '';
}
/**
* 提取文本内容
*/
private function extractText($html) {
// 移除script和style
$html = preg_replace('/<script[^>]*>.*?<\/script>/is', '', $html);
$html = preg_replace('/<style[^>]*>.*?<\/style>/is', '', $html);
// 转换为纯文本
$text = strip_tags($html);
return trim($text);
}
/**
* 获取待处理的URL
*/
private function getPendingUrls($taskId, $limit) {
$sql = "SELECT * FROM crawler_urls
WHERE task_id = ? AND status = 'pending'
LIMIT ?";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('ii', $taskId, $limit);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
/**
* 更新URL状态
*/
private function updateUrlStatus($urlId, $status) {
$sql = "UPDATE crawler_urls SET status = ? WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('si', $status, $urlId);
$stmt->execute();
}
/**
* 处理失败URL
*/
private function handleUrlFailure($urlId, $error) {
$sql = "UPDATE crawler_urls
SET status = 'failed', error_message = ?,
retry_count = retry_count + 1
WHERE id = ? AND retry_count < 3";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('si', $error, $urlId);
$stmt->execute();
}
/**
* 添加URL到队列
*/
private function addUrlsToQueue($taskId, $urls) {
$sql = "INSERT IGNORE INTO crawler_urls (task_id, url) VALUES (?, ?)";
$stmt = $this->db->prepare($sql);
foreach ($urls as $url) {
$stmt->bind_param('is', $taskId, $url);
$stmt->execute();
}
}
/**
* 获取任务信息
*/
public function getTask($taskId) {
$sql = "SELECT * FROM crawler_tasks WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('i', $taskId);
$stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
/**
* 更新任务状态
*/
private function updateTaskStatus($taskId, $status) {
$sql = "UPDATE crawler_tasks SET status = ?, last_run_at = NOW() WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('si', $status, $taskId);
$stmt->execute();
}
/**
* 统计队列数量
*/
private function getQueueCount($taskId) {
$sql = "SELECT COUNT(*) as count FROM crawler_urls WHERE task_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('i', $taskId);
$stmt->execute();
$result = $stmt->get_result()->fetch_assoc();
return $result['count'];
}
/**
* 统计待处理任务数
*/
private function getPendingCount($taskId) {
$sql = "SELECT COUNT(*) as count FROM crawler_urls
WHERE task_id = ? AND status = 'pending'";
$stmt = $this->db->prepare($sql);
$stmt->bind_param('i', $taskId);
$stmt->execute();
$result = $stmt->get_result()->fetch_assoc();
return $result['count'];
}
/**
* 保存任务进度
*/
private function saveTaskProgress($taskId) {
// 记录当前进度
$pendingCount = $this->getPendingCount($taskId);
$totalCount = $this->getQueueCount($taskId);
$this->logger->info("Task {$taskId}: Saved progress. {$pendingCount}/{$totalCount} pending");
}
}
使用示例
基础使用
<?php
// 数据库连接
$db = new mysqli('localhost', 'username', 'password', 'database');
// 创建爬虫管理器
$manager = new CrawlerManager($db);
// 创建任务
$taskId = $manager->createTask([
'name' => 'Example Site Crawler',
'target_url' => 'https://example.com',
'max_pages' => 100,
'interval_time' => 2
]);
// 启动任务
$manager->startTask($taskId);
// 暂停任务
// $manager->pauseTask($taskId);
// 获取任务状态
$task = $manager->getTask($taskId);
echo $task['status'];
简单爬虫示例
<?php
require_once 'SimpleCrawler.php';
$crawler = new SimpleCrawler([
'userAgent' => 'MyCustomBot/1.0',
'timeout' => 10,
'proxy' => 'http://127.0.0.1:8080' // 可选
]);
// 登录后爬取
$loginData = [
'username' => 'user',
'password' => 'password'
];
$result = $crawler->post('https://example.com/login', $loginData);
if ($result['success']) {
// 登录成功后再爬取
$page = $crawler->get('https://example.com/protected-page');
if ($page['success']) {
echo $page['content'];
}
}
命令行管理工具
<?php
// crawler.php
class CrawlerCLI {
private $manager;
public function __construct($db) {
$this->manager = new CrawlerManager($db);
}
public function handle($command, $args) {
switch ($command) {
case 'start':
return $this->startCrawler($args[0]);
case 'stop':
return $this->stopCrawler($args[0]);
case 'status':
return $this->getStatus($args[0]);
case 'stats':
return $this->getStats();
case 'list':
return $this->listTasks();
default:
return $this->help();
}
}
private function startCrawler($taskId) {
$result = $this->manager->startTask($taskId);
return $result ? "Task started" : "Failed to start task";
}
private function stopCrawler($taskId) {
$result = $this->manager->pauseTask($taskId);
return $result ? "Task stopped" : "Failed to stop task";
}
private function getStatus($taskId) {
$task = $this->manager->getTask($taskId);
return json_encode($task);
}
private function getStats() {
// 获取统计数据
$stats = [
'total_tasks' => $this->countTasks(),
'running_tasks' => $this->countRunningTasks(),
'total_pages' => $this->countPages()
];
return json_encode($stats);
}
private function listTasks() {
$tasks = $this->getAllTasks();
return json_encode($tasks);
}
}
最佳实践建议
使用队列系统
// 使用Redis队列
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 添加URL
$redis->lpush('crawler_queue', $url);
// 获取URL
$url = $redis->rpop('crawler_queue');
使用框架或库
- Goutte - 基于Guzzle的Web爬虫框架
- Symfony DomCrawler - 灵活的DOM操作
- PHP Simple HTML DOM Parser - 简单的HTML解析
异常处理
try {
// 爬虫逻辑
} catch (ConnectException $e) {
// 连接错误
} catch (RequestException $e) {
// 请求错误
} catch (\Exception $e) {
// 其他错误
}
遵守robots.txt
// 检查robots.txt
$robots = file_get_contents('https://example.com/robots.txt');
if (strpos($robots, 'Disallow: /crawl') !== false) {
// 不爬取该路径
}
这个完整的爬虫管理系统包含了:
- 基础爬虫类
- 任务管理(创建、启动、暂停、恢复)
- 并发控制
- 代理支持
- Cookie管理
- 错误处理
- 数据库存储
- 命令行工具
你可以根据实际需求进行调整和扩展。