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

PHP 取得遠端圖片並儲存本地完整實作:支援多格式、錯誤處理與效能優化

     編輯:PHP 相關知識

前言

專案常需從外部來源抓取圖片存至本地伺服器(如內容采集、用戶頭像遷移、CDN 回源)。本文提供健壯的 GrabImage 函式,支援多種圖片格式、自動副檔名、大小限制、錯誤處理與串流寫入避免記憶體溢位。

核心函式(重構版)

<?php
/**
 * 取得遠端圖片並儲存至本地
 *
 * @param string $url       遠端圖片完整 URL
 * @param string $saveDir   儲存目錄(預設為當前目錄下的 images/)
 * @param array  $options   選項:['maxSize' => 5*1024*1024, 'allowedTypes' => ['jpg','png','gif','webp'], 'filename' => '']
 * @return array ['success' => bool, 'path' => string, 'url' => string, 'error' => string, 'info' => array]
 */
function GrabImage(string $url, string $saveDir = '', array $options = []): array
{
    $opts = array_merge([
        'maxSize'      => 10 * 1024 * 1024, // 10MB
        'allowedTypes' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'],
        'filename'     => '',               // 空則自動產生
        'timeout'      => 30,
        'overwrite'    => false,
    ], $options);
    
    $saveDir = $saveDir ?: __DIR__ . '/images';
    if (!is_dir($saveDir)) {
        mkdir($saveDir, 0755, true);
    }
    
    // 1. 驗證 URL
    if (!filter_var($url, FILTER_VALIDATE_URL)) {
        return ['success' => false, 'error' => '無效的 URL 格式'];
    }
    
    // 2. 發送 HEAD 請求先檢查 Content-Type 與 Content-Length
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_NOBODY       => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HEADER       => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT      => 10,
        CURLOPT_USERAGENT    => 'Mozilla/5.0 (compatible; PHP Image Grabber/1.0)',
        CURLOPT_SSL_VERIFYPEER => false,
    ]);
    $head = curl_exec($ch);
    $info = curl_getinfo($ch);
    curl_close($ch);
    
    if ($info['http_code'] < 200 || $info['http_code'] >= 400) {
        return ['success' => false, 'error' => "HTTP 錯誤: {$info['http_code']}"];
    }
    
    // 解析 Content-Type
    $contentType = '';
    if (preg_match('/Content-Type:\s*([^\r\n]+)/i', $head, $m)) {
        $contentType = strtolower(trim($m[1]));
    }
    $extFromHeader = '';
    if ($contentType) {
        $map = [
            'image/jpeg' => 'jpg', 'image/jpg' => 'jpg',
            'image/png'  => 'png', 'image/gif'  => 'gif',
            'image/webp' => 'webp', 'image/bmp' => 'bmp',
        ];
        $extFromHeader = $map[$contentType] ?? '';
    }
    
    // 檢查檔案大小
    if ($info['download_content_length'] > 0 && $info['download_content_length'] > $opts['maxSize']) {
        return ['success' => false, 'error' => '檔案過大: ' . formatBytes($info['download_content_length'])];
    }
    
    // 3. 決定副檔名
    $ext = $extFromHeader;
    if (!$ext) {
        // 從 URL 路徑取得
        $pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
        $ext = strtolower($pathInfo['extension'] ?? '');
    }
    if (!$ext || !in_array($ext, $opts['allowedTypes'], true)) {
        return ['success' => false, 'error' => '不支援的圖片格式: ' . ($ext ?: 'unknown')];
    }
    // 統一 jpeg -> jpg
    if ($ext === 'jpeg') $ext = 'jpg';
    
    // 4. 產生檔名
    $filename = $opts['filename'] ?: 'img_' . date('YmdHis') . '_' . mt_rand(1000, 9999) . '.' . $ext;
    $filename = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename); // 過濾危險字元
    $filepath = $saveDir . '/' . $filename;
    
    if (is_file($filepath) && !$opts['overwrite']) {
        // 避免覆蓋:加入序號
        $base = pathinfo($filename, PATHINFO_FILENAME);
        $i = 1;
        while (is_file($saveDir . '/' . $base . '_' . $i . '.' . $ext)) $i++;
        $filename = $base . '_' . $i . '.' . $ext;
        $filepath = $saveDir . '/' . $filename;
    }
    
    // 5. 串流下載寫入(避免大檔佔用記憶體)
    $fp = fopen($filepath, 'wb');
    if (!$fp) {
        return ['success' => false, 'error' => '無法建立檔案: 權限不足'];
    }
    
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_FILE           => $fp,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_TIMEOUT        => $opts['timeout'],
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; PHP Image Grabber/1.0)',
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_LOW_SPEED_LIMIT => 1024,      // 低速限制
        CURLOPT_LOW_SPEED_TIME => 30,         // 30 秒低於 1KB/s 中斷
    ]);
    
    $result = curl_exec($ch);
    $errno = curl_errno($ch);
    $error = curl_error($ch);
    $dlInfo = curl_getinfo($ch);
    curl_close($ch);
    fclose($fp);
    
    if ($errno || $dlInfo['http_code'] < 200 || $dlInfo['http_code'] >= 400) {
        @unlink($filepath); // 清理失敗檔案
        return ['success' => false, 'error' => "下載失敗: $error (HTTP {$dlInfo['http_code']})"];
    }
    
    // 6. 驗證檔案完整性(可選:用 getimagesize 檢查是否為有效圖片)
    $imgInfo = @getimagesize($filepath);
    if (!$imgInfo) {
        @unlink($filepath);
        return ['success' => false, 'error' => '下載檔案非有效圖片'];
    }
    
    // 7. 回傳結果
    return [
        'success' => true,
        'path'    => $filepath,
        'url'     => '/images/' . $filename, // 相對 Web 路徑,視專案調整
        'info'    => [
            'width'       => $imgInfo[0],
            'height'      => $imgInfo[1],
            'mime'        => $imgInfo['mime'] ?? $contentType,
            'size'        => filesize($filepath),
            'extension'   => $ext,
        ]
    ];
}

