PHP 爬虫去重方案

wen PHP项目 2

本文目录导读:

PHP 爬虫去重方案

  1. 基于内存的去重(简单方案)
  2. 基于数据库的去重
  3. 基于布隆过滤器的去重(大规模场景)
  4. 文件系统去重(超大规模)
  5. 完整示例:综合去重方案
  6. 选择建议

在 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 做持久化存储
  • 定期清理过期数据
  • 增加合理的过期策略

这样既保证了性能,又确保了数据的安全性。

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