本文目录导读:

在 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 做持久化存储
- 定期清理过期数据
- 增加合理的过期策略
这样既保证了性能,又确保了数据的安全性。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播