首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 圖片採集按原路徑保存:目錄自動創建、文件名衝突處理、大文件流式下載

     編輯:PHP 相關知識

採集圖片時保持原站目錄結構,便於本地鏡像、CDN 回源、離線瀏覽。本文提供生產級實現:目錄遞歸創建、流式下載避免內存溢出、文件名衝突去重、斷點續傳支持。

核心需求分析

  • 輸入:圖片完整 URL (如 https://cdn.example.com/images/2024/01/photo.jpg)
  • 輸出:本地保存 ./images/2024/01/photo.jpg (保持相對路徑)
  • 挑戰:深層目錄創建、大文件內存、同名覆蓋、網絡不穩、防盜鏈 Referer

基礎版:file_get_contents + 目錄創建

function saveImageBasic(string $url, string $baseDir = '.'): ?string {
    // 解析路徑
    $parsed = parse_url($url);
    $path = $parsed['path'] ?? ''; // /images/2024/01/photo.jpg
    $path = ltrim($path, '/');
    
    $savePath = $baseDir . '/' . $path;
    $dir = dirname($savePath);
    
    // 遞歸創建目錄 (0755 權限)
    if (!is_dir($dir)) {
        if (!mkdir($dir, 0755, true)) {
            return null; // 創建失敗
        }
        // 可選:防止目錄遍歷
        file_put_contents($dir . '/index.html', '');
    }
    
    // 下載並保存
    $content = @file_get_contents($url);
    if ($content === false) {
        return null;
    }
    
    file_put_contents($savePath, $content);
    return $savePath;
}

進階版:cURL 流式下載(大文件、超時控制、防盜鏈)

function saveImageStream(string $url, string $baseDir = '.', array $options = []): ?string {
    $parsed = parse_url($url);
    $path = ltrim($parsed['path'] ?? '', '/');
    $savePath = $baseDir . '/' . $path;
    $dir = dirname($savePath);
    
    if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
        return null;
    }
    
    // 文件名衝突處理:同名添加序號
    $finalPath = $savePath;
    $counter = 1;
    $info = pathinfo($savePath);
    while (file_exists($finalPath)) {
        $finalPath = $info['dirname'] . '/' . $info['filename'] . "_$counter." . $info['extension'];
        $counter++;
    }
    
    $fp = fopen($finalPath, 'wb');
    if (!$fp) return null;
    
    $ch = curl_init($url);
    $defaultOpts = [
        CURLOPT_FILE => $fp,                    // 直接寫入文件句柄
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 5,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 60,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; ImageCollector/1.0)',
        CURLOPT_REFERER => $parsed['scheme'] . '://' . $parsed['host'] . '/', // 簡單防盜鏈
        CURLOPT_LOW_SPEED_LIMIT => 1024,        // 1 KB/s
        CURLOPT_LOW_SPEED_TIME => 30,           // 30 秒低速則中斷
    ];
    curl_setopt_array($ch, array_merge($defaultOpts, $options));
    
    $result = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);
    fclose($fp);
    
    if ($result === false || $httpCode >= 400) {
        @unlink($finalPath); // 刪除失敗文件
        error_log("Image download failed: $url - HTTP $httpCode - $error");
        return null;
    }
    
    return $finalPath;
}

企業級增強:斷點續傳 + 並發控制 + 完整性校驗

class ImageCollector {
    private $baseDir;
    private $maxConcurrency;
    private $tempSuffix = '.downloading';
    
    public function __construct(string $baseDir, int $maxConcurrency = 5) {
        $this->baseDir = rtrim($baseDir, '/');
        $this->maxConcurrency = $maxConcurrency;
    }
    
    public function collect(array $urls): array {
        $results = [];
        $pool = new \CurlMultiPool($this->maxConcurrency);
        
        foreach ($urls as $url) {
            $pool->addTask($url, [$this, 'downloadTask']);
        }
        
        while ($pool->hasPending()) {
            $completed = $pool->wait();
            foreach ($completed as $result) {
                $results[] = $result;
            }
        }
        return $results;
    }
    
    private function downloadTask(string $url): array {
        $parsed = parse_url($url);
        $path = ltrim($parsed['path'] ?? '', '/');
        $savePath = $this->baseDir . '/' . $path;
        $tempPath = $savePath . $this->tempSuffix;
        $dir = dirname($savePath);
        
        if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
            return ['url' => $url, 'status' => 'dir_failed'];
        }
        
        // 斷點續傳:檢查臨時文件大小,設置 Range 請求頭
        $resumeHeader = [];
        if (file_exists($tempPath)) {
            $size = filesize($tempPath);
            if ($size > 0) {
                $resumeHeader[] = "Range: bytes=$size-";
            }
        }
        
        $fp = fopen($tempPath, 'ab'); // 追加模式
        if (!$fp) return ['url' => $url, 'status' => 'file_open_failed'];
        
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_FILE => $fp,
            CURLOPT_HTTPHEADER => $resumeHeader,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 120,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_USERAGENT => 'ImageCollector/2.0',
            CURLOPT_REFERER => $parsed['scheme'] . '://' . $parsed['host'] . '/',
            CURLOPT_WRITEFUNCTION => function($ch, $data) use ($fp) {
                return fwrite($fp, $data); // 手動寫入可獲取進度
            },
        ]);
        
        $result = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);
        fclose($fp);
        
        if ($result === false) {
            return ['url' => $url, 'status' => 'curl_failed', 'error' => $error];
        }
        
        if ($httpCode === 200 || $httpCode === 206) { // 206 Partial Content
            // 可選:校驗 Content-Length / MD5
            rename($tempPath, $savePath);
            return ['url' => $url, 'status' => 'success', 'path' => $savePath];
        }
        
        return ['url' => $url, 'status' => 'http_error', 'code' => $httpCode];
    }
}

// 簡易 CurlMultiPool 實現(生產建議用 amphp/http-client 或 guzzle/guzzle)
class CurlMultiPool { /* ... 省略 ... */ }

關鍵細節對比表

特性基礎版流式版企業級
內存佔用全文件載入內存流式寫入,常數內存流式 + 並發池控制
大文件支持❌ 易 OOM✅✅ + 斷點續傳
網絡異常處理簡單失敗超時/低速中斷重試/斷點續傳/熔斷
文件名衝突覆蓋序號遞增序號/哈希/時間戳
完整性校驗無可選Content-Length/MD5/CRC32
防盜鏈無Referer 模擬動態 Referer/Cookie/Token
並發控制串行串行連接池限流

部署建議

  1. 權限隔離: 存儲目錄不可執行 PHP,Nginx location ~ \.php$ { deny all; }
  2. 磁盤配額: 監控剩餘空間,達閾值暫停採集
  3. 去重索引: 數據庫記錄 url_hash (md5) -> local_path,避免重複下載
  4. CDN 回源: 本地路徑映射為 CDN 域名,配置回源至採集服務器
  5. 合規性: 遵守 robots.txt、版權授權、GDPR/個信法,不採集私密/受保護內容

從簡單的 file_get_contents 到企業級採集系統,核心演進路徑:**流式下載 → 斷點續傳 → 並發控制 → 合規治理**。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved