不依賴 cURL、allow_url_fopen,純 PHP Socket 發送 HEAD 請求解析 Location Header,支援相對/絕對路徑重定向、多層跳轉檢測與循環保護。
/**
* 獲取 URL 的直接重定向目標
*
* @param string $url 完整 URL(含 scheme)
* @return string|false 重定向目標 URL,無重定向或失敗返回 false
*/
function get_redirect_url(string $url): string|false {
$parts = @parse_url($url);
if (!$parts || !isset($parts['host'])) return false;
$parts['path'] = $parts['path'] ?? '/';
$port = $parts['port'] ?? 80;
$scheme = $parts['scheme'] ?? 'http';
// 建立 Socket 連接
$fp = @fsockopen($parts['host'], $port, $errno, $errstr, 10);
if (!$fp) return false;
// 發送 HEAD 請求
$request = "HEAD {$parts['path']}" . (isset($parts['query']) ? "?{$parts['query']}" : "") . " HTTP/1.1\r\n";
$request .= "Host: {$parts['host']}\r\n";
$request .= "Connection: Close\r\n\r\n";
fwrite($fp, $request);
// 讀取響應頭
$response = '';
while (!feof($fp)) {
$response .= fread($fp, 8192);
}
fclose($fp);
// 解析 Location
if (preg_match('/^Location:\s*(.+)$/mi', $response, $m)) {
$location = trim($m[1]);
// 處理相對路徑
if ($location[0] === '/') {
return "{$scheme}://{$parts['host']}{$location}";
}
// 處理相對路徑(無 scheme)
if (strpos($location, '://') === false && $location[0] !== '/') {
$base = rtrim("{$scheme}://{$parts['host']}{$parts['path']}", '/');
return $base . '/' . ltrim($location, '/');
}
return $location;
}
return false;
}
/**
* 追蹤完整重定向鏈
*
* @param string $url 起始 URL
* @param int $max_depth 最大跳轉層數,防無限循環
* @return array 重定向鏈 [url1, url2, ...]
*/
function get_all_redirects(string $url, int $max_depth = 10): array {
$chain = [];
$visited = [];
for ($i = 0; $i < $max_depth; $i++) {
if (in_array($url, $visited, true)) break; // 循環檢測
$visited[] = $url;
$next = get_redirect_url($url);
if (!$next) break;
$chain[] = $next;
$url = $next;
}
return $chain;
}
$start = 'https://example.com/short-link';
$chain = get_all_redirects($start);
echo "起始: $start\n";
foreach ($chain as $i => $u) {
echo " {$i}. $u\n";
}
echo "最終: " . ($chain ? end($chain) : $start) . "\n";
// 輸出示例
// 起始: https://example.com/short-link
// 0. https://example.com/real-page
// 1. https://target.com/final?from=example
// 最終: https://target.com/final?from=example
function get_redirect_url_ssl(string $url): string|false {
$parts = @parse_url($url);
if (!$parts || !isset($parts['host'])) return false;
$scheme = $parts['scheme'] ?? 'http';
$host = $parts['host'];
$port = $parts['port'] ?? ($scheme === 'https' ? 443 : 80);
$target = ($scheme === 'https' ? 'ssl://' : 'tcp://') . $host . ":{$port}";
$fp = @fsockopen($target, $port, $errno, $errstr, 10);
if (!$fp) return false;
// 其餘邏輯同 HTTP,注意 Host header 需包含端口(非標準端口時)
$host_header = $port != ($scheme === 'https' ? 443 : 80) ? "$host:$port" : $host;
// ...
}
function get_redirect_curl(string $url): string|false {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_NOBODY => true, // HEAD
CURLOPT_FOLLOWLOCATION => false, // 手動處理
CURLOPT_HEADER => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false, // 視安全策略
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 300 && $code < 400) {
if (preg_match('/^Location:\s*(.+)$/mi', $resp, $m)) {
return trim($m[1]);
}
}
return false;
}
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Socket HEAD | 零依賴、輕量、可控 | 需手動處理 HTTPS、Chunked、壓縮、重試 | 受限環境、簡單爬蟲、教學 |
| cURL | 完整協議支援、高性能、成熟 | 需安裝擴展 | 生產環境、複雜重定向、生產爬蟲 |
| get_headers() | 一行代碼 | 無法禁用自動跳轉、無超時控制、錯誤處理弱 | 快速腳本、調試 |
Location: /new-path 或 Location: ../other 需基於當前 URL 解析;Location/location 均可能;rawurlencode;掌握底層 Socket 實現能深入理解 HTTP 重定向機制,生產環境請優先使用成熟的 cURL 或 Guzzle。