本文目录导读:

我来帮你搭建一个PHP简易爬虫框架,我们需要设计一个灵活且可扩展的架构。
项目结构
crawler/
├── src/
│ ├── Core/
│ │ ├── Crawler.php
│ │ ├── Request.php
│ │ └── Response.php
│ ├── Parser/
│ │ ├── ParserInterface.php
│ │ └── HTMLParser.php
│ ├── Storage/
│ │ ├── StorageInterface.php
│ │ └── DatabaseStorage.php
│ └── Utils/
│ └── Helper.php
├── config/
│ └── config.php
├── tests/
└── composer.json
核心类实现
Request.php
<?php
namespace Crawler\Core;
class Request {
private $url;
private $method;
private $headers = [];
private $body = [];
private $options = [];
private $timeout = 30;
private $maxRetries = 3;
private $userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36';
private $proxy = null;
public function __construct($url, $method = 'GET', $headers = [], $body = []) {
$this->url = $url;
$this->method = $method;
$this->headers = $headers;
$this->body = $body;
}
public function getUrl() { return $this->url; }
public function setUrl($url) { $this->url = $url; return $this; }
public function getMethod() { return $this->method; }
public function setMethod($method) { $this->method = $method; return $this; }
public function addHeader($key, $value) {
$this->headers[$key] = $value;
return $this;
}
public function getHeaders() { return $this->headers; }
public function setBody($data) {
$this->body = $data;
return $this;
}
public function getBody() { return $this->body; }
public function setTimeout($timeout) { $this->timeout = $timeout; return $this; }
public function getTimeout() { return $this->timeout; }
public function setUserAgent($ua) { $this->userAgent = $ua; return $this; }
public function getUserAgent() { return $this->userAgent; }
public function setProxy($proxy) { $this->proxy = $proxy; return $this; }
public function getProxy() { return $this->proxy; }
public function toCurlOptions() {
$curlOptions = [
CURLOPT_URL => $this->url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_USERAGENT => $this->userAgent,
CURLOPT_HEADER => false
];
// 设置请求头
if (!empty($this->headers)) {
$curlOptions[CURLOPT_HTTPHEADER] = $this->headers;
}
// 设置请求方法
switch (strtoupper($this->method)) {
case 'POST':
$curlOptions[CURLOPT_POST] = true;
$curlOptions[CURLOPT_POSTFIELDS] = $this->body;
break;
case 'PUT':
$curlOptions[CURLOPT_CUSTOMREQUEST] = 'PUT';
$curlOptions[CURLOPT_POSTFIELDS] = $this->body;
break;
case 'DELETE':
$curlOptions[CURLOPT_CUSTOMREQUEST] = 'DELETE';
break;
case 'HEAD':
$curlOptions[CURLOPT_NOBODY] = true;
break;
}
// 设置代理
if ($this->proxy) {
$curlOptions[CURLOPT_PROXY] = $this->proxy;
}
return $curlOptions;
}
}
Response.php
<?php
namespace Crawler\Core;
class Response {
private $statusCode;
private $headers = [];
private $body;
private $error;
private $executionTime;
private $finalUrl;
public function __construct($statusCode, $headers, $body, $error = null, $executionTime = 0, $finalUrl = null) {
$this->statusCode = $statusCode;
$this->headers = $headers;
$this->body = $body;
$this->error = $error;
$this->executionTime = $executionTime;
$this->finalUrl = $finalUrl;
}
public function getStatusCode() { return $this->statusCode; }
public function getHeaders() { return $this->headers; }
public function getBody() { return $this->body; }
public function getError() { return $this->error; }
public function getExecutionTime() { return $this->executionTime; }
public function getFinalUrl() { return $this->finalUrl; }
public function isSuccess() {
return $this->statusCode >= 200 && $this->statusCode < 300;
}
public function toArray() {
return [
'status_code' => $this->statusCode,
'headers' => $this->headers,
'body' => $this->body,
'error' => $this->error,
'execution_time' => $this->executionTime,
'final_url' => $this->finalUrl
];
}
}
Crawler.php
<?php
namespace Crawler\Core;
use Crawler\Parser\ParserInterface;
use Crawler\Storage\StorageInterface;
class Crawler {
protected $request;
protected $response;
protected $parser;
protected $storage;
protected $config = [];
// 爬取统计
protected $stats = [
'total_requests' => 0,
'total_pages' => 0,
'total_bytes' => 0,
'errors' => 0,
'start_time' => null,
'end_time' => null
];
// 回调函数
protected $callbacks = [
'onSuccess' => null,
'onError' => null,
'onParse' => null
];
public function __construct(array $config = []) {
$this->config = array_merge([
'max_retries' => 3,
'timeout' => 30,
'concurrency' => 1,
'use_random_user_agent' => false,
'respect_robots_txt' => true,
'user_agents' => $this->getDefaultUserAgents()
], $config);
$this->init();
}
protected function init() {
// 可以在这里初始化数据库连接等
}
public function setRequest(Request $request) {
$this->request = $request;
return $this;
}
public function setParser(ParserInterface $parser) {
$this->parser = $parser;
return $this;
}
public function setStorage(StorageInterface $storage) {
$this->storage = $storage;
return $this;
}
public function setCallback($event, $callback) {
$this->callbacks[$event] = $callback;
return $this;
}
public function fetch($url) {
$this->stats['start_time'] = microtime(true);
try {
$this->makeRequest($url);
if ($this->response->isSuccess()) {
$this->stats['total_pages']++;
$this->handleSuccess();
} else {
$this->stats['errors']++;
$this->handleError();
}
return $this->response;
} finally {
$this->stats['end_time'] = microtime(true);
}
}
protected function makeRequest($url) {
$startTime = microtime(true);
$retries = 0;
do {
$ch = curl_init();
$request = new Request($url);
if (!empty($this->request->getHeaders())) {
foreach ($this->request->getHeaders() as $key => $value) {
$request->addHeader($key, $value);
}
}
// 随机User-Agent
if ($this->config['use_random_user_agent']) {
$randomUA = $this->config['user_agents'][array_rand($this->config['user_agents'])];
$request->setUserAgent($randomUA);
} else {
$request->setUserAgent($this->request->getUserAgent());
}
$options = $request->toCurlOptions();
curl_setopt_array($ch, $options);
// 发送请求
$body = curl_exec($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($error) {
$this->showMessage("Request error: " . $error, 'error');
$retries++;
if ($retries >= $this->config['max_retries']) {
break;
}
sleep(1); // 等待1秒后重试
continue;
}
break;
} while (true);
$executionTime = microtime(true) - $startTime;
$this->response = new Response(
isset($info['http_code']) ? $info['http_code'] : 0,
$this->parseHeaders($info),
$body,
$error,
$executionTime,
isset($info['url']) ? $info['url'] : $url
);
$this->stats['total_requests']++;
$this->stats['total_bytes'] += strlen($body);
}
protected function handleSuccess() {
// 输出响应信息
$this->showMessage("获取成功: " . $this->response->getFinalUrl(), 'success');
// 执行解析
if ($this->parser !== null) {
$data = $this->parser->parse($this->response->getBody());
if ($this->callbacks['onParse']) {
$data = call_user_func($this->callbacks['onParse'], $data, $this->response);
}
// 存储数据
if ($this->storage !== null && $data) {
$this->storage->save($data);
}
}
// 执行成功回调
if ($this->callbacks['onSuccess']) {
call_user_func($this->callbacks['onSuccess'], $this->response);
}
}
protected function handleError() {
$this->showMessage("请求失败,状态码: " . $this->response->getStatusCode(), 'error');
if ($this->callbacks['onError']) {
call_user_func($this->callbacks['onError'], $this->response);
}
}
protected function parseHeaders($info) {
// 这个方法需要从curl响应中提取headers
// 简化实现,实际可以从$info获取
return [];
}
public function getStats() {
$this->stats['duration'] = $this->stats['end_time'] - $this->stats['start_time'];
return $this->stats;
}
protected function showMessage($message, $type = 'info') {
if (PHP_SAPI === 'cli') {
$colors = [
'success' => "\033[32m",
'error' => "\033[31m",
'info' => "\033[33m",
'reset' => "\033[0m"
];
echo $colors[$type] . $message . $colors['reset'] . PHP_EOL;
} else {
echo $message . "<br>";
}
}
protected function getDefaultUserAgents() {
return [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15'
];
}
}
解析器接口和实现
ParserInterface.php
<?php
namespace Crawler\Parser;
interface ParserInterface {
public function parse($html);
}
HTMLParser.php
<?php
namespace Crawler\Parser;
use DOMDocument;
use DOMXPath;
class HTMLParser implements ParserInterface {
protected $xpath = null;
protected $config = [];
public function __construct($config = []) {
$this->config = array_merge([
'selectors' => [],
'remove_tags' => [],
'encoding' => 'UTF-8'
], $config);
}
public function parse($html) {
$document = new DOMDocument();
libxml_use_internal_errors(true);
@$document->loadHTML('<?xml encoding="' . $this->config['encoding'] . '">' . $html);
libxml_clear_errors();
$this->xpath = new DOMXPath($document);
$result = [];
foreach ($this->config['selectors'] as $name => $selector) {
$result[$name] = $this->extractBySelector($selector);
}
return $result;
}
protected function extractBySelector($selector) {
// 支持简单的CSS选择器转XPath
$xpath = $this->cssToXPath($selector['selector']);
if (isset($selector['attribute'])) {
return $this->getAttribute($xpath, $selector['attribute']);
} else {
return $this->getText($xpath);
}
}
protected function cssToXPath($css) {
// 简单实现,可根据需要扩展
// 这里只处理最基本的后代选择器
$parts = explode(' ', trim($css));
$xpath = '';
foreach ($parts as $part) {
$xpath .= '/' . $part;
}
return $xpath;
}
protected function getText($xpath) {
$nodes = $this->xpath->query($xpath);
$texts = [];
foreach ($nodes as $node) {
if ($node->nodeType === XML_TEXT_NODE) {
$texts[] = trim($node->nodeValue);
} else {
$texts[] = trim($node->textContent);
}
}
return $texts;
}
protected function getAttribute($xpath, $attribute) {
$nodes = $this->xpath->query($xpath);
$values = [];
foreach ($nodes as $node) {
if ($node->nodeType === XML_ELEMENT_NODE) {
$values[] = $node->getAttribute($attribute);
}
}
return $values;
}
}
存储接口和实现
StorageInterface.php
<?php
namespace Crawler\Storage;
interface StorageInterface {
public function save(array $data);
public function get();
public function clear();
public function count();
}
DatabaseStorage.php
<?php
namespace Crawler\Storage;
class DatabaseStorage implements StorageInterface {
protected $pdo;
protected $table;
public function __construct($host, $dbname, $user, $password, $table) {
try {
$this->pdo = new \PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$user,
$password,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
);
$this->table = $table;
} catch (\PDOException $e) {
throw new \Exception("数据库连接失败: " . $e->getMessage());
}
}
public function save(array $data) {
$columns = array_keys($data);
$placeholders = array_fill(0, count($columns), '?');
$sql = "INSERT INTO " . $this->table . " (" . implode(', ', $columns) . ")
VALUES (" . implode(', ', $placeholders) . ")";
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute(array_values($data));
return $this->pdo->lastInsertId();
} catch (\PDOException $e) {
throw new \Exception("数据存储失败: " . $e->getMessage());
}
}
public function get() {
$stmt = $this->pdo->query("SELECT * FROM " . $this->table);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
public function clear() {
$this->pdo->query("DELETE FROM " . $this->table);
return true;
}
public function count() {
$stmt = $this->pdo->query("SELECT COUNT(*) FROM " . $this->table);
return $stmt->fetchColumn();
}
}
配置文件
config.php
<?php
return [
'settings' => [
'max_retries' => 3,
'timeout' => 30,
'concurrency' => 1,
'use_random_user_agent' => true,
'respect_robots_txt' => true
],
'database' => [
'host' => 'localhost',
'dbname' => 'crawler',
'user' => 'root',
'password' => '',
'table' => 'crawled_data'
],
'output' => [
'format' => 'json', // json, csv, database
'path' => __DIR__ . '/data/'
],
'selectors' => [
'title' => [
'selector' => 'h1',
'attribute' => null // null表示获取文本
],
'links' => [
'selector' => 'a',
'attribute' => 'href'
],
'content' => [
'selector' => '.content',
'attribute' => null
]
]
];
使用示例
基本使用 (examples/basic.php)
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Crawler\Core\Crawler;
use Crawler\Core\Request;
use Crawler\Parser\HTMLParser;
use Crawler\Storage\DatabaseStorage;
// 加载配置
$config = require __DIR__ . '/../config/config.php';
// 创建爬虫实例
$crawler = new Crawler($config['settings']);
// 设置请求
$request = new Request('https://example.com');
$request->setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
$crawler->setRequest($request);
// 设置解析器
$parser = new HTMLParser($config['selectors']);
$crawler->setParser($parser);
// 设置存储
$storage = new DatabaseStorage(
$config['database']['host'],
$config['database']['dbname'],
$config['database']['user'],
$config['database']['password'],
$config['database']['table']
);
$crawler->setStorage($storage);
// 设置回调
$crawler->setCallback('onSuccess', function($response) {
echo "成功获取页面: " . $response->getUrl() . " (耗时: " . $response->getExecutionTime() . "s)\n";
});
$crawler->setCallback('onError', function($response) {
echo "页面获取失败: " . $response->getUrl() . " (错误: " . $response->getError() . ")\n";
});
// 执行爬取
$response = $crawler->fetch('https://example.com');
// 获取统计信息
$stats = $crawler->getStats();
print_r($stats);
Composer 配置文件
composer.json
{
"name": "mycrawler/mycrawler",
"description": "PHP微爬虫框架",
"type": "library",
"require": {
"php": ">=7.4",
"ext-curl": "*",
"ext-dom": "*",
"ext-pdo": "*"
},
"autoload": {
"psr-4": {
"Crawler\\": "src/"
}
},
"config": {
"optimize-autoloader": true
}
}
高级功能
多线程爬取 (ConcurrentCrawler)
<?php
namespace Crawler\Core;
use parallel\Runtime;
class ConcurrentCrawler {
protected $config;
protected $crawlers = [];
public function __construct($config = []) {
$this->config = array_merge([
'max_crawlers' => 5,
'max_requests_per_crawler' => 100
], $config);
}
public function crawl($urls) {
$chunks = array_chunk($urls, ceil(count($urls) / $this->config['max_crawlers']));
foreach ($chunks as $chunk) {
$runtime = new Runtime();
$this->crawlers[] = $runtime->run(function($urls, $config) {
// 每个爬虫实例的代码
require_once 'path/to/vendor/autoload.php';
$crawler = new Crawler($config);
foreach ($urls as $url) {
$crawler->fetch($url);
}
}, [$chunk, $this->config]);
}
// 等待所有爬虫完成
foreach ($this->crawlers as $crawler) {
$crawler->join();
}
}
}
测试文件
tests/UnitTest.php
<?php
use PHPUnit\Framework\TestCase;
use Crawler\Core\Crawler;
use Crawler\Core\Request;
class CrawlerTest extends TestCase {
public function testFetchSuccess() {
$crawler = new Crawler(['max_retries' => 3]);
$request = new Request('https://example.com');
$crawler->setRequest($request);
$response = $crawler->fetch('https://example.com');
$this->assertTrue($response->isSuccess());
$this->assertNotEmpty($response->getBody());
}
public function testFetchFailure() {
$crawler = new Crawler(['max_retries' => 1]);
$response = $crawler->fetch('https://invalid.example.com');
$this->assertNotTrue($response->isSuccess());
}
}
运行命令
# 生成自动加载文件 composer dump-autoload # 运行测试 vendor/bin/phpunit tests/ # 运行基本示例 php examples/basic.php
扩展建议
- 添加更多解析器:支持CSS选择器、XPath、JSONPath等
- 缓存功能:使用Redis/Memcached缓存已爬取的数据
- 断点续爬:记录爬取状态,支持中断后继续
- 数据清洗:添加数据过滤、去重、清洗功能
- 反爬处理:支持IP池、代理池、验证码识别等
- 队列系统:集成Redis队列实现任务分发
- 监控日志:添加详细的日志和监控功能
这个框架设计灵活,你可以根据实际需求进行扩展和调整,核心的Crawler类已经实现了基本功能,你可以在此基础上添加更多高级特性。