建立搜尋引擎、內容聚合、SEO 分析工具時,常需從任意 URL 擷取 <title>、<meta name="description">、<meta name="keywords"> 及 <body> 正文。本文提供一個健壯的 getPageContent() 函數,涵蓋 cURL 設定、編碼偵測、HTML 清洗、錯誤處理與效能優化。
<?php
/**
* 從 URL 擷取頁面核心 SEO 資訊與正文
*
* @param string $url 目標網址
* @param int $timeout 連線逾時秒數
* @return array [
* 'title' => string,
* 'description' => string,
* 'keywords' => string,
* 'body_text' => string, // 已移除 script/style/tag 的純文字
* 'html' => string, // 原始 HTML
* 'http_code' => int,
* 'charset' => string,
* 'error' => string|null
* ]
*/
function getPageContent(string $url, int $timeout = 10): array {
$result = [
'title' => '', 'description' => '', 'keywords' => '',
'body_text' => '', 'html' => '', 'http_code' => 0,
'charset' => 'UTF-8', 'error' => null,
];
// 1. 初始化 cURL
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true, // 跟隨 3xx 重導向
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; PHPBot/1.0; +https://example.com/bot)',
CURLOPT_SSL_VERIFYPEER => false, // 生產環境建議 true + CA 憑證
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_ENCODING => 'gzip,deflate', // 自動解壓縮
CURLOPT_HEADER => true, // 需要回應標頭以偵測編碼
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($errno) {
$result['error'] = "cURL 錯誤 ($errno): $error";
$result['http_code'] = 0;
return $result;
}
$result['http_code'] = $info['http_code'] ?? 0;
if ($result['http_code'] < 200 || $result['http_code'] >= 400) {
$result['error'] = "HTTP {$result['http_code']}";
return $result;
}
// 2. 分離標頭與主體
$header_size = $info['header_size'];
$headers = substr($response, 0, $header_size);
$html = substr($response, $header_size);
// 3. 偵測編碼:優先 Content-Type 標頭,其次 meta charset
$charset = 'UTF-8';
if (preg_match('/charset=([^\s;]+)/i', $headers, $m)) {
$charset = strtoupper(trim($m[1]));
} elseif (preg_match('/<meta[^>]+charset=["\']?([^"\'\s>]+)/i', $html, $m)) {
$charset = strtoupper(trim($m[1]));
}
$result['charset'] = $charset;
// 4. 轉為 UTF-8 內部處理
if ($charset !== 'UTF-8') {
$html = @iconv($charset, 'UTF-8//IGNORE', $html);
if ($html === false) {
$result['error'] = "編碼轉換失敗: $charset → UTF-8";
return $result;
}
}
$result['html'] = $html;
// 5. 擷取 title
if (preg_match('/<title[^>]*>(.*?)<\/title>/is', $html, $m)) {
$result['title'] = trim(strip_tags($m[1]));
}
// 6. 擷取 meta description
if (preg_match('/<meta[^>]+name=["\']description["\'][^>]+content=["\']([^"\']+)["\']/i', $html, $m)) {
$result['description'] = trim($m[1]);
} elseif (preg_match('/<meta[^>]+content=["\']([^"\']+)["\'][^>]+name=["\']description["\']/i', $html, $m)) {
$result['description'] = trim($m[1]);
}
// 7. 擷取 meta keywords
if (preg_match('/<meta[^>]+name=["\']keywords["\'][^>]+content=["\']([^"\']+)["\']/i', $html, $m)) {
$result['keywords'] = trim($m[1]);
} elseif (preg_match('/<meta[^>]+content=["\']([^"\']+)["\'][^>]+name=["\']keywords["\']/i', $html, $m)) {
$result['keywords'] = trim($m[1]);
}
// 8. 擷取 body 並清洗為純文字
if (preg_match('/<body[^>]*>(.*?)<\/body>/is', $html, $m)) {
$body = $m[1];
// 移除 script、style、noscript、iframe、svg 等非內容標籤
$body = preg_replace('/<(script|style|noscript|iframe|svg)[^>]*>.*?<\/\1>/is', '', $body);
// 移除 HTML 註解
$body = preg_replace('/<!--.*?-->/s', '', $body);
// 移除所有標籤,保留文字
$body = strip_tags($body);
// 解碼 HTML 實體
$body = html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// 正規化空白
$body = preg_replace('/\s+/u', ' ', $body);
$result['body_text'] = trim($body);
}
return $result;
}
// === 使用範例 ===
$url = 'https://www.php.net/';
$data = getPageContent($url);
if ($data['error']) {
echo "採集失敗: {$data['error']}\n";
} else {
echo "標題: {$data['title']}\n";
echo "描述: {$data['description']}\n";
echo "關鍵字: {$data['keywords']}\n";
echo "正文長度: " . mb_strlen($data['body_text']) . " 字元\n";
echo "編碼: {$data['charset']}\n";
echo "HTTP: {$data['http_code']}\n";
}
?>
CURLOPT_ENCODING => 'gzip,deflate':自動處理壓縮回應,節省頻寬。CURLOPT_HEADER => true:取得原始標頭以精準偵測 Content-Type: charset=...。CURLOPT_USERAGENT:模擬瀏覽器,避免被簡單反爬蟲攔截。CURLOPT_SSL_VERIFYPEER => false:開發測試用;生產請設為 true 並配置 CURLOPT_CAINFO。Content-Type: text/html; charset=GBK<meta charset="GBK"> 或 <meta http-equiv="Content-Type" content="text/html; charset=GBK">UTF-8
使用 iconv(..., '//IGNORE') 忽略無法轉換字元,避免中斷。
/is 修飾符:i 忽略大小寫、s (PCRE_DOTALL) 讓 . 匹配換行。name...content 與 content...name。.*? 避免跨標籤匹配。原始 body HTML → 移除 script/style/noscript/iframe/svg (含內容) → 移除 HTML 註解 → strip_tags() 移除所有標籤 → html_entity_decode() 解碼 < 等 → 正規化空白 (\n\r\t 多空白 → 單空格) → trim()
curl_multi_* 或 GuzzleHttp\Pool 並發請求,單機可達 50-200 QPS。robots.txt,尊重 Crawl-delay、Disallow。DOMDocument + XPath 或 Masterminds\HTML5 解析器,比正則更健壯。Text_LanguageDetect 或 cld3 自動識別頁面語言。| 現象 | 原因 | 解決 |
|---|---|---|
| 中文亂碼 | 編碼偵測失敗或 iconv 不支援該編碼 | 加入 mb_detect_encoding 兜底、安裝 iconv 完整版 |
| 取不到 title/description | 頁面用 JS 動態渲染 (SPA) | 需改用 Headless Chrome (Puppeteer/Playwright) 渲染後再擷取 |
| cURL 60 錯誤 | SSL 憑證驗證失敗 | 設定 CURLOPT_CAINFO 指向 cacert.pem |
| 超時 | 目標伺服器回應慢或封鎖 | 調大 timeout、使用代理 IP 池 |
| 正文含大量垃圾文字 | strip_tags 保留了 script/style 內容 | 先用正則移除 script/style 標籤及內容,再 strip_tags |
getPageContent() 以 cURL + 正則實現輕量、無依賴的網頁核心資訊擷取。關鍵在於 標頭+Meta 雙重編碼偵測、防禦性正則匹配屬性順序、分層清洗管線產出純文字。對於大規模、高併發、JS 渲染頁面場景,建議遷移至 Guzzle + 佇列 + Headless Chrome 架構,但核心擷取邏輯 (編碼、清洗、欄位提取) 仍完全適用。