function formatBytes(int $bytes): string
{
    $units = ['B', 'KB', 'MB', 'GB'];
    $i = 0;
    while ($bytes >= 1024 && $i < count($units) - 1) {
        $bytes /= 1024;
        $i++;
    }
    return round($bytes, 2) . ' ' . $units[$i];
}

使用範例

<?php
require_once 'GrabImage.php';

// 單張下載
$result = GrabImage('https://example.com/photo.jpg', __DIR__ . '/uploads', [
    'maxSize' => 5 * 1024 * 1024,
    'filename' => 'avatar.jpg'
]);

if ($result['success']) {
    echo "下載成功: {$result['path']} ({$result['info']['width']}x{$result['info']['height']}, {$result['info']['size']} bytes)\n";
    // 寫入資料庫記錄
    // $pdo->prepare('INSERT INTO images (path, width, height, size) VALUES (?,?,?,?)')
    //     ->execute([$result['url'], $result['info']['width'], $result['info']['height'], $result['info']['size']]);
} else {
    echo "失敗: {$result['error']}\n";
}

// 批次下載(配合佇列非同步處理)
$urls = [
    'https://site.com/a.jpg',
    'https://site.com/b.png',
    // ...
];

foreach ($urls as $url) {
    $result = GrabImage($url, __DIR__ . '/batch');
    // 記錄結果...
    usleep(500000); // 禮貌延遲
}

進階:非同步併發下載(Guzzle + Promise)


// composer require guzzlehttp/guzzle
use GuzzleHttp\Client;
use GuzzleHttp\Promise\Utils;

$client = new Client(['timeout' => 30]);
$promises = [];

foreach ($urls as $i => $url) {
    $promises[$i] = $client->getAsync($url, ['sink' => __DIR__ . "/async/img_{$i}.tmp"])
        ->then(
            function ($response) use ($i) { echo "[$i] 成功\n"; },
            function ($reason) use ($i) { echo "[$i] 失敗: {$reason->getMessage()}\n"; }
        );
}

// 等待全部完成(可設定 concurrency 限制併發數)
Utils::settle($promises)->wait();

常見問題與解決

問題原因解決
下載檔案 0 bytes遠端防盜鏈、需 Referer、Cookie設定 CURLOPT_REFERER、CURLOPT_COOKIEJAR
SSL 憑證驗證失敗自簽憑證、憑證鏈不全CURLOPT_SSL_VERIFYPEER=false (僅測試) 或配置 CA 憑證
記憶體耗盡大檔案用 file_get_contents/readfile 全讀入記憶體改用 CURLOPT_FILE 串流寫入檔案
檔名中文亂碼URL 編碼、檔案系統編碼不一用 rawurldecode() 解碼、或強制轉 ASCII 檔名
下載圖片損壞重導向至錯誤頁、驗證碼頁面驗證 Content-Type、getimagesize() 檢查

總結

健壯的遠端圖片抓取需涵蓋:URL 驗證 → HEAD 預檢 → 串流下載 → 完整性驗證 → 錯誤清理。生產環境建議加入:快取機制(避免重複下載)、非同步佇列(大量下載)、CDN 回源配置、圖片壓縮/轉檔管線(上傳後自動處理)。

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