本文目录导读:

在 PHP 爬虫开发中,去重是核心需求之一,以下是几种常用的去重方案,从简单到复杂,您可以根据项目规模选择:
基于内存的去重(简单方案)
使用数组去重
class SimpleSpider {
private $visited = [];
public function crawl($url) {
// 去重检查
if (isset($this->visited[$url])) {
return false;
}
// 标记已访问
$this->visited[$url] = true;
// 继续爬取逻辑
return $this->fetchPage($url);
}
}
使用哈希函数
class HashSpider {
private $visited = [];
public function isVisited($url) {
// 使用URL的MD5值作为唯一标识
$hash = md5($url);
return isset($this->visited[$hash]);
}
public function markVisited($url) {
$this->visited[md5($url)] = true;
}
}
基于数据库的去重
MySQL 去重
class MySQLSpider {
private $pdo;
public function __construct() {
$this->pdo = new PDO('mysql:host=localhost;dbname=spider', 'user', 'pass');
$this->createTable();
}
private function createTable() {
$sql = "CREATE TABLE IF NOT EXISTS visited_urls (
id INT AUTO_INCREMENT PRIMARY KEY,
url_hash VARCHAR(32) UNIQUE,
url TEXT,
visited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (url_hash)
)";
$this->pdo->exec($sql);
}
public function isVisited($url) {
$stmt = $this->pdo->prepare("SELECT COUNT(*) FROM visited_urls WHERE url_hash = ?");
$stmt->execute([md5($url)]);
return $stmt->fetchColumn() > 0;
}
public function markVisited($url) {
$stmt = $this->pdo->prepare("INSERT INTO visited_urls (url_hash, url) VALUES (?, ?)");
$stmt->execute([md5($url), $url]);
}
}
Redis 去重(高并发场景)
class RedisSpider {
private $redis;
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function isVisited($url) {
// 使用SET数据结构存储所有访问过的URL
return $this->redis->sIsMember('spider:visited', $url);
}
public function markVisited($url) {
$this->redis->sAdd('spider:visited', $url);
// 设置过期时间(可选)
if ($this->redis->sCard('spider:visited') == 1) {
$this->redis->expire('spider:visited', 86400); // 24小时
}
}
// 批量检查
public function filterNewUrls($urls) {
$newUrls = [];
foreach ($urls as $url) {
if (!$this->isVisited($url)) {
$newUrls[] = $url;
$this->markVisited($url);
}
}
return $newUrls;
}
}
基于布隆过滤器的去重(大规模场景)
使用 PHP Redis 的布隆过滤器
class BloomFilterSpider {
private $redis;
private $key = 'spider:bloom';
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function init() {
// 初始化布隆过滤器(可能需要安装RedisBloom模块)
$this->redis->rawCommand('BF.RESERVE', $this->key, 0.001, 100000);
}
public function isVisited($url) {
return $this->redis->rawCommand('BF.EXISTS', $this->key, $url) === 1;
}
public function markVisited($url) {
$this->redis->rawCommand('BF.ADD', $this->key, $url);
}
}
自实现布隆过滤器
class SimpleBloomFilter {
private $bitArray;
private $size;
private $hashFunctions;
public function __construct($size = 100000, $hashFunctions = 3) {
$this->size = $size;
$this->hashFunctions = $hashFunctions;
$this->bitArray = array_fill(0, $size, 0);
}
private function hash($str, $seed) {
$hash = crc32($seed . $str);
return $hash % $this->size;
}
public function add($url) {
for ($i = 0; $i < $this->hashFunctions; $i++) {
$index = $this->hash($url, "seed_" . $i);
$this->bitArray[$index] = 1;
}
}
public function mightContain($url) {
for ($i = 0; $i < $this->hashFunctions; $i++) {
$index = $this->hash($url, "seed_" . $i);
if ($this->bitArray[$index] == 0) {
return false;
}
}
return true;
}
}
文件系统去重(超大规模)
class FileBasedSpider {
private $dataDir = '/tmp/spider_data/';
public function __construct() {
if (!file_exists($this->dataDir)) {
mkdir($this->dataDir, 0777, true);
}
}
public function isVisited($url) {
$hash = md5($url);
$file = $this->dataDir . substr($hash, 0, 2) . '/' . $hash;
return file_exists($file);
}
public function markVisited($url) {
$hash = md5($url);
$dir = $this->dataDir . substr($hash, 0, 2);
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents($dir . '/' . $hash, time());
}
// 定期清理过期数据
public function cleanUp($olderThan = 86400) {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->dataDir),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $file) {
if ($file->isFile()) {
$age = time() - $file->getMTime();
if ($age > $olderThan) {
unlink($file->getPathname());
}
}
}
}
}
完整示例:综合去重方案
class AdvancedSpider {
private $redis;
private $pdo;
private $bloomFilter;
private $queue;
public function __construct() {
// 初始化Redis(用于缓存去重)
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
// 初始化MySQL(持久化存储)
$this->pdo = new PDO('mysql:host=localhost;dbname=spider', 'user', 'pass');
// 初始化布隆过滤器
$this->bloomFilter = new SimpleBloomFilter(1000000, 5);
// URL队列
$this->queue = new SplQueue();
}
public function processUrl($url) {
// 1. 快速检查(内存缓存)
if ($this->isVisitedFast($url)) {
return false;
}
// 2. 慢速检查(数据库)
if ($this->isVisitedDB($url)) {
// 标记到内存缓存
$this->markVisitedFast($url);
return false;
}
// 3. 标记为已访问
$this->markVisited($url);
// 4. 获取页面内容
$content = file_get_contents($url);
// 5. 提取新链接
$newUrls = $this->extractLinks($content, $url);
// 6. 将新链接加入队列
foreach ($newUrls as $newUrl) {
$this->queue->enqueue($newUrl);
}
return true;
}
private function isVisitedFast($url) {
// 先检查Redis缓存
if ($this->redis->sIsMember('spider:visited', $url)) {
return true;
}
// 再检查布隆过滤器
return $this->bloomFilter->mightContain($url);
}
private function isVisitedDB($url) {
$stmt = $this->pdo->prepare("SELECT COUNT(*) FROM urls WHERE url_hash = ?");
$stmt->execute([md5($url)]);
return $stmt->fetchColumn() > 0;
}
private function markVisitedFast($url) {
$this->redis->sAdd('spider:visited', $url);
$this->bloomFilter->add($url);
}
private function markVisited($url) {
// 标记内存
$this->markVisitedFast($url);
// 持久化到数据库
$stmt = $this->pdo->prepare("INSERT INTO urls (url_hash, url) VALUES (?, ?)");
$stmt->execute([md5($url), $url]);
}
public function run($startUrl) {
$this->queue->enqueue($startUrl);
while (!$this->queue->isEmpty()) {
$url = $this->queue->dequeue();
echo "Processing: $url\n";
$this->processUrl($url);
}
}
}
选择建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 数组/哈希 | 简单快速 | 内存有限 | 小型爬虫 |
| MySQL | 持久化好 | 性能瓶颈 | 中小型项目 |
| Redis | 高性能 | 需要Redis服务 | 高并发爬虫 |
| 布隆过滤器 | 内存占用小 | 可能有误判 | 海量URL去重 |
| 文件系统 | 简单可靠 | 访问慢 | 超大规模 |
实际生产环境中,通常会结合多种方案:
- 使用 Redis + Bloom Filter 做快速判断
- 使用 MySQL 做持久化存储
- 定期清理过期数据
- 增加合理的过期策略
这样既保证了性能,又确保了数据的安全性。