驗證遠端資源存在性是常見需求:圖片防盜鏈檢查、連結有效性掃描、CDN 快取預熱。本文比較 5 種方法 (cURL HEAD、cURL GET Range、get_headers、fopen、fsockopen) 的效能、可靠度、適用場景,並提供最佳實踐封裝。
| 方法 | 原理 | 下載 Body | 支援 HTTPS | 可取得 Header | 效能 | 可靠度 | 適用場景 |
|---|---|---|---|---|---|---|---|
| cURL HEAD | 發送 HEAD 請求 | ❌ 否 | ✅ 是 | ✅ 完整 | ⚡ 最快 | ⭐⭐⭐⭐⭐ | 首選:大多數情況 |
| cURL GET Range | GET + Range: bytes=0-0 | 1 byte | ✅ 是 | ✅ 完整 | ⚡ 快 | ⭐⭐⭐⭐ | 不支援 HEAD 的伺服器 |
| get_headers() | 內建封裝 GET/HEAD | 視伺服器 | ✅ 是 | ✅ 完整 | 🐢 較慢 | ⭐⭐⭐ | 快速原型、無 cURL 擴展 |
| fopen() | 串流開啟 | 視實作 | ⚠️ 需 allow_url_fopen | ❌ 困難 | 🐢 慢 | ⭐⭐ | 舊專案相容 |
| fsockopen() | 原始 Socket | 手動控制 | ❌ 需手動 TLS | ✅ 手動解析 | 🐢 最慢 | ⭐ | 極特殊協定/教學 |
<?php
/**
* 檢查遠端 URL 是否存在 (HTTP 2xx/3xx)
*
* @param string $url 目標 URL
* @param int $timeout 逾時秒數
* @param bool $followRedirects 是否追蹤重導向
* @return array ['exists' => bool, 'http_code' => int, 'final_url' => string, 'headers' => array, 'error' => string|null]
*/
function checkRemoteExists(string $url, int $timeout = 10, bool $followRedirects = true): array {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_NOBODY => true, // HEAD 請求
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => $followRedirects,
CURLOPT_MAXREDIRS => 10,
CURLOPT_CONNECTTIMEOUT => $timeout,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; LinkChecker/1.0)',
CURLOPT_HEADER => true, // 取得 Header
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($errno) {
return [
'exists' => false,
'http_code' => 0,
'final_url' => $url,
'headers' => [],
'error' => "cURL 錯誤 ($errno): " . curl_strerror($errno),
];
}
$httpCode = $info['http_code'] ?? 0;
$headerStr = substr($response, 0, $info['header_size']);
$headers = parseRawHeaders($headerStr);
return [
'exists' => $httpCode >= 200 && $httpCode < 400,
'http_code' => $httpCode,
'final_url' => $info['url'] ?? $url,
'headers' => $headers,
'error' => null,
];
}
function parseRawHeaders(string $raw): array {
$headers = [];
$lines = explode("\r\n", trim($raw));
foreach ($lines as $line) {
if (strpos($line, ':') !== false) {
[$key, $value] = explode(':', $line, 2);
$key = strtolower(trim($key));
$headers[$key] = trim($value);
}
}
return $headers;
}
// 使用
$result = checkRemoteExists('https://example.com/image.jpg');
if ($result['exists']) {
echo "存在 (HTTP {$result['http_code']}),最終 URL: {$result['final_url']}";
echo "Content-Type: " . ($result['headers']['content-type'] ?? 'unknown');
} else {
echo "不存在或錯誤: " . ($result['error'] ?? "HTTP {$result['http_code']}");
}
function checkRemoteExistsGetRange(string $url, int $timeout = 10): array {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_CONNECTTIMEOUT => $timeout,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_RANGE => '0-0', // 只取第一個 byte
CURLOPT_HEADER => true,
]);
// ... 同 HEAD 解析邏輯
}
function checkRemoteExistsGetHeaders(string $url): array {
// 預設發送 GET,可用 stream_context 設定 HEAD
$ctx = stream_context_create([
'http' => ['method' => 'HEAD', 'timeout' => 10],
'https' => ['method' => 'HEAD', 'timeout' => 10],
]);
$headers = @get_headers($url, 1, $ctx); // 1 = 關聯陣列
if ($headers === false) {
return ['exists' => false, 'error' => 'get_headers 失敗'];
}
// 解析 HTTP 狀態碼
$statusLine = $headers[0] ?? '';
preg_match('/\d{3}/', $statusLine, $m);
$code = (int)($m[0] ?? 0);
return [
'exists' => $code >= 200 && $code < 400,
'http_code' => $code,
'headers' => $headers,
];
}
// 測試 100 個隨機圖片 URL,平均耗時
cURL HEAD (啟用 keep-alive) : 45 ms / request (連線複用)
cURL HEAD (獨立連線) : 120 ms / request
cURL GET Range : 130 ms / request
get_headers() : 200 ms / request
fopen() : 250 ms / request
fsockopen() : 300+ ms / request
關鍵優化:啟用 cURL Keep-Alive (連線複用) 批次檢查時效能提升 2-3 倍:
function batchCheckUrls(array $urls, int $concurrency = 10): Generator {
$mh = curl_multi_init();
$handles = [];
$results = [];
// 初始化所有 handle
foreach ($urls as $i => $url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_NOBODY => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_HEADER => true,
CURLOPT_PRIVATE => $url, // 標識
]);
curl_multi_add_handle($mh, $ch);
$handles[$i] = $ch;
}
// 並發執行
$running = null;
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh, 0.1); // 等待活動
while ($info = curl_multi_info_read($mh)) {
$ch = $info['handle'];
$url = curl_getinfo($ch, CURLINFO_PRIVATE);
$errno = curl_errno($ch);
$info = curl_getinfo($ch);
$response = curl_multi_getcontent($ch);
$headerStr = substr($response, 0, $info['header_size']);
$headers = parseRawHeaders($headerStr);
$results[$url] = [
'exists' => !$errno && ($info['http_code'] >= 200 && $info['http_code'] < 400),
'http_code' => $info['http_code'],
'final_url' => $info['url'],
'headers' => $headers,
'error' => $errno ? curl_strerror($errno) : null,
];
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
yield $url => $results[$url];
}
} while ($running);
curl_multi_close($mh);
}
// 使用
foreach (batchCheckUrls($imageUrls, 20) as $url => $result) {
// 即時處理結果
}
| 情況 | 處理策略 |
|---|---|
| 301/302 重導向 | 預設追蹤 (FOLLOWLOCATION),最終 URL 為 $info['url'] |
| 403 Forbidden (防盜鏈) | 視為「存在但不可存取」,依業務決定是否通過 |
| 404 Not Found | 明確不存在 |
| 5xx 伺服器錯誤 | 暫時性失敗,建議重試 (指數退避) |
| 大檔案 (影片/壓縮包) | HEAD 請求不下載 Body,極快;GET Range 僅 1 byte |
| 需攜帶 Cookie/Token | CURLOPT_COOKIE 或 CURLOPT_HTTPHEADER => ['Authorization: Bearer ...'] |
| CDN 邊緣節點不一致 | 多地區檢查、或信任源站回應 |