專案常需從外部來源抓取圖片存至本地伺服器(如內容采集、用戶頭像遷移、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); // 禮貌延遲
}
// 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 回源配置、圖片壓縮/轉檔管線(上傳後自動處理)。