常見場景:內容采集、編輯器粘貼圖片自動化、鏡像站構建、圖片熱鏈接防護。核心需求:
<?php
/**
* 下載遠程圖片到本地
*
* @param string $url 遠程圖片 URL
* @param string $saveDir 本地保存根目錄(絕對路徑)
* @param string $fileName 自定義文件名(含擴展名),空則自動生成
* @param array $allowedTypes 允許的圖片類型
* @param int $timeout 連線超時秒數
* @param int $retries 重試次數
* @return array ['success' => bool, 'filePath' => string, 'url' => string, 'error' => string]
*/
function downloadImage($url, $saveDir, $fileName = '', $allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'], $timeout = 15, $retries = 2) {
// 1. 基礎校驗
if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
return ['success' => false, 'error' => '無效的 URL'];
}
// 2. 解析原文件名與擴展名
$parsedUrl = parse_url($url);
$originalName = basename($parsedUrl['path'] ?? '');
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
// 3. 類型校驗
if (!in_array($ext, $allowedTypes)) {
return ['success' => false, 'error' => "不支持的圖片類型: $ext"];
}
// 4. 確定保存文件名
if (empty($fileName)) {
$fileName = uniqid('img_', true) . '.' . $ext; // 唯一文件名避免衝突
} elseif (pathinfo($fileName, PATHINFO_EXTENSION) !== $ext) {
$fileName .= '.' . $ext; // 確保擴展名正確
}
// 5. 構建按日期分級的保存路徑:/saveDir/2024/05/20/
$dateDir = date('Y/m/d/');
$fullDir = rtrim($saveDir, '/') . '/' . $dateDir;
if (!is_dir($fullDir)) {
if (!mkdir($fullDir, 0755, true)) {
return ['success' => false, 'error' => "無法創建目錄: $fullDir"];
}
}
$filePath = $fullDir . $fileName;
// 6. 下載邏輯(支持重試)
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => $timeout,
CURLOPT_TIMEOUT => $timeout * 2,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_SSL_VERIFYPEER => false, // 生產環境建議 true 並配置 CA 憑證
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; PHP Image Downloader)',
]);
$content = false;
for ($i = 0; $i <= $retries; $i++) {
$content = curl_exec($ch);
if ($content !== false) break;
$err = curl_error($ch);
if ($i < $retries) sleep(1); // 重試間隔
}
curl_close($ch);
if ($content === false) {
return ['success' => false, 'error' => "下載失敗: $err"];
}
// 7. 內容類型二次校驗(防偽裝文件)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_buffer($finfo, $content);
finfo_close($finfo);
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mime, $allowedMimes)) {
return ['success' => false, 'error' => "內容非圖片: $mime"];
}
// 8. 保存文件
if (file_put_contents($filePath, $content) === false) {
return ['success' => false, 'error' => "文件寫入失敗: $filePath"];
}
// 9. 返回可訪問的相對路徑或 URL
$relativePath = $dateDir . $fileName;
return [
'success' => true,
'filePath' => $filePath,
'url' => '/uploads/' . $relativePath, // 假設 Web 根目錄映射
'mime' => $mime,
'size' => strlen($content),
];
}
// 使用示例
$result = downloadImage(
'https://example.com/photo.jpg',
'/var/www/html/uploads',
'', // 自動生成文件名
['jpg', 'png', 'webp'],
10,
3
);
if ($result['success']) {
echo "下載成功: {$result['url']}";
} else {
echo "失敗: {$result['error']}";
}
?>
finfo 檢測實際二進制內容,防止偽裝文件(如 .jpg 實為 .php)uniqid('img_', true) 生成微秒級唯一標識/uploads/ ├── 2024/ │ ├── 05/ │ │ ├── 20/ │ │ │ ├── img_664a1b2c3d4e5.jpg │ │ │ └── img_664a1b2c3d4f6.png │ │ └── 21/ │ └── 06/
優勢:單目錄文件數可控、便於備份清理、利於 CDN 緩存層級。
CURLOPT_FOLLOWLOCATION:自動跟隨 301/302 重定向CURLOPT_MAXREDIRS:防止重定向循環CURLOPT_USERAGENT:模擬瀏覽器,避免防盜鏈攔截CURLOPT_SSL_VERIFYPEER:生產環境必須啟用並配置 CA Bundle網路抖動、服務端暫時不可用時自動重試,提高成功率。
| 問題 | 原因 | 解決 |
|---|---|---|
| 下載大文件內存溢出 | CURLOPT_RETURNTRANSFER 將整個文件載入內存 | 改用寫入文件流:CURLOPT_FILE => fopen($filePath, 'w') |
| 防盜鏈 403 | Referer/User-Agent 檢查 | 設置 CURLOPT_REFERER、CURLOPT_USERAGENT、或使用代理 IP |
| HTTPS 證書驗證失敗 | 缺少 CA 憑證 | 設置 CURLOPT_CAINFO 指向 cacert.pem |
| 文件名亂碼 | URL 中文未編碼 | urldecode(basename(...)) 或強制生成唯一名 |
一個生產級的遠程圖片下載函數,遠不止 file_get_contents 那麼簡單。本文提供的增強版實現涵蓋了輸入校驗、類型雙重檢測、安全存儲路徑、網路容錯、內容驗證等關鍵環節,可直接用於項目或作為基礎進一步擴展。核心原則:不信任外部輸入、分層驗證、失敗可觀測、擴展性預留。