首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP CURL 爬取網頁標題:從編碼偵測到跨域處理的完整實作

     編輯:PHP 相關知識

需求:可靠取得任意 URL 的 <title>

看似簡單的「抓標題」,實際面臨:編碼偵測 (Big5/UTF-8/GBK)、HTML 解析容錯、重導向、超時控制、User-Agent 偽裝、大檔案截斷等挑戰。本文提供生產級可用函式。

核心函式:fetchPageTitle(string $url): ?string

<?php
/**
 * 取得網頁標題
 * @param string $url 目標網址
 * @param int $timeout 總超時秒數
 * @param int $maxSize 最大讀取位元組 (防止下載巨大檔案)
 * @return string|null 成功回傳標題,失敗回傳 null
 */
function fetchPageTitle(string $url, int $timeout = 10, int $maxSize = 2_000_000): ?string {
    // 1. 基本驗證
    if (!filter_var($url, FILTER_VALIDATE_URL)) return null;

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; TitleBot/1.0; +https://example.com/bot)',
        CURLOPT_ENCODING       => 'gzip, deflate', // 自動解壓
        CURLOPT_SSL_VERIFYPEER => true,            // 生產環境建議開啟
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_WRITEFUNCTION  => function ($ch, $data) use ($maxSize, &$buffer) {
            $buffer .= $data;
            if (strlen($buffer) > $maxSize) return 0; // 停止下載
            return strlen($data);
        },
        CURLOPT_HEADER         => true, // 需要 header 解析 Content-Type charset
    ]);

    $buffer = '';
    $response = curl_exec($ch);
    $errno = curl_errno($ch);
    $info = curl_getinfo($ch);
    curl_close($ch);

    if ($errno !== CURLE_OK) {
        error_log("CURL error {$errno} for {$url}: " . curl_strerror($errno));
        return null;
    }

    // 2. 分離 header / body
    $headerSize = $info['header_size'];
    $headers = substr($response, 0, $headerSize);
    $body = substr($response, $headerSize);

    // 3. 從 HTTP Header 取得 charset
    $charset = 'UTF-8'; // 預設
    if (preg_match('/charset\s*=\s*([^\s;]+)/i', $headers, $m)) {
        $charset = strtoupper(trim($m[1]));
    }

    // 4. 從 HTML meta 補充偵測 (若 header 無 charset)
    if ($charset === 'UTF-8') {
        if (preg_match('/]+charset=["\']?([^"\'>\s]+)/i', $body, $m)) {
            $charset = strtoupper(trim($m[1]));
        }
    }

    // 5. 正規化 charset 別名
    $charsetMap = [
        'UTF8' => 'UTF-8', 'UTF-8' => 'UTF-8',
        'BIG5' => 'BIG-5', 'BIG-5' => 'BIG-5',
        'GBK' => 'GBK', 'GB2312' => 'GBK', 'GB18030' => 'GBK',
        'ISO-8859-1' => 'ISO-8859-1', 'LATIN1' => 'ISO-8859-1',
    ];
    $charset = $charsetMap[$charset] ?? 'UTF-8';

    // 6. 轉碼至 UTF-8
    if ($charset !== 'UTF-8') {
        $body = @iconv($charset, 'UTF-8//IGNORE', $body);
        if ($body === false) $body = ''; // iconv 失敗
    }

    // 7. 提取 title (支援跨行、屬性、大小寫)
    if (preg_match('/]*>(.*?)<\/title>/is', $body, $m)) {
        $title = trim($m[1]);
        // 移除多餘空白、換行
        $title = preg_replace('/\s+/', ' ', $title);
        return $title !== '' ? $title : null;
    }

    return null;
}

使用範例

<?php
$urls = [
    'https://www.google.com',
    'https://www.ptt.cc/bbs/Gossiping/index.html', // Big5
    'https://www.sohu.com', // GBK
];

foreach ($urls as $url) {
    $title = fetchPageTitle($url);
    echo "{$url} => " . ($title ?? 'FAILED') . "\n";
}

進階擴充:並行抓取 (CURL Multi)

大量 URL 時,使用 curl_multi_* 並行可大幅縮短總耗時:

function fetchTitlesMulti(array $urls, int $concurrency = 10): array {
    $mh = curl_multi_init();
    $handles = [];
    $results = [];

    foreach ($urls as $url) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_TIMEOUT        => 10,
            CURLOPT_USERAGENT      => 'TitleBot/1.0',
            CURLOPT_WRITEFUNCTION  => function($ch, $data) use (&$buffers, $url) {
                $buffers[$url] ??= '';
                $buffers[$url] .= $data;
                return strlen($data);
            },
        ]);
        curl_multi_add_handle($mh, $ch);
        $handles[$url] = $ch;
    }

    $running = null;
    do {
        curl_multi_exec($mh, $running);
        curl_multi_select($mh, 0.5);
    } while ($running > 0);

    foreach ($handles as $url => $ch) {
        $body = $buffers[$url] ?? '';
        $results[$url] = extractTitle($body); // 使用上述提取邏輯
        curl_multi_remove_handle($mh, $ch);
        curl_close($ch);
    }
    curl_multi_close($mh);
    return $results;
}

常見坑點與對策

現象原因解決
抓到亂碼編碼偵測失敗優先信任 HTTP Header,次信任 meta,最後 fallback UTF-8;使用 //IGNORE 容錯
超時/無回應目標站回應慢或封鎖調整 CURLOPT_CONNECTTIMEOUT/CURLOPT_TIMEOUT;更換 User-Agent;使用代理池
抓到 JS 渲染後的標題SPA 網站 title 由 JS 設定需改用 Headless Chrome (Puppeteer/Playwright) 或檢查 JSON-LD/og:title
記憶體爆增下載巨大頁面/圖片設定 CURLOPT_WRITEFUNCTION 截斷;或先 HEAD 檢查 Content-Length 與 Content-Type
SSL 憑證驗證失敗目標站憑證過期/自簽生產環境不要關閉驗證;改用 CURLOPT_CAINFO 指定信任 CA

替代方案:DOMDocument 解析

正則對付 HTML 雖快但脆弱,嚴謹專案建議:

$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML($body, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
libxml_clear_errors();
$titleNode = $dom->getElementsByTagName('title')->item(0);
$title = $titleNode ? trim($titleNode->textContent) : null;

完整 Library 化版本 (含 PSR-4、單元測試、Guzzle 介面) 見 GitHub。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved